C# & .NET
Overview
Modern C# and .NET development patterns including async programming, LINQ, and ASP.NET Core.
Modern C# Features
Records and Init-Only Properties
// Record type (immutable by default) public record User( string Id, string Email, string Name, DateTime CreatedAt );
// With-expressions for immutable updates var user = new User("1", "test@example.com", "Test User", DateTime.UtcNow); var updated = user with { Name = "Updated Name" };
// Record with validation public record ValidatedUser { public required string Id { get; init; } public required string Email { get; init; } public required string Name { get; init; }
public ValidatedUser()
{
// Validation in constructor
}
// Custom validation
public static ValidatedUser Create(string email, string name)
{
if (!email.Contains("@"))
throw new ArgumentException("Invalid email");
return new ValidatedUser
{
Id = Guid.NewGuid().ToString(),
Email = email,
Name = name
};
}
}
// Init-only properties public class Config { public required string ConnectionString { get; init; } public int Timeout { get; init; } = 30; }
Pattern Matching
// Type patterns public string Describe(object obj) => obj switch { string s => $"String of length {s.Length}", int i when i > 0 => $"Positive integer: {i}", int i => $"Non-positive integer: {i}", IEnumerable<int> list => $"Integer list with {list.Count()} items", null => "Null value", _ => "Unknown type" };
// Property patterns public decimal CalculateDiscount(Order order) => order switch { { Total: > 1000, Customer.IsPremium: true } => order.Total * 0.2m, { Total: > 1000 } => order.Total * 0.1m, { Customer.IsPremium: true } => order.Total * 0.05m, _ => 0 };
// List patterns (C# 11) public string DescribeList(int[] numbers) => numbers switch { [] => "Empty", [var single] => $"Single element: {single}", [var first, var second] => $"Two elements: {first}, {second}", [var first, .. var middle, var last] => $"First: {first}, Last: {last}, Middle count: {middle.Length}", };
// Positional patterns with deconstruct public record Point(int X, int Y);
public string Quadrant(Point point) => point switch { (0, 0) => "Origin", (> 0, > 0) => "First quadrant", (< 0, > 0) => "Second quadrant", (< 0, < 0) => "Third quadrant", (> 0, < 0) => "Fourth quadrant", (_, 0) or (0, _) => "On an axis" };
Nullable Reference Types
#nullable enable
public class UserService { // Non-nullable (compiler ensures not null) public User GetUser(string id) { return _repository.Find(id) ?? throw new NotFoundException($"User {id} not found"); }
// Nullable return
public User? FindUser(string id)
{
return _repository.Find(id);
}
// Null handling
public string GetDisplayName(User? user)
{
// Null-conditional
var name = user?.Name;
// Null-coalescing
return user?.Name ?? "Anonymous";
// Null-coalescing assignment
// user ??= CreateDefaultUser();
}
// Null-forgiving operator (use sparingly)
public void ProcessUser(User? user)
{
// When you know it's not null but compiler doesn't
var name = user!.Name;
}
}
// Required members (C# 11) public class RequiredUser { public required string Id { get; init; } public required string Email { get; init; } public string? Nickname { get; init; } }
Async Programming
using System.Threading.Tasks; using System.Threading;
public class AsyncService { // Basic async method public async Task<User> GetUserAsync(string id, CancellationToken ct = default) { var response = await _httpClient.GetAsync($"/users/{id}", ct); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<User>(ct) ?? throw new InvalidOperationException("Null response"); }
// Parallel execution
public async Task<IReadOnlyList<User>> GetUsersAsync(IEnumerable<string> ids)
{
var tasks = ids.Select(id => GetUserAsync(id));
return await Task.WhenAll(tasks);
}
// With error handling
public async Task<Result<User>> SafeGetUserAsync(string id)
{
try
{
var user = await GetUserAsync(id);
return Result<User>.Success(user);
}
catch (Exception ex)
{
return Result<User>.Failure(ex.Message);
}
}
// ValueTask for potentially synchronous operations
public ValueTask<User?> GetCachedUserAsync(string id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user);
}
return new ValueTask<User?>(FetchAndCacheUserAsync(id));
}
// Async enumerable (IAsyncEnumerable)
public async IAsyncEnumerable<User> StreamUsersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
var page = 1;
while (true)
{
var users = await FetchPageAsync(page, ct);
if (!users.Any()) yield break;
foreach (var user in users)
{
yield return user;
}
page++;
}
}
// Consuming async enumerable
public async Task ProcessAllUsersAsync()
{
await foreach (var user in StreamUsersAsync())
{
await ProcessUserAsync(user);
}
}
}
// Channels for producer-consumer public class ChannelExample { private readonly Channel<Message> _channel = Channel.CreateBounded<Message>(100);
public async Task ProduceAsync(Message message)
{
await _channel.Writer.WriteAsync(message);
}
public async Task ConsumeAsync(CancellationToken ct)
{
await foreach (var message in _channel.Reader.ReadAllAsync(ct))
{
await ProcessMessageAsync(message);
}
}
}
LINQ
using System.Linq;
public class LinqExamples { // Query syntax public IEnumerable<User> GetActiveUsers(IEnumerable<User> users) { return from user in users where user.IsActive orderby user.Name select user; }
// Method syntax (more common)
public IEnumerable<string> GetActiveUserEmails(IEnumerable<User> users)
{
return users
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.Select(u => u.Email);
}
// Grouping
public IDictionary<string, List<User>> GroupByDomain(IEnumerable<User> users)
{
return users
.GroupBy(u => u.Email.Split('@')[1])
.ToDictionary(g => g.Key, g => g.ToList());
}
// Join
public IEnumerable<OrderWithUser> JoinOrdersWithUsers(
IEnumerable<Order> orders,
IEnumerable<User> users)
{
return orders.Join(
users,
order => order.UserId,
user => user.Id,
(order, user) => new OrderWithUser(order, user));
}
// Aggregate
public OrderStats GetOrderStats(IEnumerable<Order> orders)
{
return new OrderStats(
Count: orders.Count(),
Total: orders.Sum(o => o.Amount),
Average: orders.Average(o => o.Amount),
Max: orders.Max(o => o.Amount)
);
}
// SelectMany (flatten)
public IEnumerable<OrderItem> GetAllItems(IEnumerable<Order> orders)
{
return orders.SelectMany(o => o.Items);
}
// Complex pipeline
public IEnumerable<UserSummary> GetTopCustomers(
IEnumerable<User> users,
IEnumerable<Order> orders)
{
return orders
.GroupBy(o => o.UserId)
.Select(g => new
{
UserId = g.Key,
TotalSpent = g.Sum(o => o.Amount),
OrderCount = g.Count()
})
.OrderByDescending(x => x.TotalSpent)
.Take(10)
.Join(users, x => x.UserId, u => u.Id, (x, u) => new UserSummary(
u.Name,
x.TotalSpent,
x.OrderCount
));
}
}
Dependency Injection
using Microsoft.Extensions.DependencyInjection;
// Service registration public static class ServiceCollectionExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services) { // Transient - new instance each time services.AddTransient<IEmailService, EmailService>();
// Scoped - one per request
services.AddScoped<IUserRepository, UserRepository>();
// Singleton - one instance for app lifetime
services.AddSingleton<ICacheService, RedisCacheService>();
// Factory registration
services.AddScoped<IDbConnection>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new SqlConnection(config.GetConnectionString("Default"));
});
// Options pattern
services.Configure<EmailOptions>(configuration.GetSection("Email"));
return services;
}
}
// Constructor injection public class UserService { private readonly IUserRepository _repository; private readonly IEmailService _emailService; private readonly ILogger<UserService> _logger;
public UserService(
IUserRepository repository,
IEmailService emailService,
ILogger<UserService> logger)
{
_repository = repository;
_emailService = emailService;
_logger = logger;
}
public async Task<User> CreateUserAsync(CreateUserRequest request)
{
_logger.LogInformation("Creating user {Email}", request.Email);
var user = new User(request.Email, request.Name);
await _repository.AddAsync(user);
await _emailService.SendWelcomeEmailAsync(user);
return user;
}
}
// Primary constructor (C# 12) public class UserServiceNew( IUserRepository repository, IEmailService emailService, ILogger<UserServiceNew> logger) { public async Task<User> CreateUserAsync(CreateUserRequest request) { logger.LogInformation("Creating user {Email}", request.Email); // Use repository, emailService directly return null!; } }
Generic Patterns
// Generic repository public interface IRepository<T> where T : class, IEntity { Task<T?> FindAsync(string id); Task<IReadOnlyList<T>> FindAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(string id); }
public class Repository<T> : IRepository<T> where T : class, IEntity { private readonly DbContext _context;
public Repository(DbContext context)
{
_context = context;
}
public async Task<T?> FindAsync(string id)
{
return await _context.Set<T>().FindAsync(id);
}
public async Task<IReadOnlyList<T>> FindAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
}
// ...
}
// Generic constraints public class Service<TEntity, TId> where TEntity : class, IEntity<TId>, new() where TId : struct { // ... }
// Covariance and contravariance public interface IProducer<out T> { T Produce(); }
public interface IConsumer<in T> { void Consume(T item); }
Error Handling
// Result type pattern public readonly struct Result<T> { public T? Value { get; } public string? Error { get; } public bool IsSuccess => Error is null;
private Result(T value)
{
Value = value;
Error = null;
}
private Result(string error)
{
Value = default;
Error = error;
}
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(string error) => new(error);
public TResult Match<TResult>(
Func<T, TResult> success,
Func<string, TResult> failure) =>
IsSuccess ? success(Value!) : failure(Error!);
}
// Custom exceptions public class DomainException : Exception { public string Code { get; }
public DomainException(string code, string message)
: base(message)
{
Code = code;
}
}
public class ValidationException : DomainException { public IDictionary<string, string[]> Errors { get; }
public ValidationException(IDictionary<string, string[]> errors)
: base("VALIDATION_ERROR", "Validation failed")
{
Errors = errors;
}
}
// Exception filter middleware public class ExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionMiddleware> _logger;
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { ex.Code, ex.Errors });
}
catch (NotFoundException ex)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new { ex.Code, ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { Code = "INTERNAL_ERROR" });
}
}
}
Related Skills
-
[[backend]] - ASP.NET Core development
-
[[database]] - Entity Framework Core
-
[[testing]] - xUnit, NUnit