anti-patterns-and-reflection.md
# Anti-Patterns and Reflection Avoidance
Guidelines on avoiding reflection-based metaprogramming and common C# anti-patterns.
## Contents
- [Avoid Reflection-Based Metaprogramming](#avoid-reflection-based-metaprogramming)
- [UnsafeAccessorAttribute (.NET 8+)](#unsafeaccessorattribute-net-8)
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
## Avoid Reflection-Based Metaprogramming
**Prefer statically-typed, explicit code over reflection-based "magic" libraries.**
Reflection-based libraries like AutoMapper trade compile-time safety for convenience. When mappings break, you find out at runtime (or worse, in production) instead of at compile time.
### Banned Libraries
| Library | Problem |
|---------|---------|
| **AutoMapper** | Reflection magic, hidden mappings, runtime failures, hard to debug |
| **Mapster** | Same issues as AutoMapper |
| **ExpressMapper** | Same issues |
### Why Reflection Mapping Fails
```csharp
// With AutoMapper - compiles fine, fails at runtime
public record UserDto(string Id, string Name, string Email);
public record UserEntity(Guid Id, string FullName, string EmailAddress);
// This mapping silently produces garbage:
// - Id: string vs Guid mismatch
// - Name vs FullName: no match, null/default
// - Email vs EmailAddress: no match, null/default
var dto = _mapper.Map<UserDto>(entity); // Compiles! Breaks at runtime.
```
### Use Explicit Mapping Methods Instead
```csharp
// Extension method - compile-time checked, easy to find, easy to debug
public static class UserMappings
{
public static UserDto ToDto(this UserEntity entity) => new(
Id: entity.Id.ToString(),
Name: entity.FullName,
Email: entity.EmailAddress);
public static UserEntity ToEntity(this CreateUserRequest request) => new(
Id: Guid.NewGuid(),
FullName: request.Name,
EmailAddress: request.Email);
}
// Usage - explicit and traceable
var dto = entity.ToDto();
var entity = request.ToEntity();
```
### Benefits of Explicit Mappings
| Aspect | AutoMapper | Explicit Methods |
|--------|------------|------------------|
| **Compile-time safety** | No - runtime errors | Yes - compiler catches mismatches |
| **Discoverability** | Hidden in profiles | "Go to Definition" works |
| **Debugging** | Black box | Step through code |
| **Refactoring** | Rename breaks silently | IDE renames correctly |
| **Performance** | Reflection overhead | Direct property access |
| **Testing** | Need integration tests | Simple unit tests |
### Complex Mappings
For complex transformations, explicit code is even more valuable:
```csharp
public static OrderSummaryDto ToSummary(this Order order) => new(
OrderId: order.Id.Value.ToString(),
CustomerName: order.Customer.FullName,
ItemCount: order.Items.Count,
Total: order.Items.Sum(i => i.Quantity * i.UnitPrice),
Status: order.Status switch
{
OrderStatus.Pending => "Awaiting Payment",
OrderStatus.Paid => "Processing",
OrderStatus.Shipped => "On the Way",
OrderStatus.Delivered => "Completed",
_ => "Unknown"
},
FormattedDate: order.CreatedAt.ToString("MMMM d, yyyy"));
```
This is:
- **Readable**: Anyone can understand the transformation
- **Debuggable**: Set a breakpoint, inspect values
- **Testable**: Pass an Order, assert on the result
- **Refactorable**: Change a property name, compiler tells you everywhere it's used
### When Reflection is Acceptable
Reflection has legitimate uses, but mapping DTOs isn't one of them:
| Use Case | Acceptable? |
|----------|-------------|
| Serialization (System.Text.Json, Newtonsoft) | Yes - well-tested, source generators available |
| Dependency injection container | Yes - framework infrastructure |
| ORM entity mapping (EF Core) | Yes - necessary for database abstraction |
| Test fixtures and builders | Sometimes - for convenience in tests only |
| **DTO/domain object mapping** | **No - use explicit methods** |
## UnsafeAccessorAttribute (.NET 8+)
When you genuinely need to access private or internal members (serializers, test helpers, framework code), use `UnsafeAccessorAttribute` instead of traditional reflection. It provides **zero-overhead, AOT-compatible** member access.
```csharp
// AVOID: Traditional reflection - slow, allocates, breaks AOT
var field = typeof(Order).GetField("_status", BindingFlags.NonPublic | BindingFlags.Instance);
var status = (OrderStatus)field!.GetValue(order)!;
// PREFER: UnsafeAccessor - zero overhead, AOT-compatible
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_status")]
static extern ref OrderStatus GetStatusField(Order order);
var status = GetStatusField(order); // Direct access, no reflection
```
**Supported accessor kinds:**
```csharp
// Private field access
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref List<OrderItem> GetItemsField(Order order);
// Private method access
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Recalculate")]
static extern void CallRecalculate(Order order);
// Private static field
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "_instanceCount")]
static extern ref int GetInstanceCount(Order order);
// Private constructor
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern Order CreateOrder(OrderId id, CustomerId customerId);
```
**Why UnsafeAccessor over reflection:**
| Aspect | Reflection | UnsafeAccessor |
|--------|------------|----------------|
| Performance | Slow (100-1000x) | Zero overhead |
| AOT compatible | No | Yes |
| Allocations | Yes (boxing, arrays) | None |
| Compile-time checked | No | Partially (signature) |
**Use cases:**
- Serializers accessing private backing fields
- Test helpers verifying internal state
- Framework code that needs to bypass visibility
**Resources:**
- [A new way of doing reflection with .NET 8](https://steven-giesel.com/blogPost/05ecdd16-8dc4-490f-b1cf-780c994346a4)
- [Accessing private members without reflection in .NET 8.0](https://www.strathweb.com/2023/10/accessing-private-members-without-reflection-in-net-8-0/)
- [Modern .NET Reflection with UnsafeAccessor](https://blog.ndepend.com/modern-net-reflection-with-unsafeaccessor/)
## Anti-Patterns to Avoid
### Don't: Use mutable DTOs
```csharp
// BAD: Mutable DTO
public class CustomerDto
{
public string Id { get; set; }
public string Name { get; set; }
}
// GOOD: Immutable record
public record CustomerDto(string Id, string Name);
```
### Don't: Use classes for value objects
```csharp
// BAD: Value object as class
public class OrderId
{
public string Value { get; }
public OrderId(string value) => Value = value;
}
// GOOD: Value object as readonly record struct
public readonly record struct OrderId(string Value);
```
### Don't: Create deep inheritance hierarchies
```csharp
// BAD: Deep inheritance
public abstract class Entity { }
public abstract class AggregateRoot : Entity { }
public abstract class Order : AggregateRoot { }
public class CustomerOrder : Order { }
// GOOD: Flat structure with composition
public interface IEntity
{
Guid Id { get; }
}
public record Order(OrderId Id, CustomerId CustomerId, Money Total) : IEntity
{
Guid IEntity.Id => Id.Value;
}
```
### Don't: Return List<T> when you mean IReadOnlyList<T>
```csharp
// BAD: Exposes internal list for modification
public List<Order> GetOrders() => _orders;
// GOOD: Returns read-only view
public IReadOnlyList<Order> GetOrders() => _orders;
```
### Don't: Use byte[] when ReadOnlySpan<byte> works
```csharp
// BAD: Allocates array on every call
public byte[] GetHeader()
{
var header = new byte[64];
// Fill header
return header;
}
// GOOD: Zero allocation with Span
public void GetHeader(Span<byte> destination)
{
if (destination.Length < 64)
throw new ArgumentException("Buffer too small");
// Fill header directly into caller's buffer
}
```
### Don't: Forget CancellationToken in async methods
```csharp
// BAD: No cancellation support
public async Task<Order> GetOrderAsync(OrderId id)
{
return await _repository.GetAsync(id);
}
// GOOD: Cancellation support
public async Task<Order> GetOrderAsync(
OrderId id,
CancellationToken cancellationToken = default)
{
return await _repository.GetAsync(id, cancellationToken);
}
```
### Don't: Block on async code
```csharp
// BAD: Deadlock risk!
public Order GetOrder(OrderId id)
{
return GetOrderAsync(id).Result;
}
// BAD: Also deadlock risk!
public Order GetOrder(OrderId id)
{
return GetOrderAsync(id).GetAwaiter().GetResult();
}
// GOOD: Async all the way
public async Task<Order> GetOrderAsync(
OrderId id,
CancellationToken cancellationToken)
{
return await _repository.GetAsync(id, cancellationToken);
}
```
composition-and-error-handling.md
# Composition and Error Handling
Composition over inheritance, Result type pattern, and testing patterns for modern C#.
## Contents
- [Composition Over Inheritance](#composition-over-inheritance)
- [Result Type Pattern](#result-type-pattern)
- [Testing Patterns](#testing-patterns)
## Composition Over Inheritance
**Avoid abstract base classes and inheritance hierarchies.** Use composition and interfaces instead.
```csharp
// BAD: Abstract base class hierarchy
public abstract class PaymentProcessor
{
public abstract Task<PaymentResult> ProcessAsync(Money amount);
protected async Task<bool> ValidateAsync(Money amount)
{
// Shared validation logic
return amount.Amount > 0;
}
}
public class CreditCardProcessor : PaymentProcessor
{
public override async Task<PaymentResult> ProcessAsync(Money amount)
{
await ValidateAsync(amount);
// Process credit card...
}
}
// GOOD: Composition with interfaces
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(Money amount, CancellationToken cancellationToken);
}
public interface IPaymentValidator
{
Task<ValidationResult> ValidateAsync(Money amount, CancellationToken cancellationToken);
}
// Concrete implementations compose validators
public sealed class CreditCardProcessor : IPaymentProcessor
{
private readonly IPaymentValidator _validator;
private readonly ICreditCardGateway _gateway;
public CreditCardProcessor(IPaymentValidator validator, ICreditCardGateway gateway)
{
_validator = validator;
_gateway = gateway;
}
public async Task<PaymentResult> ProcessAsync(Money amount, CancellationToken cancellationToken)
{
var validation = await _validator.ValidateAsync(amount, cancellationToken);
if (!validation.IsValid)
return PaymentResult.Failed(validation.Error);
return await _gateway.ChargeAsync(amount, cancellationToken);
}
}
// GOOD: Static helper classes for shared logic (no inheritance)
public static class PaymentValidation
{
public static ValidationResult ValidateAmount(Money amount)
{
if (amount.Amount <= 0)
return ValidationResult.Invalid("Amount must be positive");
if (amount.Amount > 10000m)
return ValidationResult.Invalid("Amount exceeds maximum");
return ValidationResult.Valid();
}
}
// GOOD: Records for modeling variants (not inheritance)
public enum PaymentType { CreditCard, BankTransfer, Cash }
public record PaymentMethod
{
public PaymentType Type { get; init; }
public string? Last4 { get; init; } // For credit cards
public string? AccountNumber { get; init; } // For bank transfers
public static PaymentMethod CreditCard(string last4) => new()
{
Type = PaymentType.CreditCard,
Last4 = last4
};
public static PaymentMethod BankTransfer(string accountNumber) => new()
{
Type = PaymentType.BankTransfer,
AccountNumber = accountNumber
};
public static PaymentMethod Cash() => new() { Type = PaymentType.Cash };
}
```
**When inheritance is acceptable:**
- Framework requirements (e.g., `ControllerBase` in ASP.NET Core)
- Library integration (e.g., custom exceptions inheriting from `Exception`)
- **These should be rare cases in your application code**
## Result Type Pattern
For expected errors, use a **domain-specific result type** instead of exceptions. Don't build a generic `Result<T>` — each operation knows what success and failure look like, so let the result type reflect that. Use sealed records with factory methods and enum error codes.
```csharp
// Enum for error classification - type-safe and switchable
public enum OrderErrorCode
{
ValidationError,
InsufficientInventory,
NotFound
}
// Domain-specific result type - sealed record with factory methods
public sealed record CreateOrderResult
{
public bool IsSuccess { get; private init; }
public Order? Order { get; private init; }
public OrderErrorCode? ErrorCode { get; private init; }
public string? ErrorMessage { get; private init; }
public static CreateOrderResult Success(Order order) => new()
{
IsSuccess = true,
Order = order
};
public static CreateOrderResult Failed(OrderErrorCode code, string message) => new()
{
IsSuccess = false,
ErrorCode = code,
ErrorMessage = message
};
}
// Usage example
public sealed class OrderService(IOrderRepository repository)
{
public async Task<CreateOrderResult> CreateOrderAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
if (!IsValid(request))
return CreateOrderResult.Failed(
OrderErrorCode.ValidationError, "Invalid order request");
if (!await HasInventoryAsync(request.Items, cancellationToken))
return CreateOrderResult.Failed(
OrderErrorCode.InsufficientInventory, "Items out of stock");
var order = new Order(
OrderId.New(),
new CustomerId(request.CustomerId),
request.Items);
await repository.SaveAsync(order, cancellationToken);
return CreateOrderResult.Success(order);
}
// Map result to HTTP response - switch on enum error codes
public IActionResult MapToActionResult(CreateOrderResult result)
{
if (result.IsSuccess)
return new OkObjectResult(result.Order);
return result.ErrorCode switch
{
OrderErrorCode.ValidationError =>
new BadRequestObjectResult(new { error = result.ErrorMessage }),
OrderErrorCode.InsufficientInventory =>
new ConflictObjectResult(new { error = result.ErrorMessage }),
OrderErrorCode.NotFound =>
new NotFoundObjectResult(new { error = result.ErrorMessage }),
_ => new ObjectResult(new { error = result.ErrorMessage }) { StatusCode = 500 }
};
}
}
```
**When to use Result vs Exceptions:**
- **Use Result**: Expected errors (validation, business rules, not found)
- **Use Exceptions**: Unexpected errors (network failures, system errors, programming bugs)
## Testing Patterns
```csharp
// Use record for test data builders
public record OrderBuilder
{
public OrderId Id { get; init; } = OrderId.New();
public CustomerId CustomerId { get; init; } = CustomerId.New();
public Money Total { get; init; } = new Money(100m, "USD");
public IReadOnlyList<OrderItem> Items { get; init; } = Array.Empty<OrderItem>();
public Order Build() => new(Id, CustomerId, Total, Items);
}
// Use 'with' expression for test variations
[Fact]
public void CalculateDiscount_LargeOrder_AppliesCorrectDiscount()
{
// Arrange
var baseOrder = new OrderBuilder().Build();
var largeOrder = baseOrder with
{
Total = new Money(1500m, "USD")
};
// Act
var discount = _service.CalculateDiscount(largeOrder);
// Assert
discount.Should().Be(new Money(225m, "USD")); // 15% of 1500
}
// Span-based testing
[Theory]
[InlineData("ORD-12345", true)]
[InlineData("INVALID", false)]
public void TryParseOrderId_VariousInputs_ReturnsExpectedResult(
string input,
bool expected)
{
// Act
var result = OrderIdParser.TryParse(input.AsSpan(), out var orderId);
// Assert
result.Should().Be(expected);
}
// Testing with value objects
[Fact]
public void Money_Add_SameCurrency_ReturnsSum()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
// Act
var result = money1.Add(money2);
// Assert
result.Should().Be(new Money(150m, "USD"));
}
[Fact]
public void Money_Add_DifferentCurrency_ThrowsException()
{
// Arrange
var usd = new Money(100m, "USD");
var eur = new Money(50m, "EUR");
// Act & Assert
var act = () => usd.Add(eur);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*different currencies*");
}
```
performance-and-api-design.md
# Performance and API Design Patterns
Zero-allocation patterns with Span<T>/Memory<T> and API design principles for accepting and returning the right types.
## Contents
- [Span<T> and Memory<T> for Zero-Allocation Code](#spant-and-memoryt-for-zero-allocation-code)
- [API Design Principles](#api-design-principles)
- [Method Signatures Best Practices](#method-signatures-best-practices)
## Span<T> and Memory<T> for Zero-Allocation Code
Use `Span<T>` and `Memory<T>` instead of `byte[]` or `string` for performance-critical code.
```csharp
// Span<T> for synchronous, zero-allocation operations
public int ParseOrderId(ReadOnlySpan<char> input)
{
// Work with data without allocations
if (!input.StartsWith("ORD-"))
throw new FormatException("Invalid order ID format");
var numberPart = input.Slice(4);
return int.Parse(numberPart);
}
// stackalloc with Span<T>
public void FormatMessage()
{
Span<char> buffer = stackalloc char[256];
var written = FormatInto(buffer);
var message = new string(buffer.Slice(0, written));
}
// SkipLocalsInit with stackalloc - skips zero-initialization for performance
// By default, .NET zero-initializes all locals (.locals init flag). This can have
// measurable overhead with stackalloc. Use [SkipLocalsInit] when:
// - You write to the buffer before reading (like FormatInto below)
// - Profiling shows zero-init as a bottleneck
// WARNING: Reading before writing returns garbage data
// Requires: <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in .csproj
// See: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/general#skiplocalsinit-attribute
using System.Runtime.CompilerServices;
[SkipLocalsInit]
public void FormatMessage()
{
Span<char> buffer = stackalloc char[256];
var written = FormatInto(buffer);
var message = new string(buffer.Slice(0, written));
}
// Memory<T> for async operations (Span can't cross await)
public async Task<int> ReadDataAsync(
Memory<byte> buffer,
CancellationToken cancellationToken)
{
return await _stream.ReadAsync(buffer, cancellationToken);
}
// String manipulation with Span to avoid allocations
public bool TryParseKeyValue(ReadOnlySpan<char> line, out string key, out string value)
{
key = string.Empty;
value = string.Empty;
int colonIndex = line.IndexOf(':');
if (colonIndex == -1)
return false;
// Only allocate strings once we know the format is valid
key = new string(line.Slice(0, colonIndex).Trim());
value = new string(line.Slice(colonIndex + 1).Trim());
return true;
}
// ArrayPool for temporary large buffers
public async Task ProcessLargeFileAsync(
Stream stream,
CancellationToken cancellationToken)
{
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(), cancellationToken)) > 0)
{
ProcessChunk(buffer.AsSpan(0, bytesRead));
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
// Hybrid buffer pattern for transient UTF-8 work. See caveats of SkipLocalsInit in the corresponding section.
[SkipLocalsInit]
static short GenerateHashCode(string? key)
{
if (key is null) return 0;
const int StackLimit = 256;
var enc = Encoding.UTF8;
var max = enc.GetMaxByteCount(key.Length);
byte[]? rented = null;
Span<byte> buf = max <= StackLimit
? stackalloc byte[StackLimit]
: (rented = ArrayPool<byte>.Shared.Rent(max));
try
{
var written = enc.GetBytes(key.AsSpan(), buf);
ComputeHash(buf[..written], out var h1, out var h2);
return unchecked((short)(h1 ^ h2));
}
finally
{
if (rented is not null) ArrayPool<byte>.Shared.Return(rented);
}
}
// Span-based parsing without substring allocations
public static (string Protocol, string Host, int Port) ParseUrl(ReadOnlySpan<char> url)
{
var protocolEnd = url.IndexOf("://");
var protocol = new string(url.Slice(0, protocolEnd));
var afterProtocol = url.Slice(protocolEnd + 3);
var portStart = afterProtocol.IndexOf(':');
var host = new string(afterProtocol.Slice(0, portStart));
var portSpan = afterProtocol.Slice(portStart + 1);
var port = int.Parse(portSpan);
return (protocol, host, port);
}
// Writing data to Span
public bool TryFormatOrderId(int orderId, Span<char> destination, out int charsWritten)
{
const string prefix = "ORD-";
if (destination.Length < prefix.Length + 10)
{
charsWritten = 0;
return false;
}
prefix.AsSpan().CopyTo(destination);
var numberWritten = orderId.TryFormat(
destination.Slice(prefix.Length),
out var numberChars);
charsWritten = prefix.Length + numberChars;
return numberWritten;
}
```
**When to use what:**
| Type | Use Case |
|------|----------|
| `Span<T>` | Synchronous operations, stack-allocated buffers, slicing without allocation |
| `ReadOnlySpan<T>` | Read-only views, method parameters for data you won't modify |
| `Memory<T>` | Async operations (Span can't cross await boundaries) |
| `ReadOnlyMemory<T>` | Read-only async operations |
| `byte[]` | When you need to store data long-term or pass to APIs requiring arrays |
| `ArrayPool<T>` | Large temporary buffers (>1KB) to avoid GC pressure |
## API Design Principles
### Accept Abstractions, Return Appropriately Specific
**For Parameters (Accept):**
```csharp
// Accept IEnumerable<T> if you only iterate once
public decimal CalculateTotal(IEnumerable<OrderItem> items)
{
return items.Sum(item => item.Price * item.Quantity);
}
// Accept IReadOnlyCollection<T> if you need Count
public bool HasMinimumItems(IReadOnlyCollection<OrderItem> items, int minimum)
{
return items.Count >= minimum;
}
// Accept IReadOnlyList<T> if you need indexing
public OrderItem GetMiddleItem(IReadOnlyList<OrderItem> items)
{
if (items.Count == 0)
throw new ArgumentException("List cannot be empty");
return items[items.Count / 2]; // Indexed access
}
// Accept ReadOnlySpan<T> for high-performance, zero-allocation APIs
public int Sum(ReadOnlySpan<int> numbers)
{
int total = 0;
foreach (var num in numbers)
total += num;
return total;
}
// Accept IAsyncEnumerable<T> for async streaming
public async Task<int> CountItemsAsync(
IAsyncEnumerable<Order> orders,
CancellationToken cancellationToken)
{
int count = 0;
await foreach (var order in orders.WithCancellation(cancellationToken))
count++;
return count;
}
```
**For Return Types:**
```csharp
// Return IEnumerable<T> for lazy/deferred execution
public IEnumerable<Order> GetOrdersLazy(string customerId)
{
foreach (var order in _repository.Query())
{
if (order.CustomerId == customerId)
yield return order; // Lazy evaluation
}
}
// Return IReadOnlyList<T> for materialized, immutable collections
public IReadOnlyList<Order> GetOrders(string customerId)
{
return _repository
.Query()
.Where(o => o.CustomerId == customerId)
.ToList(); // Materialized
}
// Return concrete types when callers need mutation
public List<Order> GetMutableOrders(string customerId)
{
// Explicitly allow mutation by returning List<T>
return _repository
.Query()
.Where(o => o.CustomerId == customerId)
.ToList();
}
// Return IAsyncEnumerable<T> for async streaming
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;
}
}
// Return arrays for interop or when caller expects array
public byte[] SerializeOrder(Order order)
{
// Binary serialization - byte[] is appropriate here
return MessagePackSerializer.Serialize(order);
}
```
**Summary Table:**
| Scenario | Accept | Return |
|----------|--------|--------|
| Only iterate once | `IEnumerable<T>` | `IEnumerable<T>` (if lazy) |
| Need count | `IReadOnlyCollection<T>` | `IReadOnlyCollection<T>` |
| Need indexing | `IReadOnlyList<T>` | `IReadOnlyList<T>` |
| High-performance, sync | `ReadOnlySpan<T>` | `Span<T>` (rarely) |
| Async streaming | `IAsyncEnumerable<T>` | `IAsyncEnumerable<T>` |
| Caller needs mutation | - | `List<T>`, `T[]` |
## Method Signatures Best Practices
```csharp
// Complete async method signature
public async Task<Result<Order, OrderError>> CreateOrderAsync(
CreateOrderRequest request,
CancellationToken cancellationToken = default)
{
// Implementation
}
// Optional parameters at the end
public async Task<List<Order>> GetOrdersAsync(
string customerId,
DateTime? startDate = null,
DateTime? endDate = null,
CancellationToken cancellationToken = default)
{
// Implementation
}
// Use record for multiple related parameters
public record SearchOrdersRequest(
string? CustomerId,
DateTime? StartDate,
DateTime? EndDate,
OrderStatus? Status,
int PageSize = 20,
int PageNumber = 1
);
public async Task<PagedResult<Order>> SearchOrdersAsync(
SearchOrdersRequest request,
CancellationToken cancellationToken = default)
{
// Implementation
}
// Primary constructors (C# 12+) for simple classes
public sealed class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
public async Task<Order> GetOrderAsync(OrderId orderId, CancellationToken cancellationToken)
{
logger.LogInformation("Fetching order {OrderId}", orderId);
return await repository.GetAsync(orderId, cancellationToken);
}
}
// Options pattern for complex configuration
public sealed class EmailServiceOptions
{
public required string SmtpHost { get; init; }
public int SmtpPort { get; init; } = 587;
public bool UseSsl { get; init; } = true;
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
}
public sealed class EmailService(IOptions<EmailServiceOptions> options)
{
private readonly EmailServiceOptions _options = options.Value;
}
```
SKILL.md
---
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.
user-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
- [value-objects-and-patterns.md](value-objects-and-patterns.md): Full value object examples and pattern matching code
- [performance-and-api-design.md](performance-and-api-design.md): Span<T>/Memory<T> examples and API design principles
- [composition-and-error-handling.md](composition-and-error-handling.md): Composition over inheritance, Result type, testing patterns
- [anti-patterns-and-reflection.md](anti-patterns-and-reflection.md): Reflection avoidance and common anti-patterns
## Core Principles
1. **Immutability by Default** - Use `record` types and `init`-only properties
2. **Type Safety** - Leverage nullable reference types and value objects
3. **Modern Pattern Matching** - Use `switch` expressions and patterns extensively
4. **Async Everywhere** - Prefer async APIs with proper cancellation support
5. **Zero-Allocation Patterns** - Use `Span<T>` and `Memory<T>` for performance-critical code
6. **API Design** - Accept abstractions, return appropriately specific types
7. **Composition Over Inheritance** - Avoid abstract base classes, prefer composition
8. **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.
```csharp
// Simple immutable DTO
public record CustomerDto(string Id, string Name, string Email);
// Record with validation in constructor
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;
}
}
// Records with collections - use IReadOnlyList
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.
```csharp
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](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.
```csharp
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](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.
```csharp
// In .csproj
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// Explicit nullability
public string? FindUserName(string userId)
{
var user = _repository.Find(userId);
return user?.Name;
}
// Pattern matching with null checks
public decimal GetDiscount(Customer? customer) => customer switch
{
null => 0m,
{ IsVip: true } => 0.20m,
{ OrderCount: > 10 } => 0.10m,
_ => 0.05m
};
// Guard clauses with ArgumentNullException.ThrowIfNull (C# 11+)
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
// order is now non-nullable in this scope
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](composition-and-error-handling.md) for full examples.
---
## Performance Patterns
### Async/Await Best Practices
```csharp
// Async all the way - always accept CancellationToken
public async Task<Order> GetOrderAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
return order;
}
// ValueTask for frequently-called, often-synchronous methods
public ValueTask<Order?> GetCachedOrderAsync(string orderId, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(orderId, out var order))
return ValueTask.FromResult<Order?>(order);
return GetFromDatabaseAsync(orderId, cancellationToken);
}
// IAsyncEnumerable for streaming
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<T> and Memory<T>
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](performance-and-api-design.md) for complete Span/Memory examples and the API design section.
---
## Error Handling: Result Type
For expected errors, use a domain-specific result type instead of exceptions, not a generic `Result<T>`. Use exceptions only for unexpected/system errors.
See [composition-and-error-handling.md](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](anti-patterns-and-reflection.md) for full guidance.
---
## Code Organization
```csharp
// File: Domain/Orders/Order.cs
namespace MyApp.Domain.Orders;
// 1. Primary domain type
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 });
}
}
// 2. Enums for state
public enum OrderStatus { Draft, Submitted, Processing, Completed, Cancelled }
// 3. Related types
public record OrderItem(ProductId ProductId, Quantity Quantity, Money UnitPrice)
{
public Money Total => new(UnitPrice.Amount * Quantity.Value, UnitPrice.Currency);
}
// 4. Value objects
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
// 5. Errors
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](anti-patterns-and-reflection.md) for detailed anti-pattern examples.
---
## Additional Resources
- **C# Language Specification**: https://learn.microsoft.com/en-us/dotnet/csharp/
- **Pattern Matching**: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
- **Span<T> and Memory<T>**: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
- **Async Best Practices**: https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- **.NET Performance Tips**: https://learn.microsoft.com/en-us/dotnet/framework/performance/
value-objects-and-patterns.md
# Value Objects and Pattern Matching
Full code examples for value objects and pattern matching in modern C#.
## Contents
- [Value Objects as readonly record struct](#value-objects-as-readonly-record-struct)
- [Constraint-Enforcing Value Objects](#constraint-enforcing-value-objects)
- [No Implicit Conversions](#no-implicit-conversions)
- [Pattern Matching (C# 8-12)](#pattern-matching-c-8-12)
## Value Objects as readonly record struct
Value objects should **always be `readonly record struct`** for performance and value semantics.
```csharp
// Single-value object
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;
// NO implicit conversions - defeats type safety!
// Access inner value explicitly: orderId.Value
}
// Multi-value object
public readonly record struct Money(decimal Amount, string Currency)
{
public Money(decimal amount, string currency) : this(
amount >= 0 ? amount : throw new ArgumentException("Amount cannot be negative", nameof(amount)),
ValidateCurrency(currency))
{
}
private static string ValidateCurrency(string currency)
{
if (string.IsNullOrWhiteSpace(currency) || currency.Length != 3)
throw new ArgumentException("Currency must be a 3-letter code", nameof(currency));
return currency.ToUpperInvariant();
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException($"Cannot add {Currency} to {other.Currency}");
return new Money(Amount + other.Amount, Currency);
}
public override string ToString() => $"{Amount:N2} {Currency}";
}
// Value object with input normalization
public readonly record struct PhoneNumber
{
public string Value { get; }
public PhoneNumber(string input)
{
if (string.IsNullOrWhiteSpace(input))
throw new ArgumentException("Phone number cannot be empty", nameof(input));
// Normalize: remove all non-digits
var digits = new string(input.Where(char.IsDigit).ToArray());
if (digits.Length is < 10 or > 15)
throw new ArgumentException("Phone number must be 10-15 digits", nameof(input));
Value = digits;
}
public override string ToString() => Value;
}
// Percentage value object with range validation
public readonly record struct Percentage
{
private readonly decimal _value;
public decimal Value => _value;
public Percentage(decimal value)
{
if (value < 0 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value), "Percentage must be between 0 and 100");
_value = value;
}
public decimal AsDecimal() => _value / 100m;
public static Percentage FromDecimal(decimal decimalValue)
{
if (decimalValue < 0 || decimalValue > 1)
throw new ArgumentOutOfRangeException(nameof(decimalValue), "Decimal must be between 0 and 1");
return new Percentage(decimalValue * 100);
}
public override string ToString() => $"{_value}%";
}
// Strongly-typed ID
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// Quantity with units
public readonly record struct Quantity(int Value, string Unit)
{
public Quantity(int value, string unit) : this(
value >= 0 ? value : throw new ArgumentException("Quantity cannot be negative"),
!string.IsNullOrWhiteSpace(unit) ? unit : throw new ArgumentException("Unit cannot be empty"))
{
}
public override string ToString() => $"{Value} {Unit}";
}
```
**Why `readonly record struct` for value objects:**
- **Value semantics**: Equality based on content, not reference
- **Stack allocation**: Better performance, no GC pressure
- **Immutability**: `readonly` prevents accidental mutation
- **Pattern matching**: Works seamlessly with switch expressions
## Constraint-Enforcing Value Objects
Value objects aren't just for identifiers. They're equally valuable for **enforcing domain constraints** on strings, numbers, and URIs — making illegal states unrepresentable at the type level.
**Key principle: validate at construction, trust everywhere else.** Once you have an `AbsoluteUrl`, every consumer knows it's valid without re-checking.
```csharp
// AbsoluteUrl - enforces HTTP/HTTPS scheme constraints
public readonly record struct AbsoluteUrl
{
public Uri Value { get; }
public AbsoluteUrl(string uriString) : this(new Uri(uriString, UriKind.Absolute)) { }
public AbsoluteUrl(Uri value)
{
if (!value.IsAbsoluteUri)
throw new ArgumentException(
$"Value must be an absolute URL. Instead found [{value}]", nameof(value));
if (value.Scheme != Uri.UriSchemeHttp && value.Scheme != Uri.UriSchemeHttps)
throw new ArgumentException(
$"Value must be an HTTP or HTTPS URL. Instead found [{value.Scheme}]", nameof(value));
Value = value;
}
/// <summary>
/// Resolves a potentially relative URL against a base URL.
/// Handles Linux quirk where Uri.TryCreate("/path", UriKind.Absolute)
/// succeeds as file:///path.
/// </summary>
public static AbsoluteUrl FromRelative(string? url, AbsoluteUrl baseUrl)
{
if (string.IsNullOrEmpty(url))
throw new ArgumentException("URL cannot be null or empty", nameof(url));
if (Uri.TryCreate(url, UriKind.Absolute, out var absoluteUri) &&
(absoluteUri.Scheme == Uri.UriSchemeHttp || absoluteUri.Scheme == Uri.UriSchemeHttps))
return new AbsoluteUrl(absoluteUri);
return new AbsoluteUrl(new Uri(baseUrl.Value, url));
}
public override string ToString() => Value.ToString();
}
// NonEmptyString - prevents empty/whitespace strings from propagating
public readonly record struct NonEmptyString
{
public string Value { get; }
public NonEmptyString(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Value cannot be null or whitespace", nameof(value));
Value = value;
}
public override string ToString() => Value;
}
// EmailAddress - format validation at construction
public readonly record struct EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email cannot be empty", nameof(value));
if (!value.Contains('@') || !value.Contains('.'))
throw new ArgumentException($"Invalid email format: {value}", nameof(value));
Value = value.ToLowerInvariant();
}
public override string ToString() => Value;
}
// PositiveAmount - numeric range constraint
public readonly record struct PositiveAmount
{
public decimal Value { get; }
public PositiveAmount(decimal value)
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "Amount must be positive");
Value = value;
}
public override string ToString() => Value.ToString("N2");
}
```
**Why this matters:**
- APIs like Slack Block Kit silently reject relative URLs with cryptic errors. Transactional email links break if they're relative. `AbsoluteUrl` makes the compiler prevent this.
- Platform gotchas belong in the value object — e.g., Linux `Uri.TryCreate` treating `/path` as `file:///path` is handled once in `FromRelative`, not at every call site.
### TypeConverter Support for Configuration Binding
Add a `TypeConverter` so your value objects work with `IOptions<T>` and configuration binding:
```csharp
[TypeConverter(typeof(AbsoluteUrlTypeConverter))]
public readonly record struct AbsoluteUrl
{
// ... same as above
}
public sealed class AbsoluteUrlTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
public override object? ConvertFrom(
ITypeDescriptorContext? context, CultureInfo? culture, object value)
=> value is string s ? new AbsoluteUrl(s) : base.ConvertFrom(context, culture, value);
}
// Now this works with appsettings.json binding:
public sealed class WebhookOptions
{
public AbsoluteUrl CallbackUrl { get; set; }
public AbsoluteUrl HealthCheckUrl { get; set; }
}
// appsettings.json:
// { "Webhook": { "CallbackUrl": "https://example.com/callback" } }
services.Configure<WebhookOptions>(configuration.GetSection("Webhook"));
```
## No Implicit Conversions
**CRITICAL: NO implicit conversions.** Implicit operators defeat the purpose of value objects by allowing silent type coercion:
```csharp
// WRONG - defeats compile-time safety:
public readonly record struct UserId(Guid Value)
{
public static implicit operator UserId(Guid value) => new(value); // NO!
public static implicit operator Guid(UserId value) => value.Value; // NO!
}
// With implicit operators, this compiles silently:
void ProcessUser(UserId userId) { }
ProcessUser(Guid.NewGuid()); // Oops - meant to pass PostId
// CORRECT - all conversions explicit:
public readonly record struct UserId(Guid Value)
{
public static UserId New() => new(Guid.NewGuid());
// No implicit operators
// Create: new UserId(guid) or UserId.New()
// Extract: userId.Value
}
```
Explicit conversions force every boundary crossing to be visible:
```csharp
// API boundary - explicit conversion IN
var userId = new UserId(request.UserId); // Validates on entry
// Database boundary - explicit conversion OUT
await _db.ExecuteAsync(sql, new { UserId = userId.Value });
```
## Pattern Matching (C# 8-12)
Leverage modern pattern matching for cleaner, more expressive code.
```csharp
// Switch expressions with value objects
public string GetPaymentMethodDescription(PaymentMethod payment) => payment switch
{
{ Type: PaymentType.CreditCard, Last4: var last4 } => $"Credit card ending in {last4}",
{ Type: PaymentType.BankTransfer, AccountNumber: var account } => $"Bank transfer from {account}",
{ Type: PaymentType.Cash } => "Cash payment",
_ => "Unknown payment method"
};
// Property patterns
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
};
// Relational and logical patterns
public string ClassifyTemperature(int temp) => temp switch
{
< 0 => "Freezing",
>= 0 and < 10 => "Cold",
>= 10 and < 20 => "Cool",
>= 20 and < 30 => "Warm",
>= 30 => "Hot",
_ => throw new ArgumentOutOfRangeException(nameof(temp))
};
// List patterns (C# 11+)
public bool IsValidSequence(int[] numbers) => numbers switch
{
[] => false, // Empty
[_] => true, // Single element
[var first, .., var last] when first < last => true, // First < last
_ => false
};
// Type patterns with null checks
public string FormatValue(object? value) => value switch
{
null => "null",
string s => $"\"{s}\"",
int i => i.ToString(),
double d => d.ToString("F2"),
DateTime dt => dt.ToString("yyyy-MM-dd"),
Money m => m.ToString(),
IEnumerable<object> collection => $"[{string.Join(", ", collection)}]",
_ => value.ToString() ?? "unknown"
};
// Combining patterns for complex logic
public record OrderState(bool IsPaid, bool IsShipped, bool IsCancelled);
public string GetOrderStatus(OrderState state) => state switch
{
{ IsCancelled: true } => "Cancelled",
{ IsPaid: true, IsShipped: true } => "Delivered",
{ IsPaid: true, IsShipped: false } => "Processing",
{ IsPaid: false } => "Awaiting Payment",
_ => "Unknown"
};
// Pattern matching with value objects
public decimal CalculateShipping(Money total, Country destination) => (total, destination) switch
{
({ Amount: > 100m }, _) => 0m, // Free shipping over $100
(_, { Code: "US" or "CA" }) => 5m, // North America
(_, { Code: "GB" or "FR" or "DE" }) => 10m, // Europe
_ => 25m // International
};
```