C# 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
.cs- C# source files.csproj- Project files.sln- Solution files.cshtml- Razor views.razor- Blazor components
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);
…(truncated)