| name | modern-csharp-coding-standards |
| description | Write modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. |
| invocable | false |
Modern C# Coding Standards
When to Use This Skill
Use this skill when:
- Writing new C# code or refactoring existing code
- Designing public APIs for libraries or services
- Optimizing performance-critical code paths
- Implementing domain models with strong typing
- Building async/await-heavy applications
- Working with binary data, buffers, or high-throughput scenarios
Reference Files
Core Principles
- Immutability by Default - Use
record types and init-only properties
- Type Safety - Leverage nullable reference types and value objects
- Modern Pattern Matching - Use
switch expressions and patterns extensively
- Async Everywhere - Prefer async APIs with proper cancellation support
- Zero-Allocation Patterns - Use
Span<T> and Memory<T> for performance-critical code
- API Design - Accept abstractions, return appropriately specific types
- Composition Over Inheritance - Avoid abstract base classes, prefer composition
- Value Objects as Structs - Use
readonly record struct for value objects
Language Patterns
Records for Immutable Data (C# 9+)
Use record types for DTOs, messages, events, and domain entities.
public record CustomerDto(string Id, string Name, string Email);
public record EmailAddress
{
public string Value { get; init; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("Invalid email address", nameof(value));
Value = value;
}
}
public record ShoppingCart(
string CartId,
string CustomerId,
IReadOnlyList<CartItem> Items
)
{
public decimal Total => Items.Sum(item => item.Price * item.Quantity);
}
When to use record class vs record struct:
record class (default): Reference types, use for entities, aggregates, DTOs with multiple properties
record struct: Value types, use for value objects (see next section)
Value Objects as readonly record struct
Value objects should always be readonly record struct for performance and value semantics. Use explicit conversions, never implicit operators.
public readonly record struct OrderId(string Value)
{
public OrderId(string value) : this(
!string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("OrderId cannot be empty", nameof(value)))
{ }
public override string ToString() => Value;
}
public readonly record struct Money(decimal Amount, string Currency);
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
}
See value-objects-and-patterns.md for complete examples including multi-value objects, factory patterns, and the no-implicit-conversion rule.
Pattern Matching (C# 8-12)
Use switch expressions, property patterns, relational patterns, and list patterns for cleaner code.
public decimal CalculateDiscount(Order order) => order switch
{
{ Total: > 1000m } => order.Total * 0.15m,
{ Total: > 500m } => order.Total * 0.10m,
{ Total: > 100m } => order.Total * 0.05m,
_ => 0m
};
See value-objects-and-patterns.md for full pattern matching examples.
Nullable Reference Types (C# 8+)
Enable nullable reference types in your project and handle nulls explicitly.
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
public string? FindUserName(string userId)
{
var user = _repository.Find(userId);
return user?.Name;
}
public decimal GetDiscount(Customer? customer) => customer switch
{
null => 0m,
{ IsVip: true } => 0.20m,
{ OrderCount: > 10 } => 0.10m,
_ => 0.05m
};
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
Console.WriteLine(order.Id);
}
Composition Over Inheritance
Avoid abstract base classes. Use interfaces + composition. Use static helpers for shared logic. Use records with factory methods for variants.
See composition-and-error-handling.md for full examples.
Performance Patterns
Async/Await Best Practices
public async Task<Order> GetOrderAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
return order;
}
public ValueTask<Order?> GetCachedOrderAsync(string orderId, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(orderId, out var order))
return ValueTask.FromResult<Order?>(order);
return GetFromDatabaseAsync(orderId, cancellationToken);
}
public async IAsyncEnumerable<Order> StreamOrdersAsync(
string customerId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamAllAsync(cancellationToken))
{
if (order.CustomerId == customerId)
yield return order;
}
}
Key rules:
- Always accept
CancellationToken with = default
- Use
ConfigureAwait(false) in library code
- Never block on async code (no
.Result or .Wait())
- Use linked CancellationTokenSource for timeouts
Span and Memory
Use Span<T> for synchronous zero-allocation operations, Memory<T> for async, and ArrayPool<T> for large temporary buffers.
See performance-and-api-design.md for complete Span/Memory examples and the API design section.
Error Handling: Result Type
For expected errors, use Result<T, TError> instead of exceptions. Use exceptions only for unexpected/system errors.
See composition-and-error-handling.md for the full Result type implementation and usage examples.
Avoid Reflection-Based Metaprogramming
Banned: AutoMapper, Mapster, ExpressMapper. Use explicit mapping extension methods instead. Use UnsafeAccessorAttribute (.NET 8+) when you genuinely need private member access.
See anti-patterns-and-reflection.md for full guidance.
Code Organization
namespace MyApp.Domain.Orders;
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
public bool IsCompleted => Status is OrderStatus.Completed;
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "Can only add items to draft orders"));
var newItems = Items.Append(item).ToList();
var newTotal = new Money(
Items.Sum(i => i.Total.Amount) + item.Total.Amount,
Total.Currency);
return Result<Order, OrderError>.Success(
this with { Items = newItems, Total = newTotal });
}
}
public enum OrderStatus { Draft, Submitted, Processing, Completed, Cancelled }
public record OrderItem(ProductId ProductId, Quantity Quantity, Money UnitPrice)
{
public Money Total => new(UnitPrice.Amount * Quantity.Value, UnitPrice.Currency);
}
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
public readonly record struct OrderError(string Code, string Message);
Best Practices Summary
DO's
- Use
record for DTOs, messages, and domain entities
- Use
readonly record struct for value objects
- Leverage pattern matching with
switch expressions
- Enable and respect nullable reference types
- Use async/await for all I/O operations
- Accept
CancellationToken in all async methods
- Use
Span<T> and Memory<T> for high-performance scenarios
- Accept abstractions (
IEnumerable<T>, IReadOnlyList<T>)
- Use
Result<T, TError> for expected errors
- Pool buffers with
ArrayPool<T> for large allocations
- Prefer composition over inheritance
DON'Ts
- Don't use mutable classes when records work
- Don't use classes for value objects (use
readonly record struct)
- Don't create deep inheritance hierarchies
- Don't ignore nullable reference type warnings
- Don't block on async code (
.Result, .Wait())
- Don't use
byte[] when Span<byte> suffices
- Don't forget
CancellationToken parameters
- Don't return mutable collections from APIs
- Don't throw exceptions for expected business errors
- Don't allocate large arrays repeatedly (use
ArrayPool)
See anti-patterns-and-reflection.md for detailed anti-pattern examples.
Additional Resources