modern-csharp-coding-standards

Modern C# coding standards covering records, pattern matching, value objects, async/await, and high-performance patterns. Use record types for immutable DTOs and domain entities; readonly record struct for value objects with zero-allocation semantics Leverage pattern matching with switch expressions, nullable reference types, and composition over inheritance for cleaner, type-safe code Apply async/await throughout with mandatory CancellationToken parameters; use Span<T> , Memory<T> , and ArrayPool<T> for performance-critical paths Handle expected business errors with Result<T, TError> types; reserve exceptions for unexpected system failures Avoid reflection-based metaprogramming (AutoMapper, Mapster) in favor of explicit mapping and composition patterns

INSTALLATION
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill modern-csharp-coding-standards
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

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.

// 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.

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.

// 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 for full examples.

Performance Patterns

Async/Await Best Practices

// 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 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

// 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 for detailed anti-pattern examples.

Additional Resources

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card