Claude-skill-registry lang-csharp-dev
Foundational C# patterns covering LINQ, async/await, nullable types, records, and pattern matching. Use when writing C# code or needing guidance on C# features. This is the entry point for C# development.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-csharp-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-csharp-dev && rm -rf "$T"
skills/data/lang-csharp-dev/SKILL.mdC# Development Skill
Comprehensive foundational patterns for modern C# development covering language features, best practices, and common idioms.
Quick Reference
Essential Patterns
// Nullable reference types string? nullableString = null; string nonNullableString = "value"; // Records public record Person(string Name, int Age); // Pattern matching var result = value switch { null => "null", 0 => "zero", > 0 => "positive", _ => "negative" }; // LINQ method syntax var results = collection .Where(x => x.IsActive) .Select(x => x.Name) .OrderBy(x => x) .ToList(); // Async/await public async Task<string> GetDataAsync() { return await httpClient.GetStringAsync(url); }
File Extensions
- C# source files.cs
- Project files.csproj
- Solution files.sln
- Razor views.cshtml
- Blazor components.razor
1. Nullable Reference Types
Overview
Nullable reference types help prevent null reference exceptions by making nullability explicit in the type system.
Enabling Nullable Context
// In .csproj <PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup> // Or per-file #nullable enable // Disable warnings #nullable disable
Nullable Annotations
// Nullable reference type string? nullableString = null; // Non-nullable reference type (default when nullable context enabled) string nonNullableString = "value"; // Array of nullable strings string?[] arrayOfNullableStrings = new string?[10]; // Nullable array of strings string[]? nullableArrayOfStrings = null; // Nullable array of nullable strings string?[]? fullyNullable = null;
Null-Forgiving Operator
// When you know a value isn't null but compiler doesn't string value = GetValue()!; // Use sparingly - defeats purpose of nullable reference types public void Process(string? input) { // Bad - suppresses warning without checking Console.WriteLine(input!.Length); // Good - check first if (input is not null) { Console.WriteLine(input.Length); } }
Null Checking Patterns
// Traditional null check if (value != null) { Console.WriteLine(value.Length); } // Pattern matching if (value is not null) { Console.WriteLine(value.Length); } // Null-conditional operator Console.WriteLine(value?.Length); // Null-coalescing operator string result = value ?? "default"; // Null-coalescing assignment value ??= "default";
Method Annotations
// Return nullable public string? FindUser(int id) { return users.FirstOrDefault(u => u.Id == id)?.Name; } // Accept nullable public void UpdateName(string? newName) { if (newName is null) { throw new ArgumentNullException(nameof(newName)); } name = newName; } // Attributes for advanced scenarios public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) { // Tells compiler that value is not null when method returns true return dictionary.TryGetValue(key, out value); } [return: NotNullIfNotNull(nameof(input))] public string? Transform(string? input) { // Return value nullability matches input nullability return input?.ToUpper(); }
Generic Nullability
// Nullable value type public class Container<T> { public T? Value { get; set; } // Works for both reference and value types } // Constrain to non-nullable reference types public class Container<T> where T : notnull { public T Value { get; set; } = default!; } // Nullable reference type constraint public class Container<T> where T : class? { public T? Value { get; set; } }
Best Practices
// DO: Enable nullable context globally // In .csproj <Nullable>enable</Nullable> // DO: Check for null before use public void Process(string? input) { ArgumentNullException.ThrowIfNull(input); // C# 11+ // or if (input is null) { throw new ArgumentNullException(nameof(input)); } Console.WriteLine(input.Length); } // DO: Use nullable return types when appropriate public User? FindUser(int id) => users.FirstOrDefault(u => u.Id == id); // DON'T: Overuse null-forgiving operator // Bad public void Bad(string? input) { Console.WriteLine(input!.Length); } // Good public void Good(string? input) { if (input is not null) { Console.WriteLine(input.Length); } } // DO: Initialize non-nullable properties public class User { public string Name { get; set; } = string.Empty; // Good public string Email { get; set; } // Warning: non-nullable field must contain non-null value }
2. LINQ (Language Integrated Query)
Overview
LINQ provides a consistent model for querying data across different data sources using both query and method syntax.
Query Syntax
// Basic query var results = from user in users where user.Age > 18 select user.Name; // Multiple from clauses (SelectMany) var pairs = from user in users from order in user.Orders where order.Total > 100 select new { user.Name, order.Id }; // Join var results = from user in users join order in orders on user.Id equals order.UserId select new { user.Name, order.Total }; // Group join (left join) var results = from user in users join order in orders on user.Id equals order.UserId into userOrders select new { user.Name, Orders = userOrders }; // Group by var grouped = from user in users group user by user.Department into g select new { Department = g.Key, Count = g.Count() }; // Order by var ordered = from user in users orderby user.LastName, user.FirstName descending select user; // Let clause var results = from user in users let fullName = $"{user.FirstName} {user.LastName}" where fullName.Length > 10 select fullName;
Method Syntax
// Filtering var adults = users.Where(u => u.Age >= 18); // Projection var names = users.Select(u => u.Name); var dto = users.Select(u => new UserDto { Name = u.Name, Email = u.Email }); // Ordering var sorted = users.OrderBy(u => u.LastName) .ThenByDescending(u => u.FirstName); // Grouping var grouped = users.GroupBy(u => u.Department) .Select(g => new { Department = g.Key, Count = g.Count() }); // Joining var results = users.Join( orders, u => u.Id, o => o.UserId, (u, o) => new { u.Name, o.Total } ); // SelectMany (flattening) var allOrders = users.SelectMany(u => u.Orders); var pairs = users.SelectMany( u => u.Orders, (u, o) => new { u.Name, o.Id } ); // Aggregation var total = orders.Sum(o => o.Total); var average = orders.Average(o => o.Total); var max = orders.Max(o => o.Total); var count = orders.Count(o => o.IsCompleted); // Quantifiers var hasAny = orders.Any(o => o.Total > 1000); var allCompleted = orders.All(o => o.IsCompleted); // Element operations var first = users.First(u => u.Id == 1); // Throws if not found var firstOrNull = users.FirstOrDefault(u => u.Id == 1); // Returns null/default var single = users.Single(u => u.Email == email); // Throws if 0 or >1 matches
Deferred vs. Immediate Execution
// Deferred execution - query not executed until enumerated IEnumerable<User> query = users.Where(u => u.Age > 18); // Query executes here when enumerating foreach (var user in query) { } // Immediate execution - query executes immediately List<User> list = users.Where(u => u.Age > 18).ToList(); User[] array = users.Where(u => u.Age > 18).ToArray(); Dictionary<int, User> dict = users.ToDictionary(u => u.Id); // Aggregation methods execute immediately int count = users.Count(); decimal total = orders.Sum(o => o.Total);
Complex LINQ Patterns
// Conditional where clauses var query = users.AsQueryable(); if (!string.IsNullOrEmpty(searchTerm)) { query = query.Where(u => u.Name.Contains(searchTerm)); } if (minAge.HasValue) { query = query.Where(u => u.Age >= minAge.Value); } var results = query.ToList(); // Nested queries var usersWithExpensiveOrders = users .Where(u => u.Orders.Any(o => o.Total > 1000)) .Select(u => new { u.Name, ExpensiveOrders = u.Orders.Where(o => o.Total > 1000) }); // Distinct var uniqueAges = users.Select(u => u.Age).Distinct(); var uniqueUsers = users.DistinctBy(u => u.Email); // C# 11+ // Set operations var union = list1.Union(list2); var intersect = list1.Intersect(list2); var except = list1.Except(list2); // Partitioning var page = users.Skip(pageSize * pageNumber).Take(pageSize); // Zip var pairs = list1.Zip(list2, (x, y) => new { x, y }); // Chunk (C# 11+) var batches = users.Chunk(100); foreach (var batch in batches) { ProcessBatch(batch); }
LINQ to Objects Performance
// DO: Use List<T> or array for known collections List<User> users = GetUsers(); var results = users.Where(u => u.Age > 18); // Fast iteration // DO: Materialize once if reusing query results var activeUsers = users.Where(u => u.IsActive).ToList(); var count = activeUsers.Count; var first = activeUsers.First(); // DON'T: Materialize unnecessarily // Bad - Count() can work on IEnumerable var badCount = users.Where(u => u.IsActive).ToList().Count(); // Good var goodCount = users.Count(u => u.IsActive); // DO: Filter before projecting // Good var names = users.Where(u => u.Age > 18).Select(u => u.Name); // Less efficient var names2 = users.Select(u => u.Name).Where(n => users.First(u => u.Name == n).Age > 18); // DO: Use appropriate methods // Good - short circuits bool hasAdmin = users.Any(u => u.Role == "Admin"); // Bad - checks entire collection bool hasAdmin2 = users.Where(u => u.Role == "Admin").Count() > 0;
Queryable vs. Enumerable
// IEnumerable<T> - LINQ to Objects (in-memory) IEnumerable<User> enumerable = users.Where(u => u.Age > 18); // IQueryable<T> - LINQ provider translates to data source query IQueryable<User> queryable = dbContext.Users.Where(u => u.Age > 18); // AsQueryable converts IEnumerable to IQueryable (still executes in memory) IQueryable<User> query = users.AsQueryable().Where(u => u.Age > 18); // AsEnumerable forces remaining query to execute in memory var results = dbContext.Users .Where(u => u.Age > 18) // Translated to SQL .AsEnumerable() .Where(u => ComplexInMemoryCheck(u)); // Executes in memory
3. Async/Await Patterns
Overview
Async/await enables non-blocking asynchronous operations while maintaining readable code.
Basic Async/Await
// Async method returning Task public async Task ProcessDataAsync() { await Task.Delay(1000); Console.WriteLine("Processed"); } // Async method returning Task<T> public async Task<string> GetDataAsync() { var result = await httpClient.GetStringAsync(url); return result; } // Async void - only for event handlers private async void Button_Click(object sender, EventArgs e) { await ProcessDataAsync(); }
Task Basics
// Creating tasks Task task = Task.Run(() => DoWork()); Task<int> taskWithResult = Task.Run(() => CalculateValue()); // Completing immediately Task<string> completed = Task.FromResult("value"); Task failed = Task.FromException(new Exception("error")); Task canceled = Task.FromCanceled(cancellationToken); // Waiting (blocks current thread - avoid in async code) task.Wait(); int result = taskWithResult.Result; // Async waiting (doesn't block thread) await task; int result = await taskWithResult;
ConfigureAwait
// Library code - don't capture synchronization context public async Task<string> LibraryMethodAsync() { var result = await httpClient.GetStringAsync(url) .ConfigureAwait(false); return result; } // UI/ASP.NET Core code - usually omit (capture context) public async Task ButtonClickAsync() { var data = await GetDataAsync(); // Returns to UI thread textBox.Text = data; // Can update UI } // When to use ConfigureAwait(false) // - Library code that doesn't need synchronization context // - Improves performance by avoiding context capture // - Prevents potential deadlocks // When to omit ConfigureAwait or use ConfigureAwait(true) // - UI code that needs to update controls // - ASP.NET code that needs HttpContext // - Code that depends on synchronization context
Parallel Async Operations
// Run tasks concurrently and wait for all Task<string> task1 = GetDataAsync(url1); Task<string> task2 = GetDataAsync(url2); Task<string> task3 = GetDataAsync(url3); await Task.WhenAll(task1, task2, task3); string result1 = task1.Result; // Already completed string result2 = task2.Result; string result3 = task3.Result; // With results var tasks = new[] { GetDataAsync(url1), GetDataAsync(url2), GetDataAsync(url3) }; string[] results = await Task.WhenAll(tasks); // Wait for first to complete Task<string> firstCompleted = await Task.WhenAny(task1, task2, task3); string firstResult = await firstCompleted; // Process as they complete var tasks = urls.Select(url => GetDataAsync(url)).ToList(); while (tasks.Count > 0) { Task<string> completedTask = await Task.WhenAny(tasks); tasks.Remove(completedTask); string result = await completedTask; ProcessResult(result); }
Cancellation
// Creating cancellation token source using var cts = new CancellationTokenSource(); // Cancel after timeout cts.CancelAfter(TimeSpan.FromSeconds(30)); // Manual cancellation cts.Cancel(); // Passing token to async method await ProcessDataAsync(cts.Token); // Implementing cancellation public async Task ProcessDataAsync(CancellationToken cancellationToken) { for (int i = 0; i < 1000; i++) { // Check for cancellation cancellationToken.ThrowIfCancellationRequested(); // Or manual check if (cancellationToken.IsCancellationRequested) { // Cleanup return; } await ProcessItemAsync(i, cancellationToken); } } // Linking tokens using var linkedCts = CancellationTokenSource .CreateLinkedTokenSource(token1, token2); await ProcessAsync(linkedCts.Token); // Registering callback cancellationToken.Register(() => { Console.WriteLine("Cancellation requested"); });
Error Handling
// Try-catch with async public async Task<string> GetDataWithErrorHandlingAsync() { try { return await httpClient.GetStringAsync(url); } catch (HttpRequestException ex) { logger.LogError(ex, "HTTP request failed"); throw; } catch (Exception ex) { logger.LogError(ex, "Unexpected error"); return string.Empty; } } // Multiple tasks - exceptions aggregated try { await Task.WhenAll(task1, task2, task3); } catch (Exception ex) { // Only first exception is caught logger.LogError(ex, "At least one task failed"); } // To get all exceptions var tasks = new[] { task1, task2, task3 }; try { await Task.WhenAll(tasks); } catch { foreach (var task in tasks) { if (task.IsFaulted) { logger.LogError(task.Exception, "Task failed"); } } } // Handling faulted tasks if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { var result = task.Result; }
Async Enumerable (IAsyncEnumerable)
// Async iterator public async IAsyncEnumerable<int> GenerateNumbersAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { for (int i = 0; i < 100; i++) { await Task.Delay(100, cancellationToken); yield return i; } } // Consuming async enumerable await foreach (var number in GenerateNumbersAsync()) { Console.WriteLine(number); } // With cancellation await foreach (var number in GenerateNumbersAsync(cancellationToken)) { Console.WriteLine(number); } // Real-world example - streaming API results public async IAsyncEnumerable<User> StreamUsersAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { int page = 0; while (true) { var users = await GetPageAsync(page, cancellationToken); if (users.Count == 0) break; foreach (var user in users) { yield return user; } page++; } }
ValueTask
// Use ValueTask when result is often available synchronously public ValueTask<int> GetCachedValueAsync(string key) { if (cache.TryGetValue(key, out int value)) { return new ValueTask<int>(value); // Synchronous completion } return new ValueTask<int>(FetchFromDatabaseAsync(key)); // Async completion } // Consuming ValueTask int value = await GetCachedValueAsync("key"); // DO: Await ValueTask immediately // Good var result = await GetCachedValueAsync("key"); // DON'T: Store or await multiple times // Bad ValueTask<int> task = GetCachedValueAsync("key"); int result1 = await task; int result2 = await task; // May throw or return incorrect result // DON'T: Use with Task.WhenAll // Bad - convert to Task first ValueTask<int> vt = GetCachedValueAsync("key"); await Task.WhenAll(vt.AsTask(), otherTask);
Best Practices
// DO: Use async all the way // Good public async Task<ActionResult> GetDataAsync() { var data = await service.GetDataAsync(); return Ok(data); } // Bad - sync over async (can cause deadlocks) public ActionResult GetData() { var data = service.GetDataAsync().Result; return Ok(data); } // DO: Suffix async methods with Async public async Task<User> GetUserAsync(int id) { } // DO: Return Task directly when possible public Task<User> GetUserAsync(int id) { return repository.GetByIdAsync(id); // No await needed } // DON'T: Use async void except for event handlers // Bad public async void ProcessData() { await DoWorkAsync(); } // Good public async Task ProcessDataAsync() { await DoWorkAsync(); } // DO: Use cancellation tokens public async Task ProcessAsync(CancellationToken cancellationToken = default) { await DoWorkAsync(cancellationToken); } // DO: ConfigureAwait(false) in library code public async Task<string> LibraryMethodAsync() { return await httpClient.GetStringAsync(url).ConfigureAwait(false); } // DON'T: Create unnecessary tasks // Bad public async Task<int> GetValueAsync() { return await Task.Run(() => value); } // Good public Task<int> GetValueAsync() { return Task.FromResult(value); }
4. Records and Init-Only Properties
Overview
Records provide concise syntax for immutable reference types with value semantics. Init-only properties allow setting properties during object initialization but not after.
Record Basics
// Positional record public record Person(string FirstName, string LastName, int Age); // Usage var person = new Person("John", "Doe", 30); Console.WriteLine(person.FirstName); // John // Records are immutable by default - use 'with' for modifications var older = person with { Age = 31 }; // Traditional property syntax public record User { public string Name { get; init; } public string Email { get; init; } public DateTime CreatedAt { get; init; } } // Mixed syntax public record Product(string Name, decimal Price) { public string Description { get; init; } = string.Empty; public bool IsAvailable { get; init; } = true; }
Record Value Semantics
// Records use value-based equality var person1 = new Person("John", "Doe", 30); var person2 = new Person("John", "Doe", 30); Console.WriteLine(person1 == person2); // True Console.WriteLine(person1.Equals(person2)); // True Console.WriteLine(ReferenceEquals(person1, person2)); // False // Automatic ToString implementation Console.WriteLine(person1); // Person { FirstName = John, LastName = Doe, Age = 30 } // Automatic Deconstruction var (firstName, lastName, age) = person1; // GetHashCode based on values var dict = new Dictionary<Person, string>(); dict[person1] = "Value"; Console.WriteLine(dict[person2]); // Value (same key due to value equality)
With-Expressions
var original = new Person("John", "Doe", 30); // Create modified copy var modified = original with { Age = 31 }; // Multiple properties var updated = original with { LastName = "Smith", Age = 32 }; // Original unchanged Console.WriteLine(original.Age); // 30 Console.WriteLine(modified.Age); // 31 // Chaining var final = original .with { Age = 31 } .with { LastName = "Smith" };
Record Inheritance
// Base record public record Person(string FirstName, string LastName); // Derived record public record Employee(string FirstName, string LastName, string Department) : Person(FirstName, LastName); // Usage Employee emp = new("John", "Doe", "IT"); Person person = emp; // Upcasting // With-expression preserves derived type Employee updated = emp with { Department = "HR" }; // Equality respects hierarchy Person p = new("John", "Doe"); Employee e = new("John", "Doe", "IT"); Console.WriteLine(p == e); // False (different types)
Record Structs (C# 10+)
// Readonly record struct public readonly record struct Point(int X, int Y); // Mutable record struct public record struct MutablePoint(int X, int Y); // Usage var p1 = new Point(1, 2); // p1.X = 3; // Error - readonly var p2 = new MutablePoint(1, 2); p2.X = 3; // OK - mutable // Value semantics still apply var p3 = new Point(1, 2); Console.WriteLine(p1 == p3); // True
Init-Only Properties
// Init-only property public class User { public string Name { get; init; } public string Email { get; init; } } // Can set during initialization var user = new User { Name = "John", Email = "john@example.com" }; // Cannot set after initialization // user.Name = "Jane"; // Error // With positional parameters public class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; init; } public decimal Price { get; init; } public string Description { get; init; } = string.Empty; } var product = new Product("Widget", 9.99m) { Description = "A useful widget" };
Required Properties (C# 11+)
// Required property must be set during initialization public class User { public required string Name { get; init; } public required string Email { get; init; } public string? PhoneNumber { get; init; } } // Must set required properties var user = new User { Name = "John", Email = "john@example.com" // PhoneNumber is optional }; // With records public record Person { public required string FirstName { get; init; } public required string LastName { get; init; } } // SetsRequiredMembers attribute for constructors public record Person { public required string FirstName { get; init; } public required string LastName { get; init; } [SetsRequiredMembers] public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } } var person = new Person("John", "Doe"); // No initializer needed
Record Patterns
// DTOs public record UserDto(int Id, string Name, string Email); // Domain events public record UserCreatedEvent(int UserId, DateTime CreatedAt); public record UserUpdatedEvent(int UserId, DateTime UpdatedAt); // API responses public record ApiResponse<T>(bool Success, T? Data, string? Error); // Configuration public record DatabaseConfig { public required string ConnectionString { get; init; } public int MaxRetries { get; init; } = 3; public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30); } // Immutable collections in records public record ShoppingCart { public ImmutableList<CartItem> Items { get; init; } = ImmutableList<CartItem>.Empty; public ShoppingCart AddItem(CartItem item) { return this with { Items = Items.Add(item) }; } }
Best Practices
// DO: Use records for DTOs and value objects public record AddressDto(string Street, string City, string ZipCode); // DO: Use records for immutable data public record Configuration(string ApiKey, string BaseUrl); // DON'T: Use records for entities with identity // Bad - entities need reference equality public record User(int Id, string Name); // Good - use class for entities public class User { public int Id { get; set; } public string Name { get; set; } } // DO: Use init for immutability in classes public class ValueObject { public string Value { get; init; } } // DON'T: Mix mutable and immutable properties // Bad public record ConfusingRecord { public string ImmutableProperty { get; init; } public string MutableProperty { get; set; } } // DO: Use required for mandatory properties public record CreateUserRequest { public required string Name { get; init; } public required string Email { get; init; } public string? PhoneNumber { get; init; } }
5. Pattern Matching
Overview
Pattern matching provides concise syntax for testing values against patterns and extracting information.
Type Patterns
// Basic type check if (obj is string) { string str = (string)obj; } // Type pattern with variable if (obj is string str) { Console.WriteLine(str.Length); } // Multiple type patterns string result = obj switch { string s => s, int i => i.ToString(), null => "null", _ => "unknown" };
Constant Patterns
// Constant pattern if (value is null) { return; } if (value is 0) { Console.WriteLine("Zero"); } // Switch expression with constants string description = value switch { 0 => "zero", 1 => "one", 2 => "two", _ => "other" };
Relational Patterns
// Relational operators: <, <=, >, >= string category = age switch { < 13 => "child", < 20 => "teenager", < 65 => "adult", _ => "senior" }; // Combining relational patterns string grade = score switch { >= 90 => "A", >= 80 => "B", >= 70 => "C", >= 60 => "D", _ => "F" }; // With and/or patterns bool isValid = value switch { > 0 and < 100 => true, _ => false };
Logical Patterns
// And pattern if (obj is string s and { Length: > 0 }) { Console.WriteLine(s); } // Or pattern if (value is 0 or 1 or 2) { Console.WriteLine("Small number"); } // Not pattern if (value is not null) { Process(value); } // Complex combinations string result = value switch { null or "" => "empty", { Length: > 0 and < 10 } => "short", { Length: >= 10 } => "long", _ => "unknown" };
Property Patterns
// Property pattern if (person is { Age: > 18 }) { Console.WriteLine("Adult"); } // Multiple properties if (person is { Age: > 18, IsActive: true }) { Process(person); } // Nested properties if (order is { Customer: { IsVip: true }, Total: > 1000 }) { ApplyVipDiscount(order); } // Switch expression with properties string description = person switch { { Age: < 18 } => "minor", { Age: >= 18, IsStudent: true } => "student", { Age: >= 18, IsEmployed: true } => "employed", _ => "other" }; // Extracting values if (person is { Name: var name, Age: var age }) { Console.WriteLine($"{name} is {age} years old"); }
Positional Patterns
// Deconstruction pattern if (point is (0, 0)) { Console.WriteLine("Origin"); } // With variables if (point is (var x, var y)) { Console.WriteLine($"X: {x}, Y: {y}"); } // Switch expression string quadrant = point switch { (0, 0) => "origin", (var x, var y) when x > 0 && y > 0 => "quadrant I", (var x, var y) when x < 0 && y > 0 => "quadrant II", (var x, var y) when x < 0 && y < 0 => "quadrant III", (var x, var y) when x > 0 && y < 0 => "quadrant IV", _ => "on axis" }; // Custom Deconstruct method public class Person { public string Name { get; set; } public int Age { get; set; } public void Deconstruct(out string name, out int age) { name = Name; age = Age; } } if (person is ("John", var age)) { Console.WriteLine($"John is {age} years old"); }
List Patterns (C# 11+)
// List pattern matching int[] numbers = { 1, 2, 3 }; string result = numbers switch { [] => "empty", [1] => "single one", [1, 2] => "one and two", [1, 2, 3] => "one, two, three", _ => "other" }; // Discard pattern if (numbers is [_, 2, _]) { Console.WriteLine("Middle element is 2"); } // Slice pattern string description = numbers switch { [1, .. var rest] => $"starts with 1, {rest.Length} more", [.. var middle, 3] => $"ends with 3, {middle.Length} before", [1, .., 3] => "starts with 1 and ends with 3", _ => "other" }; // Var pattern for slice if (numbers is [var first, .. var middle, var last]) { Console.WriteLine($"First: {first}, Middle: [{string.Join(", ", middle)}], Last: {last}"); }
When Clauses
// When clause (case guard) string category = value switch { int i when i < 0 => "negative", int i when i == 0 => "zero", int i when i > 0 => "positive", _ => "not an integer" }; // Complex conditions string result = obj switch { string s when s.StartsWith("A") => "starts with A", string s when s.Length > 10 => "long string", int i when i % 2 == 0 => "even number", _ => "other" }; // With property patterns string description = person switch { { Age: var age } when age < 18 => "minor", { Age: var age, IsStudent: true } when age < 25 => "student", { IsEmployed: true } => "employed", _ => "other" };
Practical Examples
// Parsing different input types public static int ParseInput(object input) { return input switch { int i => i, string s when int.TryParse(s, out int result) => result, string => 0, _ => throw new ArgumentException("Cannot parse input") }; } // State machine public State ProcessEvent(Event evt, State current) { return (evt, current) switch { (StartEvent, IdleState) => new RunningState(), (StopEvent, RunningState) => new IdleState(), (PauseEvent, RunningState) => new PausedState(), (ResumeEvent, PausedState) => new RunningState(), _ => current }; } // Visitor pattern public decimal CalculatePrice(Product product) { return product switch { Book { Pages: > 500 } => 29.99m, Book => 19.99m, Electronics { Warranty: true } => 599.99m, Electronics => 499.99m, Clothing { Size: "XL" or "XXL" } => 39.99m, Clothing => 29.99m, _ => 9.99m }; } // Response handling public async Task<string> HandleResponseAsync(HttpResponseMessage response) { return response.StatusCode switch { HttpStatusCode.OK => await response.Content.ReadAsStringAsync(), HttpStatusCode.NotFound => "Resource not found", HttpStatusCode.Unauthorized => "Unauthorized access", >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError => "Client error", >= HttpStatusCode.InternalServerError => "Server error", _ => "Unknown error" }; }
Best Practices
// DO: Use switch expressions for multiple cases // Good string result = value switch { 1 => "one", 2 => "two", _ => "other" }; // Less readable string result; if (value == 1) result = "one"; else if (value == 2) result = "two"; else result = "other"; // DO: Use not pattern for null checks if (value is not null) { Process(value); } // DO: Use property patterns for complex checks if (person is { Age: > 18, IsActive: true }) { Process(person); } // DON'T: Overuse when clauses - consider separate methods // Bad var result = value switch { int i when ComplexCondition1(i) => "a", int i when ComplexCondition2(i) => "b", _ => "c" }; // Better if (value is int i && ComplexCondition1(i)) return "a"; if (value is int j && ComplexCondition2(j)) return "b"; return "c"; // DO: Exhaust all possibilities or use discard pattern string result = value switch { 0 => "zero", > 0 => "positive", < 0 => "negative", // All cases covered, _ not needed };
6. Delegates and Events
Overview
Delegates are type-safe function pointers. Events provide a publish-subscribe mechanism built on delegates.
Delegate Basics
// Delegate declaration public delegate void NotifyHandler(string message); public delegate int Calculate(int x, int y); // Using delegates NotifyHandler handler = ShowMessage; handler("Hello"); // Invokes ShowMessage("Hello") void ShowMessage(string message) { Console.WriteLine(message); } // Multi-cast delegates NotifyHandler handler = ShowMessage; handler += LogMessage; handler += SendEmail; handler("Event occurred"); // Calls all three methods // Removing delegates handler -= LogMessage; // Return value with multi-cast (only last value returned) Calculate calc = Add; calc += Multiply; int result = calc(5, 3); // Returns Multiply result, Add result discarded
Built-in Delegates
// Action - no return value Action action = () => Console.WriteLine("Action"); Action<string> actionWithParam = message => Console.WriteLine(message); Action<int, string> actionMultiParam = (id, name) => Console.WriteLine($"{id}: {name}"); action(); actionWithParam("Hello"); actionMultiParam(1, "John"); // Func - with return value Func<int> func = () => 42; Func<int, int> funcWithParam = x => x * 2; Func<int, int, int> funcMultiParam = (x, y) => x + y; int value = func(); int doubled = funcWithParam(5); int sum = funcMultiParam(3, 4); // Predicate - returns bool Predicate<int> isEven = x => x % 2 == 0; bool result = isEven(4); // Comparison Comparison<int> comparison = (x, y) => x.CompareTo(y);
Events
// Event declaration public class Publisher { // Event with EventHandler public event EventHandler? SomethingHappened; // Event with EventHandler<TEventArgs> public event EventHandler<DataEventArgs>? DataReceived; // Event with custom delegate public event NotifyHandler? Notification; protected virtual void OnSomethingHappened(EventArgs e) { SomethingHappened?.Invoke(this, e); } protected virtual void OnDataReceived(DataEventArgs e) { DataReceived?.Invoke(this, e); } public void DoSomething() { // Raise event OnSomethingHappened(EventArgs.Empty); OnDataReceived(new DataEventArgs { Data = "test" }); } } // Custom EventArgs public class DataEventArgs : EventArgs { public string Data { get; set; } = string.Empty; } // Subscribing to events var publisher = new Publisher(); publisher.SomethingHappened += OnSomethingHappened; publisher.DataReceived += OnDataReceived; void OnSomethingHappened(object? sender, EventArgs e) { Console.WriteLine("Something happened"); } void OnDataReceived(object? sender, DataEventArgs e) { Console.WriteLine($"Data received: {e.Data}"); } // Unsubscribing publisher.SomethingHappened -= OnSomethingHappened;
Lambda Expressions
// Expression lambda Func<int, int> square = x => x * x; // Statement lambda Func<int, int, int> divide = (x, y) => { if (y == 0) throw new DivideByZeroException(); return x / y; }; // Lambda with no parameters Action greet = () => Console.WriteLine("Hello"); // Capturing variables (closure) int factor = 10; Func<int, int> multiply = x => x * factor; int result = multiply(5); // 50 factor = 20; result = multiply(5); // 100 (captures current value) // Async lambda Func<Task<string>> fetchData = async () => { await Task.Delay(1000); return "Data"; };
Event Patterns
// Standard event pattern public class Button { public event EventHandler? Click; protected virtual void OnClick(EventArgs e) { Click?.Invoke(this, e); } public void PerformClick() { OnClick(EventArgs.Empty); } } // Weak event pattern (prevents memory leaks) public class WeakEventManager { private readonly List<WeakReference<EventHandler>> handlers = new(); public void AddHandler(EventHandler handler) { handlers.Add(new WeakReference<EventHandler>(handler)); } public void RemoveHandler(EventHandler handler) { handlers.RemoveAll(wr => { if (!wr.TryGetTarget(out var target)) return true; // Remove dead reference return target == handler; }); } public void Raise(object? sender, EventArgs e) { foreach (var wr in handlers.ToList()) { if (wr.TryGetTarget(out var handler)) { handler(sender, e); } else { handlers.Remove(wr); // Cleanup } } } } // Custom add/remove public class CustomEvents { private EventHandler? _changed; public event EventHandler? Changed { add { Console.WriteLine("Handler added"); _changed += value; } remove { Console.WriteLine("Handler removed"); _changed -= value; } } }
Practical Examples
// Observer pattern public class StockMonitor { public event EventHandler<StockChangedEventArgs>? StockChanged; private decimal _price; public decimal Price { get => _price; set { if (_price != value) { var oldPrice = _price; _price = value; OnStockChanged(new StockChangedEventArgs { OldPrice = oldPrice, NewPrice = value }); } } } protected virtual void OnStockChanged(StockChangedEventArgs e) { StockChanged?.Invoke(this, e); } } public class StockChangedEventArgs : EventArgs { public decimal OldPrice { get; set; } public decimal NewPrice { get; set; } } // Usage var monitor = new StockMonitor(); monitor.StockChanged += (sender, e) => { Console.WriteLine($"Price changed from {e.OldPrice} to {e.NewPrice}"); }; monitor.Price = 100.50m; // Progress reporting public class FileProcessor { public event EventHandler<ProgressEventArgs>? ProgressChanged; public async Task ProcessFilesAsync(string[] files) { for (int i = 0; i < files.Length; i++) { await ProcessFileAsync(files[i]); OnProgressChanged(new ProgressEventArgs { Percentage = (i + 1) * 100 / files.Length, Message = $"Processed {files[i]}" }); } } protected virtual void OnProgressChanged(ProgressEventArgs e) { ProgressChanged?.Invoke(this, e); } } public class ProgressEventArgs : EventArgs { public int Percentage { get; set; } public string Message { get; set; } = string.Empty; } // Callback pattern public class DataLoader { public async Task LoadDataAsync( Func<string, Task> onProgress, Func<Exception, Task<bool>> onError) { try { await onProgress("Starting..."); // Load data await onProgress("Completed"); } catch (Exception ex) { bool retry = await onError(ex); if (retry) { await LoadDataAsync(onProgress, onError); } } } }
Best Practices
// DO: Use EventHandler<T> for events public event EventHandler<DataEventArgs>? DataReceived; // DON'T: Use custom delegates unless necessary // Avoid public delegate void DataHandler(string data); public event DataHandler? DataReceived; // DO: Follow event naming conventions protected virtual void OnDataReceived(DataEventArgs e) { DataReceived?.Invoke(this, e); } // DO: Check for null before invoking // Good - null conditional SomethingHappened?.Invoke(this, EventArgs.Empty); // Old way var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); } // DO: Unsubscribe from events to prevent memory leaks publisher.DataReceived -= OnDataReceived; // DO: Use weak references for long-lived publishers // See WeakEventManager example above // DON'T: Return values from multi-cast delegates // Bad - only last value returned public delegate int Calculate(int x); Calculate calc = x => x * 2; calc += x => x + 1; int result = calc(5); // Only returns 6, ignores 10 // DO: Use Action/Func for callbacks public void ProcessData(Action<string> callback) { callback("Processing..."); } // DON'T: Raise events outside of the class // Events can only be raised by the declaring class
7. Generics
Overview
Generics enable type-safe code reuse without boxing/unboxing or type casting.
Generic Classes
// Generic class public class Container<T> { private T _value; public Container(T value) { _value = value; } public T GetValue() => _value; public void SetValue(T value) => _value = value; } // Usage var intContainer = new Container<int>(42); var stringContainer = new Container<string>("hello"); // Multiple type parameters public class Pair<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } } var pair = new Pair<int, string> { First = 1, Second = "one" };
Generic Methods
// Generic method public T GetDefault<T>() { return default(T); } // Type inference int defaultInt = GetDefault<int>(); // Explicit int inferredInt = GetDefault(); // Inferred from return type // Generic method with constraints public T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; } int maxInt = Max(5, 10); string maxString = Max("apple", "banana"); // Multiple type parameters public TResult Convert<TSource, TResult>(TSource source, Func<TSource, TResult> converter) { return converter(source); } string result = Convert(42, i => i.ToString());
Generic Constraints
// Class constraint public class Repository<T> where T : class { public T? FindById(int id) => null; } // Struct constraint public class Calculator<T> where T : struct { public T Add(T a, T b) => default; } // Interface constraint public class Sorter<T> where T : IComparable<T> { public List<T> Sort(List<T> items) { return items.OrderBy(x => x).ToList(); } } // Base class constraint public class Manager<T> where T : BaseEntity { public void Save(T entity) { entity.UpdatedAt = DateTime.UtcNow; } } // Constructor constraint public class Factory<T> where T : new() { public T Create() { return new T(); } } // Multiple constraints public class AdvancedRepository<T> where T : BaseEntity, IValidatable, new() { public T Create() { var entity = new T(); entity.Validate(); return entity; } } // Constraint on multiple type parameters public class Converter<TInput, TOutput> where TInput : class where TOutput : class, new() { public TOutput Convert(TInput input) { return new TOutput(); } } // Notnull constraint (C# 8+) public class Container<T> where T : notnull { private Dictionary<string, T> _items = new(); }
Covariance and Contravariance
// Covariance (out) - can return more derived type public interface IProducer<out T> { T Produce(); // T Consume(T item); // Error - T in input position } IProducer<string> stringProducer = GetProducer(); IProducer<object> objectProducer = stringProducer; // OK - string is object // Contravariance (in) - can accept more general type public interface IConsumer<in T> { void Consume(T item); // T Produce(); // Error - T in output position } IConsumer<object> objectConsumer = GetConsumer(); IConsumer<string> stringConsumer = objectConsumer; // OK - object accepts string // Real-world example IEnumerable<string> strings = new List<string> { "a", "b" }; IEnumerable<object> objects = strings; // OK - IEnumerable<out T> is covariant Action<object> objectAction = obj => Console.WriteLine(obj); Action<string> stringAction = objectAction; // OK - Action<in T> is contravariant
Generic Collections
// List<T> List<int> numbers = new() { 1, 2, 3 }; List<string> names = ["Alice", "Bob"]; // C# 12 collection expression // Dictionary<TKey, TValue> Dictionary<string, int> ages = new() { ["Alice"] = 30, ["Bob"] = 25 }; // HashSet<T> HashSet<string> uniqueNames = new() { "Alice", "Bob", "Alice" }; // Queue<T> Queue<int> queue = new(); queue.Enqueue(1); int item = queue.Dequeue(); // Stack<T> Stack<int> stack = new(); stack.Push(1); int top = stack.Pop(); // LinkedList<T> LinkedList<string> list = new(); list.AddFirst("first"); list.AddLast("last"); // SortedSet<T> SortedSet<int> sorted = new() { 3, 1, 2 }; // Maintains order // SortedDictionary<TKey, TValue> SortedDictionary<string, int> sortedDict = new() { ["z"] = 1, ["a"] = 2 };
Generic Interfaces
// IEnumerable<T> public class CustomCollection<T> : IEnumerable<T> { private List<T> _items = new(); public IEnumerator<T> GetEnumerator() { return _items.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } // IComparer<T> public class DescendingComparer<T> : IComparer<T> where T : IComparable<T> { public int Compare(T? x, T? y) { if (x == null || y == null) return 0; return y.CompareTo(x); // Reversed } } List<int> numbers = new() { 3, 1, 2 }; numbers.Sort(new DescendingComparer<int>()); // IEqualityComparer<T> public class CaseInsensitiveComparer : IEqualityComparer<string> { public bool Equals(string? x, string? y) { return string.Equals(x, y, StringComparison.OrdinalIgnoreCase); } public int GetHashCode(string obj) { return obj.ToLowerInvariant().GetHashCode(); } } var dict = new Dictionary<string, int>(new CaseInsensitiveComparer()); dict["KEY"] = 1; Console.WriteLine(dict["key"]); // 1
Advanced Generic Patterns
// Generic factory public interface IFactory<T> { T Create(); } public class DefaultFactory<T> : IFactory<T> where T : new() { public T Create() => new T(); } // Generic builder public class Builder<T> where T : class, new() { private readonly T _instance = new(); private readonly List<Action<T>> _actions = new(); public Builder<T> With(Action<T> action) { _actions.Add(action); return this; } public T Build() { foreach (var action in _actions) { action(_instance); } return _instance; } } var user = new Builder<User>() .With(u => u.Name = "John") .With(u => u.Email = "john@example.com") .Build(); // Generic repository pattern public interface IRepository<T> where T : class { Task<T?> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(int id); } public class Repository<T> : IRepository<T> where T : class { private readonly DbContext _context; public Repository(DbContext context) { _context = context; } public async Task<T?> GetByIdAsync(int id) { return await _context.Set<T>().FindAsync(id); } public async Task<IEnumerable<T>> GetAllAsync() { return await _context.Set<T>().ToListAsync(); } // ... other methods } // Type recursion public class TreeNode<T> where T : TreeNode<T> { public List<T> Children { get; set; } = new(); } public class BinaryNode : TreeNode<BinaryNode> { public BinaryNode? Left { get; set; } public BinaryNode? Right { get; set; } }
Best Practices
// DO: Use meaningful type parameter names public class Dictionary<TKey, TValue> // Good public class Dictionary<K, V> // Less clear // DO: Use single letter for simple cases public T Identity<T>(T value) => value; // DO: Constrain type parameters when needed public void Sort<T>(List<T> items) where T : IComparable<T> { items.Sort(); } // DON'T: Overuse constraints // Bad - unnecessary constraint public T Clone<T>(T item) where T : class, ICloneable { return (T)item.Clone(); } // DO: Use generic collections over non-generic List<int> numbers = new(); // Good ArrayList numbers2 = new(); // Bad - uses boxing // DO: Consider covariance/contravariance for interfaces public interface IReader<out T> // Can return derived types { T Read(); } // DON'T: Use generics when not needed // Bad - not reusable public class IntContainer { public int Value { get; set; } } // Good - reusable public class Container<T> { public T Value { get; set; } }
8. Extension Methods
Overview
Extension methods add functionality to existing types without modifying them or creating derived types.
Basic Extension Methods
// Extension method class (must be static) public static class StringExtensions { // Extension method (must be static with this parameter) public static bool IsNullOrEmpty(this string? value) { return string.IsNullOrEmpty(value); } public static string Truncate(this string value, int maxLength) { if (value.Length <= maxLength) return value; return value.Substring(0, maxLength) + "..."; } } // Usage string text = "Hello, World!"; bool isEmpty = text.IsNullOrEmpty(); // false string truncated = text.Truncate(5); // "Hello..." // Null reference string? nullText = null; bool isNull = nullText.IsNullOrEmpty(); // true
Extension Methods for Collections
public static class EnumerableExtensions { public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source) { return source == null || !source.Any(); } public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class { return source.Where(x => x != null)!; } public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) { foreach (var item in source) { action(item); } } public static Dictionary<TKey, TValue> ToDictionarySafe<TSource, TKey, TValue>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector) where TKey : notnull { var dict = new Dictionary<TKey, TValue>(); foreach (var item in source) { var key = keySelector(item); if (!dict.ContainsKey(key)) { dict[key] = valueSelector(item); } } return dict; } public static IEnumerable<TResult> SelectMany<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>?> selector) { foreach (var item in source) { var results = selector(item); if (results != null) { foreach (var result in results) { yield return result; } } } } } // Usage List<int>? numbers = GetNumbers(); bool isEmpty = numbers.IsNullOrEmpty(); List<string?> names = GetNames(); var nonNullNames = names.WhereNotNull(); numbers?.ForEach(n => Console.WriteLine(n));
Extension Methods for Specific Types
public static class DateTimeExtensions { public static bool IsWeekend(this DateTime date) { return date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; } public static DateTime StartOfDay(this DateTime date) { return date.Date; } public static DateTime EndOfDay(this DateTime date) { return date.Date.AddDays(1).AddTicks(-1); } public static int Age(this DateTime birthDate) { var today = DateTime.Today; var age = today.Year - birthDate.Year; if (birthDate.Date > today.AddYears(-age)) age--; return age; } } // Usage DateTime date = DateTime.Now; bool isWeekend = date.IsWeekend(); DateTime start = date.StartOfDay(); int age = new DateTime(1990, 1, 1).Age(); public static class IntExtensions { public static bool IsEven(this int value) => value % 2 == 0; public static bool IsOdd(this int value) => value % 2 != 0; public static bool IsPrime(this int value) { if (value <= 1) return false; if (value == 2) return true; if (value % 2 == 0) return false; for (int i = 3; i * i <= value; i += 2) { if (value % i == 0) return false; } return true; } } // Usage bool even = 42.IsEven(); bool prime = 17.IsPrime();
Extension Methods for Custom Types
public class User { public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } } public static class UserExtensions { public static bool IsNew(this User user) { return user.CreatedAt > DateTime.UtcNow.AddDays(-30); } public static string GetDisplayName(this User user) { return string.IsNullOrEmpty(user.Name) ? user.Email : user.Name; } public static UserDto ToDto(this User user) { return new UserDto { Name = user.Name, Email = user.Email }; } } // Usage User user = GetUser(); bool isNew = user.IsNew(); string display = user.GetDisplayName(); UserDto dto = user.ToDto();
Fluent Interface Extensions
public static class FluentExtensions { public static T Also<T>(this T obj, Action<T> action) { action(obj); return obj; } public static TResult Let<T, TResult>(this T obj, Func<T, TResult> func) { return func(obj); } public static T? TakeIf<T>(this T obj, Func<T, bool> predicate) { return predicate(obj) ? obj : default; } } // Usage var user = new User() .Also(u => u.Name = "John") .Also(u => u.Email = "john@example.com"); int length = "Hello" .Let(s => s.ToUpper()) .Let(s => s.Length); string? value = "test" .TakeIf(s => s.Length > 5); // null public static class BuilderExtensions { public static StringBuilder AppendLineIf( this StringBuilder builder, bool condition, string value) { if (condition) builder.AppendLine(value); return builder; } public static StringBuilder AppendJoin<T>( this StringBuilder builder, string separator, IEnumerable<T> values) { builder.Append(string.Join(separator, values)); return builder; } } // Usage var sb = new StringBuilder() .AppendLine("Header") .AppendLineIf(includeDetails, "Details") .AppendJoin(", ", numbers);
LINQ-Style Extensions
public static class QueryableExtensions { public static IQueryable<T> WhereIf<T>( this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate) { return condition ? query.Where(predicate) : query; } public static IQueryable<T> Page<T>( this IQueryable<T> query, int page, int pageSize) { return query.Skip((page - 1) * pageSize).Take(pageSize); } public static async Task<PagedResult<T>> ToPagedResultAsync<T>( this IQueryable<T> query, int page, int pageSize) { var total = await query.CountAsync(); var items = await query.Page(page, pageSize).ToListAsync(); return new PagedResult<T> { Items = items, TotalCount = total, Page = page, PageSize = pageSize }; } } // Usage var query = context.Users.AsQueryable() .WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm)) .WhereIf(minAge.HasValue, u => u.Age >= minAge.Value); var paged = query.Page(pageNumber, pageSize); var result = await query.ToPagedResultAsync(pageNumber, pageSize);
Extension Methods for Async
public static class TaskExtensions { public static async Task<TResult> Then<T, TResult>( this Task<T> task, Func<T, TResult> func) { var result = await task; return func(result); } public static async Task<TResult> ThenAsync<T, TResult>( this Task<T> task, Func<T, Task<TResult>> func) { var result = await task; return await func(result); } public static async Task<T> WithTimeout<T>( this Task<T> task, TimeSpan timeout) { var completedTask = await Task.WhenAny(task, Task.Delay(timeout)); if (completedTask == task) { return await task; } throw new TimeoutException(); } public static async Task<T> WithRetry<T>( this Func<Task<T>> taskFactory, int retryCount, TimeSpan delay) { for (int i = 0; i < retryCount; i++) { try { return await taskFactory(); } catch when (i < retryCount - 1) { await Task.Delay(delay); } } return await taskFactory(); } } // Usage var result = await GetDataAsync() .Then(data => data.ToUpper()) .ThenAsync(async upper => await ProcessAsync(upper)) .WithTimeout(TimeSpan.FromSeconds(30)); var data = await new Func<Task<string>>(() => FetchDataAsync()) .WithRetry(3, TimeSpan.FromSeconds(1));
Best Practices
// DO: Use clear, descriptive names public static bool IsValidEmail(this string email) { } // Good public static bool Valid(this string s) { } // Bad // DO: Keep extension methods simple public static string Truncate(this string value, int length) { return value.Length <= length ? value : value.Substring(0, length); } // DON'T: Add extension methods to object // Bad - affects everything public static void DoSomething(this object obj) { } // DO: Group related extensions in same class public static class StringExtensions { public static bool IsNullOrEmpty(this string? value) { } public static string Truncate(this string value, int length) { } public static bool IsValidEmail(this string email) { } } // DON'T: Override existing methods // Bad - confusing public static int Length(this string value) => value.Length * 2; // DO: Handle null appropriately public static bool IsNullOrEmpty(this string? value) { return string.IsNullOrEmpty(value); } // DON'T: Modify state unexpectedly // Bad - side effect public static string Uppercase(this StringBuilder sb) { sb.Clear(); sb.Append("MODIFIED"); return sb.ToString(); } // DO: Return new instances for value types public static DateTime AddBusinessDays(this DateTime date, int days) { return date.AddDays(days); // Returns new DateTime } // DO: Document extension methods /// <summary> /// Truncates the string to the specified length. /// </summary> /// <param name="value">The string to truncate.</param> /// <param name="maxLength">The maximum length.</param> /// <returns>The truncated string.</returns> public static string Truncate(this string value, int maxLength) { }
9. Properties and Indexers
Overview
Properties provide a flexible mechanism to read, write, or compute values. Indexers allow instances to be indexed like arrays.
Auto-Implemented Properties
// Auto-property public class User { public string Name { get; set; } public int Age { get; set; } } // With default value public class Product { public string Name { get; set; } = string.Empty; public decimal Price { get; set; } = 0m; public bool IsAvailable { get; set; } = true; } // Init-only property public class ImmutableUser { public string Name { get; init; } public string Email { get; init; } } var user = new ImmutableUser { Name = "John", Email = "john@example.com" }; // user.Name = "Jane"; // Error - init-only // Required property (C# 11+) public class RequiredUser { public required string Name { get; init; } public required string Email { get; init; } } // Must initialize required properties var user2 = new RequiredUser { Name = "John", Email = "john@example.com" };
Properties with Backing Fields
// Private backing field public class User { private string _name = string.Empty; public string Name { get => _name; set => _name = value ?? throw new ArgumentNullException(nameof(value)); } } // Validation public class Product { private decimal _price; public decimal Price { get => _price; set { if (value < 0) throw new ArgumentException("Price cannot be negative"); _price = value; } } } // Computed property public class Rectangle { public double Width { get; set; } public double Height { get; set; } public double Area => Width * Height; // Expression-bodied public double Perimeter { get { return 2 * (Width + Height); } } } // Lazy initialization public class DataService { private HttpClient? _httpClient; public HttpClient HttpClient { get { if (_httpClient == null) { _httpClient = new HttpClient(); } return _httpClient; } } // Or with Lazy<T> private readonly Lazy<HttpClient> _lazyClient = new(() => new HttpClient()); public HttpClient Client => _lazyClient.Value; }
Property Accessors
// Different access levels public class User { public string Name { get; set; } public string Email { get; private set; } // Public get, private set public DateTime CreatedAt { get; init; } // Public get, init-only set private string Password { get; set; } // Private get and set } // Get-only property public class Constants { public string AppName => "MyApp"; // Cannot be set public int MaxRetries { get; } = 3; // Can only be set in constructor public Constants() { MaxRetries = 5; // OK in constructor } } // Set-only property (rare) public class Logger { private string _logPath = string.Empty; public string LogPath { set => _logPath = value; } }
Expression-Bodied Members
public class Person { private string _firstName = string.Empty; private string _lastName = string.Empty; // Expression-bodied property public string FullName => $"{_firstName} {_lastName}"; // Expression-bodied getter and setter public string FirstName { get => _firstName; set => _firstName = value ?? throw new ArgumentNullException(nameof(value)); } // Expression-bodied method public string GetGreeting() => $"Hello, {FullName}!"; // Expression-bodied constructor public Person(string firstName, string lastName) => (_firstName, _lastName) = (firstName, lastName); }
Indexers
// Basic indexer public class StringCollection { private readonly List<string> _items = new(); public string this[int index] { get => _items[index]; set => _items[index] = value; } public int Count => _items.Count; public void Add(string item) => _items.Add(item); } // Usage var collection = new StringCollection(); collection.Add("first"); collection.Add("second"); string item = collection[0]; // "first" collection[1] = "modified"; // Multiple parameters public class Matrix { private readonly double[,] _data; public Matrix(int rows, int cols) { _data = new double[rows, cols]; } public double this[int row, int col] { get => _data[row, col]; set => _data[row, col] = value; } } // Usage var matrix = new Matrix(3, 3); matrix[0, 0] = 1.0; double value = matrix[0, 0]; // String indexer public class Configuration { private readonly Dictionary<string, string> _settings = new(); public string? this[string key] { get => _settings.TryGetValue(key, out var value) ? value : null; set { if (value != null) _settings[key] = value; else _settings.Remove(key); } } } // Usage var config = new Configuration(); config["apiKey"] = "secret"; string? key = config["apiKey"]; // Read-only indexer public class ReadOnlyCollection<T> { private readonly List<T> _items; public ReadOnlyCollection(List<T> items) { _items = items; } public T this[int index] => _items[index]; // Get only public int Count => _items.Count; }
Advanced Property Patterns
// Lazy property public class Report { private string? _cachedContent; public string Content { get { if (_cachedContent == null) { _cachedContent = GenerateContent(); } return _cachedContent; } } private string GenerateContent() { // Expensive operation return "Report content"; } public void InvalidateCache() { _cachedContent = null; } } // Property with notification public class ObservableUser : INotifyPropertyChanged { private string _name = string.Empty; public string Name { get => _name; set { if (_name != value) { _name = value; OnPropertyChanged(); } } } public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } // Dependency property pattern (similar to WPF) public class DependencyObject { private readonly Dictionary<string, object?> _properties = new(); protected T? GetValue<T>(string propertyName) { return _properties.TryGetValue(propertyName, out var value) && value is T typed ? typed : default; } protected void SetValue<T>(string propertyName, T value) { _properties[propertyName] = value; OnPropertyChanged(propertyName); } protected virtual void OnPropertyChanged(string propertyName) { } } public class CustomControl : DependencyObject { public string Text { get => GetValue<string>(nameof(Text)) ?? string.Empty; set => SetValue(nameof(Text), value); } }
Best Practices
// DO: Use auto-properties when no validation needed public class User { public string Name { get; set; } = string.Empty; public int Age { get; set; } } // DO: Use init for immutability public record Product(string Name, decimal Price) { public string Description { get; init; } = string.Empty; } // DO: Validate in property setters public class Product { private decimal _price; public decimal Price { get => _price; set { if (value < 0) throw new ArgumentException("Price cannot be negative"); _price = value; } } } // DON'T: Perform expensive operations in getters // Bad public string Data { get { return File.ReadAllText("data.txt"); // Expensive! } } // Good - cache or use method private string? _cachedData; public string Data => _cachedData ??= File.ReadAllText("data.txt"); // Or public string GetData() => File.ReadAllText("data.txt"); // DO: Use expression-bodied properties for simple computed values public string FullName => $"{FirstName} {LastName}"; // DON'T: Return arrays from properties // Bad - caller can modify array public int[] Numbers { get; set; } // Good - use IReadOnlyCollection or return copy public IReadOnlyList<int> Numbers { get; } public int[] GetNumbers() => _numbers.ToArray(); // DO: Use private setters for internal state public class User { public string Name { get; set; } public DateTime CreatedAt { get; private set; } public User(string name) { Name = name; CreatedAt = DateTime.UtcNow; } } // DO: Use required for mandatory initialization (C# 11+) public class Config { public required string ApiKey { get; init; } public required string BaseUrl { get; init; } }
10. Exception Handling
Overview
Exception handling provides a structured way to handle runtime errors and exceptional conditions.
Basic Exception Handling
// Try-catch try { int result = Divide(10, 0); } catch (DivideByZeroException ex) { Console.WriteLine($"Cannot divide by zero: {ex.Message}"); } // Multiple catch blocks try { ProcessData(); } catch (FileNotFoundException ex) { Console.WriteLine($"File not found: {ex.Message}"); } catch (UnauthorizedAccessException ex) { Console.WriteLine($"Access denied: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Unexpected error: {ex.Message}"); } // Catch with when clause try { await httpClient.GetAsync(url); } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { Console.WriteLine("Resource not found"); } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { Console.WriteLine("Unauthorized access"); } catch (HttpRequestException ex) { Console.WriteLine($"HTTP error: {ex.Message}"); } // Finally block FileStream? file = null; try { file = File.OpenRead("data.txt"); // Process file } catch (IOException ex) { Console.WriteLine($"IO error: {ex.Message}"); } finally { file?.Close(); // Always executed } // Using statement (automatic disposal) try { using var file = File.OpenRead("data.txt"); // Process file } // Automatically disposed catch (IOException ex) { Console.WriteLine($"IO error: {ex.Message}"); }
Throwing Exceptions
// Throw exception public void ValidateAge(int age) { if (age < 0) { throw new ArgumentException("Age cannot be negative", nameof(age)); } } // Throw with inner exception try { ProcessData(); } catch (Exception ex) { throw new ApplicationException("Failed to process data", ex); } // Rethrow try { ProcessData(); } catch (Exception ex) { LogError(ex); throw; // Preserves stack trace } // DON'T: throw ex (loses stack trace) try { ProcessData(); } catch (Exception ex) { LogError(ex); throw ex; // Bad - resets stack trace } // Throw expressions (C# 7+) public string GetName(string? input) { return input ?? throw new ArgumentNullException(nameof(input)); } public User GetUser(int id) => users.Find(u => u.Id == id) ?? throw new KeyNotFoundException($"User {id} not found");
Standard Exception Types
// ArgumentException - invalid argument public void SetAge(int age) { if (age < 0 || age > 150) throw new ArgumentException("Age must be between 0 and 150", nameof(age)); } // ArgumentNullException - null argument public void ProcessUser(User user) { ArgumentNullException.ThrowIfNull(user); // C# 11+ // or if (user == null) throw new ArgumentNullException(nameof(user)); } // ArgumentOutOfRangeException - argument out of valid range public void SetPercentage(int value) { if (value < 0 || value > 100) throw new ArgumentOutOfRangeException(nameof(value), value, "Percentage must be between 0 and 100"); } // InvalidOperationException - invalid state for operation public void Start() { if (_isRunning) throw new InvalidOperationException("Already running"); _isRunning = true; } // NotSupportedException - operation not supported public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException("Stream does not support writing"); } // NotImplementedException - not yet implemented public virtual void Process() { throw new NotImplementedException("Subclasses must implement this method"); } // KeyNotFoundException - dictionary key not found public string GetValue(string key) { if (!_dict.ContainsKey(key)) throw new KeyNotFoundException($"Key '{key}' not found"); return _dict[key]; }
Custom Exceptions
// Custom exception public class ValidationException : Exception { public ValidationException() { } public ValidationException(string message) : base(message) { } public ValidationException(string message, Exception innerException) : base(message, innerException) { } } // With additional properties public class ApiException : Exception { public int StatusCode { get; } public string? ResponseBody { get; } public ApiException(int statusCode, string message, string? responseBody = null) : base(message) { StatusCode = statusCode; ResponseBody = responseBody; } } // Usage try { var response = await httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new ApiException((int)response.StatusCode, "API request failed", body); } } catch (ApiException ex) { Console.WriteLine($"Status: {ex.StatusCode}, Body: {ex.ResponseBody}"); }
Exception Filters
// When clause for conditional catching try { ProcessData(); } catch (Exception ex) when (ex.Message.Contains("timeout")) { Console.WriteLine("Operation timed out"); } catch (Exception ex) when (LogException(ex)) { // Never executes - LogException returns false // But exception is logged as side effect } private bool LogException(Exception ex) { logger.LogError(ex, "Exception occurred"); return false; // Continue exception propagation } // Retry with filter int retries = 3; for (int i = 0; i < retries; i++) { try { await ProcessAsync(); break; } catch (HttpRequestException ex) when (i < retries - 1) { await Task.Delay(1000); } }
Async Exception Handling
// Async exception handling public async Task ProcessDataAsync() { try { await FetchDataAsync(); } catch (HttpRequestException ex) { logger.LogError(ex, "HTTP request failed"); throw; } } // Multiple async operations try { await Task.WhenAll( ProcessAsync(1), ProcessAsync(2), ProcessAsync(3) ); } catch (Exception ex) { // Only first exception caught logger.LogError(ex, "At least one operation failed"); } // Handling all exceptions var tasks = new[] { ProcessAsync(1), ProcessAsync(2), ProcessAsync(3) }; try { await Task.WhenAll(tasks); } catch { foreach (var task in tasks.Where(t => t.IsFaulted)) { foreach (var ex in task.Exception?.InnerExceptions ?? Enumerable.Empty<Exception>()) { logger.LogError(ex, "Task failed"); } } } // ConfigureAwait with exception handling try { var result = await GetDataAsync().ConfigureAwait(false); } catch (Exception ex) { // Exception still caught properly logger.LogError(ex, "Operation failed"); }
Best Practices
// DO: Catch specific exceptions // Good try { ProcessData(); } catch (FileNotFoundException ex) { Console.WriteLine("File not found"); } catch (UnauthorizedAccessException ex) { Console.WriteLine("Access denied"); } // Bad - catches everything try { ProcessData(); } catch (Exception ex) { Console.WriteLine("Error occurred"); } // DO: Use using for disposable resources // Good using var file = File.OpenRead("data.txt"); ProcessFile(file); // Bad - manual disposal FileStream file = null; try { file = File.OpenRead("data.txt"); ProcessFile(file); } finally { file?.Dispose(); } // DON'T: Use exceptions for control flow // Bad try { var user = users.First(u => u.Id == id); } catch (InvalidOperationException) { return null; } // Good var user = users.FirstOrDefault(u => u.Id == id); // DO: Include relevant information in exceptions // Good throw new ValidationException($"Invalid email format: {email}"); // Bad throw new ValidationException("Invalid input"); // DO: Document exceptions /// <summary> /// Gets a user by ID. /// </summary> /// <param name="id">The user ID.</param> /// <returns>The user.</returns> /// <exception cref="ArgumentException">Thrown when id is invalid.</exception> /// <exception cref="KeyNotFoundException">Thrown when user not found.</exception> public User GetUser(int id) { if (id <= 0) throw new ArgumentException("Invalid ID", nameof(id)); return users[id] ?? throw new KeyNotFoundException($"User {id} not found"); } // DON'T: Catch and ignore exceptions // Bad try { ProcessData(); } catch { // Silently ignored - very bad! } // Good - at minimum, log the exception try { ProcessData(); } catch (Exception ex) { logger.LogError(ex, "Failed to process data"); throw; } // DO: Use ArgumentNullException.ThrowIfNull (C# 11+) public void Process(string value) { ArgumentNullException.ThrowIfNull(value); // Process value } // DO: Preserve stack trace when rethrowing try { ProcessData(); } catch (Exception ex) { LogError(ex); throw; // Good - preserves stack trace }
11. Metaprogramming
C# provides multiple metaprogramming mechanisms: attributes for declarative metadata, reflection for runtime introspection, and source generators for compile-time code generation.
Attributes
// Built-in attributes [Obsolete("Use NewMethod instead", error: false)] public void OldMethod() { } [Conditional("DEBUG")] public void DebugOnlyMethod() { } [Serializable] public class DataClass { } // Parameter attributes public void Process([Required] string input, [Range(1, 100)] int value) { } // Method attributes [MethodImpl(MethodImplOptions.AggressiveInlining)] public int FastMethod() => 42; // Assembly attributes (in AssemblyInfo.cs or project file) [assembly: InternalsVisibleTo("MyApp.Tests")] // Custom attribute definition [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class AuthorizeAttribute : Attribute { public string[] Roles { get; set; } = Array.Empty<string>(); public string Policy { get; set; } = string.Empty; public AuthorizeAttribute() { } public AuthorizeAttribute(params string[] roles) => Roles = roles; } // Usage [Authorize("Admin", "Manager")] public class AdminController { }
Reflection
// Get type information Type type = typeof(User); Type runtimeType = user.GetType(); // Get members PropertyInfo[] properties = type.GetProperties(); MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); // Get and set property values PropertyInfo nameProp = type.GetProperty("Name")!; string name = (string)nameProp.GetValue(user)!; nameProp.SetValue(user, "New Name"); // Invoke methods dynamically MethodInfo method = type.GetMethod("ProcessData")!; object? result = method.Invoke(user, new object[] { "arg1", 42 }); // Create instance dynamically object instance = Activator.CreateInstance(type)!; User typedInstance = (User)Activator.CreateInstance(typeof(User), "Name", 30)!; // Generic method invocation MethodInfo genericMethod = type.GetMethod("GenericMethod")!; MethodInfo constructed = genericMethod.MakeGenericMethod(typeof(string)); constructed.Invoke(instance, null); // Read attributes var authorizeAttr = type.GetCustomAttribute<AuthorizeAttribute>(); var allAuthorize = type.GetCustomAttributes<AuthorizeAttribute>(); bool hasAttr = type.IsDefined(typeof(SerializableAttribute));
Source Generators (Compile-time)
// Source generator definition (in separate analyzer project) [Generator] public class AutoNotifyGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // Find classes with [AutoNotify] attribute var classDeclarations = context.SyntaxProvider .ForAttributeWithMetadataName( "AutoNotifyAttribute", predicate: static (s, _) => s is ClassDeclarationSyntax, transform: static (ctx, _) => GetClassInfo(ctx)); context.RegisterSourceOutput(classDeclarations, static (spc, source) => { spc.AddSource($"{source.Name}.g.cs", GenerateCode(source)); }); } } // Usage in consuming project [AutoNotify] public partial class ViewModel { private string _name = ""; private int _age; } // Generated code (automatic) public partial class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } public int Age { get => _age; set { _age = value; OnPropertyChanged(); } } protected void OnPropertyChanged([CallerMemberName] string? name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }
Expression Trees
// Build expressions programmatically ParameterExpression param = Expression.Parameter(typeof(User), "u"); MemberExpression property = Expression.Property(param, "Age"); ConstantExpression constant = Expression.Constant(18); BinaryExpression comparison = Expression.GreaterThan(property, constant); // Compile to delegate Expression<Func<User, bool>> lambda = Expression.Lambda<Func<User, bool>>(comparison, param); Func<User, bool> compiled = lambda.Compile(); // Use like regular delegate bool isAdult = compiled(user); // Parse existing expressions Expression<Func<User, bool>> expr = u => u.Age > 18; var visitor = new CustomExpressionVisitor(); visitor.Visit(expr);
See Also
- Cross-language metaprogramming patternspatterns-metaprogramming-dev
12. Build and Dependencies
.NET uses the SDK-style project system with
csproj files, NuGet for package management, and MSBuild for building.
Project File (csproj)
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LangVersion>latest</LangVersion> <!-- Output settings --> <OutputType>Exe</OutputType> <AssemblyName>MyApp</AssemblyName> <RootNamespace>MyApp</RootNamespace> <!-- Package metadata (for libraries) --> <PackageId>MyCompany.MyLibrary</PackageId> <Version>1.0.0</Version> <Authors>Your Name</Authors> <Description>A useful library</Description> <PackageTags>utility;helper</PackageTags> <RepositoryUrl>https://github.com/user/repo</RepositoryUrl> <PackageLicenseExpression>MIT</PackageLicenseExpression> </PropertyGroup> <!-- Dependencies --> <ItemGroup> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> </ItemGroup> <!-- Project references --> <ItemGroup> <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" /> </ItemGroup> <!-- Conditional references --> <ItemGroup Condition="'$(Configuration)' == 'Debug'"> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" /> </ItemGroup> </Project>
dotnet CLI Commands
# Create projects dotnet new console -n MyApp # Console app dotnet new classlib -n MyLib # Class library dotnet new webapi -n MyApi # Web API dotnet new sln -n MySolution # Solution file dotnet sln add MyApp/MyApp.csproj # Add project to solution # Package management dotnet add package Newtonsoft.Json # Add package dotnet add package Serilog --version 3.1.1 # Specific version dotnet remove package Newtonsoft.Json # Remove package dotnet list package # List packages dotnet list package --outdated # Check for updates dotnet restore # Restore packages # Build and run dotnet build # Build project dotnet build -c Release # Release build dotnet run # Build and run dotnet run --project MyApp # Run specific project dotnet watch run # Run with hot reload # Testing dotnet test # Run tests dotnet test --filter "Category=Unit" # Filter tests dotnet test --collect:"XPlat Code Coverage" # With coverage # Publishing dotnet publish -c Release # Publish for deployment dotnet publish -c Release -r win-x64 --self-contained # Self-contained dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true # Single file # NuGet publishing dotnet pack -c Release # Create NuGet package dotnet nuget push MyLib.1.0.0.nupkg --api-key KEY --source https://api.nuget.org/v3/index.json
NuGet Configuration (nuget.config)
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="private" value="https://pkgs.mycompany.com/v3/index.json" /> </packageSources> <packageSourceCredentials> <private> <add key="Username" value="user" /> <add key="ClearTextPassword" value="password" /> </private> </packageSourceCredentials> <packageSourceMapping> <packageSource key="nuget.org"> <package pattern="*" /> </packageSource> <packageSource key="private"> <package pattern="MyCompany.*" /> </packageSource> </packageSourceMapping> </configuration>
Directory.Build.props (Shared Settings)
<!-- Directory.Build.props - applies to all projects in directory tree --> <Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <!-- Versioning --> <Version>1.0.0</Version> <Company>MyCompany</Company> <Copyright>Copyright © 2024 MyCompany</Copyright> </PropertyGroup> <!-- Shared analyzers --> <ItemGroup> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> </ItemGroup> </Project>
Global.json (SDK Version)
{ "sdk": { "version": "8.0.100", "rollForward": "latestMinor", "allowPrerelease": false } }
13. Testing
.NET has multiple testing frameworks. xUnit is the most popular, with NUnit and MSTest as alternatives. Moq and NSubstitute provide mocking, while FluentAssertions improves readability.
xUnit
// xUnit test class (no attribute needed) public class CalculatorTests { private readonly Calculator _calculator = new(); [Fact] public void Add_TwoNumbers_ReturnsSum() { // Arrange int a = 2, b = 3; // Act int result = _calculator.Add(a, b); // Assert Assert.Equal(5, result); } [Theory] [InlineData(1, 1, 2)] [InlineData(2, 3, 5)] [InlineData(-1, 1, 0)] public void Add_MultipleInputs_ReturnsCorrectSum(int a, int b, int expected) { Assert.Equal(expected, _calculator.Add(a, b)); } [Theory] [MemberData(nameof(TestData))] public void Add_FromMemberData_Works(int a, int b, int expected) { Assert.Equal(expected, _calculator.Add(a, b)); } public static IEnumerable<object[]> TestData => new List<object[]> { new object[] { 1, 1, 2 }, new object[] { 5, 5, 10 } }; [Fact] public void Divide_ByZero_ThrowsException() { Assert.Throws<DivideByZeroException>(() => _calculator.Divide(1, 0)); } [Fact] public async Task FetchData_ValidUrl_ReturnsData() { var result = await _calculator.FetchDataAsync("https://api.example.com"); Assert.NotNull(result); Assert.NotEmpty(result); } } // Test fixtures (shared setup) public class DatabaseTests : IClassFixture<DatabaseFixture> { private readonly DatabaseFixture _fixture; public DatabaseTests(DatabaseFixture fixture) { _fixture = fixture; } [Fact] public void Query_ReturnsData() { var result = _fixture.Database.Query("SELECT 1"); Assert.NotNull(result); } } public class DatabaseFixture : IDisposable { public Database Database { get; } public DatabaseFixture() { Database = new Database(); Database.Initialize(); } public void Dispose() => Database.Dispose(); }
NUnit
[TestFixture] public class CalculatorTests { private Calculator _calculator = null!; [SetUp] public void Setup() { _calculator = new Calculator(); } [TearDown] public void TearDown() { // Cleanup } [Test] public void Add_TwoNumbers_ReturnsSum() { Assert.That(_calculator.Add(2, 3), Is.EqualTo(5)); } [TestCase(1, 1, ExpectedResult = 2)] [TestCase(2, 3, ExpectedResult = 5)] public int Add_ReturnsSum(int a, int b) { return _calculator.Add(a, b); } [Test] public void Divide_ByZero_Throws() { Assert.Throws<DivideByZeroException>(() => _calculator.Divide(1, 0)); } [Test] [Category("Integration")] [Ignore("Requires database")] public void Integration_Test() { } }
MSTest
[TestClass] public class CalculatorTests { private Calculator _calculator = null!; [TestInitialize] public void Setup() { _calculator = new Calculator(); } [TestMethod] public void Add_TwoNumbers_ReturnsSum() { Assert.AreEqual(5, _calculator.Add(2, 3)); } [DataTestMethod] [DataRow(1, 1, 2)] [DataRow(2, 3, 5)] public void Add_MultipleInputs(int a, int b, int expected) { Assert.AreEqual(expected, _calculator.Add(a, b)); } [TestMethod] [ExpectedException(typeof(DivideByZeroException))] public void Divide_ByZero_Throws() { _calculator.Divide(1, 0); } }
Moq (Mocking)
// Interface to mock public interface IUserRepository { User? GetById(int id); Task<User?> GetByIdAsync(int id); void Save(User user); } // Test with Moq public class UserServiceTests { private readonly Mock<IUserRepository> _mockRepo; private readonly UserService _service; public UserServiceTests() { _mockRepo = new Mock<IUserRepository>(); _service = new UserService(_mockRepo.Object); } [Fact] public void GetUser_ValidId_ReturnsUser() { // Setup var user = new User { Id = 1, Name = "John" }; _mockRepo.Setup(r => r.GetById(1)).Returns(user); // Act var result = _service.GetUser(1); // Assert Assert.Equal("John", result?.Name); _mockRepo.Verify(r => r.GetById(1), Times.Once); } [Fact] public async Task GetUserAsync_ReturnsUser() { var user = new User { Id = 1, Name = "John" }; _mockRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(user); var result = await _service.GetUserAsync(1); Assert.NotNull(result); } [Fact] public void SaveUser_CallsRepository() { var user = new User { Name = "John" }; _service.SaveUser(user); _mockRepo.Verify(r => r.Save(It.Is<User>(u => u.Name == "John")), Times.Once); } [Fact] public void GetUser_WhenNotFound_ReturnsNull() { _mockRepo.Setup(r => r.GetById(It.IsAny<int>())).Returns((User?)null); var result = _service.GetUser(999); Assert.Null(result); } [Fact] public void SequencedReturns() { _mockRepo.SetupSequence(r => r.GetById(It.IsAny<int>())) .Returns(new User { Name = "First" }) .Returns(new User { Name = "Second" }) .Throws<InvalidOperationException>(); } }
FluentAssertions
using FluentAssertions; [Fact] public void User_ShouldHaveCorrectProperties() { var user = new User { Name = "John", Age = 30 }; user.Name.Should().Be("John"); user.Age.Should().BeGreaterThan(18).And.BeLessThan(100); user.Email.Should().BeNullOrEmpty(); } [Fact] public void Collection_Assertions() { var numbers = new[] { 1, 2, 3, 4, 5 }; numbers.Should().HaveCount(5); numbers.Should().Contain(3); numbers.Should().BeInAscendingOrder(); numbers.Should().OnlyContain(n => n > 0); } [Fact] public void Exception_Assertions() { Action act = () => throw new InvalidOperationException("Test error"); act.Should().Throw<InvalidOperationException>() .WithMessage("*error*"); } [Fact] public async Task Async_Assertions() { Func<Task> act = async () => await FailingMethodAsync(); await act.Should().ThrowAsync<HttpRequestException>(); } [Fact] public void Object_Comparison() { var user1 = new User { Name = "John", Age = 30 }; var user2 = new User { Name = "John", Age = 30 }; user1.Should().BeEquivalentTo(user2); // Deep comparison user1.Should().BeEquivalentTo(user2, options => options.Excluding(u => u.CreatedAt)); // Exclude property } [Fact] public void Execution_Time() { Action act = () => Thread.Sleep(100); act.ExecutionTime().Should().BeLessThan(200.Milliseconds()); }
Running Tests
# Run all tests dotnet test # Run with verbosity dotnet test --logger "console;verbosity=detailed" # Filter tests dotnet test --filter "FullyQualifiedName~CalculatorTests" dotnet test --filter "Category=Unit" dotnet test --filter "TestCategory!=Integration" # Code coverage dotnet test --collect:"XPlat Code Coverage" dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover # Generate report reportgenerator -reports:coverage.xml -targetdir:coveragereport
Test Organization
MyApp.sln ├── src/ │ ├── MyApp/ │ │ └── MyApp.csproj │ └── MyApp.Core/ │ └── MyApp.Core.csproj └── tests/ ├── MyApp.Tests/ # Unit tests │ └── MyApp.Tests.csproj ├── MyApp.IntegrationTests/ # Integration tests │ └── MyApp.IntegrationTests.csproj └── MyApp.Tests.Common/ # Shared test utilities └── MyApp.Tests.Common.csproj
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- Async/await, channels, threadspatterns-concurrency-dev
- JSON, validation, struct tagspatterns-serialization-dev
- Decorators, macros, annotationspatterns-metaprogramming-dev
Skill Routing
When to Use This Skill
- Writing C# code in any .NET application
- Working with modern C# language features (C# 8+)
- Implementing LINQ queries
- Asynchronous programming with async/await
- Creating immutable data structures with records
- Pattern matching scenarios
- Generic programming
- Extension methods for code reusability
Related Skills
- lang-dotnet-web-dev - ASP.NET Core, Blazor, Web APIs
- lang-dotnet-data-dev - Entity Framework Core, Dapper, ADO.NET
- lang-xaml-dev - WPF, UWP, MAUI
- testing-dotnet-dev - xUnit, NUnit, MSTest
- lang-fsharp-dev - F# functional programming
Complementary Skills
- git-workflow - Version control and collaboration
- docker-dev - Containerization
- azure-dev - Azure services and deployment
- api-design - RESTful API design principles
Additional Resources
Official Documentation
- C# Language Reference: https://docs.microsoft.com/en-us/dotnet/csharp/
- .NET API Browser: https://docs.microsoft.com/en-us/dotnet/api/
- C# Programming Guide: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/
Learning Resources
- What's New in C#: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/
- C# Coding Conventions: https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
- Async Best Practices: https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
Tools
- Visual Studio: https://visualstudio.microsoft.com/
- Visual Studio Code: https://code.visualstudio.com/
- JetBrains Rider: https://www.jetbrains.com/rider/
- LINQPad: https://www.linqpad.net/
Summary
This skill covers foundational C# programming patterns and language features essential for modern C# development. Key areas include:
- Nullable Reference Types - Prevent null reference exceptions with compile-time null safety
- LINQ - Query and manipulate collections with both query and method syntax
- Async/Await - Write efficient asynchronous code with Task-based patterns
- Records - Create immutable value types with concise syntax
- Pattern Matching - Express complex conditional logic clearly
- Delegates and Events - Implement callbacks and event-driven patterns
- Generics - Write reusable, type-safe code
- Extension Methods - Add functionality to existing types
- Properties and Indexers - Encapsulate data access
- Exception Handling - Handle errors gracefully and reliably
Apply these patterns consistently for maintainable, robust C# applications. For specialized scenarios (web development, data access, UI), refer to the related skills listed above.