PluginBench
Skill
Pass
Audit score 90

dotnet-patterns

affaan-m/ecc

Idiomatic C# and .NET patterns, conventions, and best practices for robust, maintainable applications.

What is dotnet-patterns?

A reference for writing idiomatic C# and .NET code following established patterns and conventions. Use this when writing, reviewing, or refactoring C# code to ensure consistency with best practices around immutability, dependency injection, async/await, and service architecture.

  • Apply immutability patterns using records and init-only properties
  • Structure async/await code with proper CancellationToken handling and avoid blocking calls
  • Implement dependency injection and depend on abstractions via interfaces
  • Use the Options pattern for strongly-typed configuration binding
  • Apply the Result pattern for explicit success/failure handling instead of exceptions for expected failures
  • Build repositories and data access layers with Entity Framework Core

How to install dotnet-patterns

npx skills add null --skill dotnet-patterns
Claude Code
Cursor
Windsurf
Cline

How to use dotnet-patterns

  1. 1.Reference the core principles (immutability, explicit over implicit, depend on abstractions) when writing new code
  2. 2.Apply async/await patterns with CancellationToken for all I/O operations
  3. 3.Use the Options pattern to bind configuration sections to strongly-typed objects
  4. 4.Implement the Result pattern for domain operations that can fail in expected ways
  5. 5.Structure repositories with EF Core following the provided patterns
  6. 6.Organize middleware and minimal API endpoints using route groups and proper tagging

Use cases

Good for
  • Writing new C# services or ASP.NET Core applications with clean architecture
  • Reviewing C# code to ensure it follows idiomatic patterns and conventions
  • Refactoring existing .NET applications to improve maintainability and consistency
  • Designing service architectures with proper dependency injection and abstraction layers
  • Building REST APIs with minimal APIs and organized route groups
Who it's for
  • C# developers
  • Backend engineers building .NET applications
  • Teams adopting ASP.NET Core
  • Developers reviewing or refactoring legacy .NET code

dotnet-patterns FAQ

When should I use records vs. classes?

Use records for immutable value objects and DTOs. Use sealed classes when you need more control over initialization or have complex logic. Prefer immutability by default.

Why pass CancellationToken to every async method?

CancellationToken allows graceful cancellation of long-running operations and respects timeout policies. It's essential for responsive applications and proper resource cleanup.

Should I throw exceptions for all errors?

No. Use exceptions for unexpected, exceptional conditions. Use the Result pattern for expected business failures (validation errors, not found, etc.) to make error handling explicit and type-safe.

How do I structure dependency injection in ASP.NET Core?

Register dependencies in Program.cs using builder.Services, depend on abstractions (interfaces), and let the DI container inject them. Use scoped lifetime for repositories and services, singleton for stateless utilities.

What's the difference between Task.WhenAll and sequential awaits?

Task.WhenAll runs multiple async operations concurrently and waits for all to complete. Sequential awaits run one after another. Use WhenAll for independent operations to improve performance.

Full instructions (SKILL.md)

Source of truth, from affaan-m/ecc.


name: dotnet-patterns description: Idiomatic C# and .NET patterns, conventions, dependency injection, async/await, and best practices for building robust, maintainable .NET applications. metadata: origin: ECC

.NET Development Patterns

Idiomatic C# and .NET patterns for building robust, performant, and maintainable applications.

When to Activate

  • Writing new C# code
  • Reviewing C# code
  • Refactoring existing .NET applications
  • Designing service architectures with ASP.NET Core

Core Principles

1. Prefer Immutability

Use records and init-only properties for data models. Mutability should be an explicit, justified choice.

// Good: Immutable value object
public sealed record Money(decimal Amount, string Currency);

// Good: Immutable DTO with init setters
public sealed class CreateOrderRequest
{
    public required string CustomerId { get; init; }
    public required IReadOnlyList<OrderItem> Items { get; init; }
}

// Bad: Mutable model with public setters
public class Order
{
    public string CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

2. Explicit Over Implicit

Be clear about nullability, access modifiers, and intent.

// Good: Explicit access modifiers and nullability
public sealed class UserService
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repository, ILogger<UserService> logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await _repository.FindByIdAsync(id, cancellationToken);
    }
}

3. Depend on Abstractions

Use interfaces for service boundaries. Register via DI container.

// Good: Interface-based dependency
public interface IOrderRepository
{
    Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
    Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
    Task AddAsync(Order order, CancellationToken cancellationToken);
}

// Registration
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

Async/Await Patterns

Proper Async Usage

// Good: Async all the way, with CancellationToken
public async Task<OrderSummary> GetOrderSummaryAsync(
    Guid orderId,
    CancellationToken cancellationToken)
{
    var order = await _repository.FindByIdAsync(orderId, cancellationToken)
        ?? throw new NotFoundException($"Order {orderId} not found");

    var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);

    return new OrderSummary(order, customer);
}

// Bad: Blocking on async
public OrderSummary GetOrderSummary(Guid orderId)
{
    var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // Deadlock risk
    return new OrderSummary(order);
}

Parallel Async Operations

// Good: Concurrent independent operations
public async Task<DashboardData> LoadDashboardAsync(CancellationToken cancellationToken)
{
    var ordersTask = _orderService.GetRecentAsync(cancellationToken);
    var metricsTask = _metricsService.GetCurrentAsync(cancellationToken);
    var alertsTask = _alertService.GetActiveAsync(cancellationToken);

    await Task.WhenAll(ordersTask, metricsTask, alertsTask);

    return new DashboardData(
        Orders: await ordersTask,
        Metrics: await metricsTask,
        Alerts: await alertsTask);
}

Options Pattern

Bind configuration sections to strongly-typed objects.

public sealed class SmtpOptions
{
    public const string SectionName = "Smtp";

    public required string Host { get; init; }
    public required int Port { get; init; }
    public required string Username { get; init; }
    public bool UseSsl { get; init; } = true;
}

// Registration
builder.Services.Configure<SmtpOptions>(
    builder.Configuration.GetSection(SmtpOptions.SectionName));

// Usage via injection
public class EmailService(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;
}

Result Pattern

Return explicit success/failure instead of throwing for expected failures.

public sealed record Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(T value) { IsSuccess = true; Value = value; }
    private Result(string error) { IsSuccess = false; Error = error; }

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(string error) => new(error);
}

// Usage
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
{
    if (request.Items.Count == 0)
        return Result<Order>.Failure("Order must contain at least one item");

    var order = Order.Create(request);
    await _repository.AddAsync(order, CancellationToken.None);
    return Result<Order>.Success(order);
}

Repository Pattern with EF Core

public sealed class SqlOrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;

    public SqlOrderRepository(AppDbContext db) => _db = db;

    public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await _db.Orders
            .Include(o => o.Items)
            .AsNoTracking()
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task<IReadOnlyList<Order>> FindByCustomerAsync(
        string customerId,
        CancellationToken cancellationToken)
    {
        return await _db.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }

    public async Task AddAsync(Order order, CancellationToken cancellationToken)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(cancellationToken);
    }
}

Middleware and Pipeline

// Custom middleware
public sealed class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            _logger.LogInformation(
                "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
                context.Request.Method,
                context.Request.Path,
                stopwatch.ElapsedMilliseconds,
                context.Response.StatusCode);
        }
    }
}

Minimal API Patterns

// Organized with route groups
var orders = app.MapGroup("/api/orders")
    .RequireAuthorization()
    .WithTags("Orders");

orders.MapGet("/{id:guid}", async (
    Guid id,
    IOrderRepository repository,
    CancellationToken cancellationToken) =>
{
    var order = await repository.FindByIdAsync(id, cancellationToken);
    return order is not null
        ? TypedResults.Ok(order)
        : TypedResults.NotFound();
});

orders.MapPost("/", async (
    CreateOrderRequest request,
    IOrderService service,
    CancellationToken cancellationToken) =>
{
    var result = await service.PlaceOrderAsync(request, cancellationToken);
    return result.IsSuccess
        ? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value)
        : TypedResults.BadRequest(result.Error);
});

Guard Clauses

// Good: Early returns with clear validation
public async Task<ProcessResult> ProcessPaymentAsync(
    PaymentRequest request,
    CancellationToken cancellationToken)
{
    ArgumentNullException.ThrowIfNull(request);

    if (request.Amount <= 0)
        throw new ArgumentOutOfRangeException(nameof(request.Amount), "Amount must be positive");

    if (string.IsNullOrWhiteSpace(request.Currency))
        throw new ArgumentException("Currency is required", nameof(request.Currency));

    // Happy path continues here without nesting
    var gateway = _gatewayFactory.Create(request.Currency);
    return await gateway.ChargeAsync(request, cancellationToken);
}

Anti-Patterns to Avoid

Anti-PatternFix
async void methodsReturn Task (except event handlers)
.Result or .Wait()Use await
catch (Exception) { }Handle or rethrow with context
new Service() in constructorsUse constructor injection
public fieldsUse properties with appropriate accessors
dynamic in business logicUse generics or explicit types
Mutable static stateUse DI scoping or ConcurrentDictionary
string.Format in loopsUse StringBuilder or interpolated string handlers