Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Entwicklung · SKILL.md

dotnet-patterns

dotnet-patterns

Use this skill when implementing .NET patterns — MediatR pipelines, CQRS, Result pattern, EF Core patterns, pipeline behaviors, and event sourcing. This skill enforces: MediatR for CQRS, FluentValidation for validation, Result type for error handling, EF Core conventions. Requires .NET SDK (dotnet new). Do NOT use for: Java, Node.js, Go, or non-.NET stacks.

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: dotnet-patterns description: > Use this skill when implementing .NET patterns — MediatR pipelines, CQRS, Result pattern, EF Core patterns, pipeline behaviors, and event sourcing. This skill enforces: MediatR for CQRS, FluentValidation for validation, Result type for error handling, EF Core conventions. Requires .NET SDK (dotnet new). Do NOT use for: Java, Node.js, Go, or non-.NET stacks. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, dotnet, phase-3]

.NET Patterns

Purpose

Implement production-grade .NET patterns — MediatR pipeline behaviors, CQRS with separate models, Result pattern for explicit error handling, EF Core performance patterns, and event sourcing.

Agent Protocol

Trigger

User request includes: dotnet MediatR pattern, dotnet CQRS, dotnet Result pattern, dotnet EF Core, dotnet pipeline behavior, dotnet FluentValidation, dotnet AutoMapper.

Input Context

  • Architecture (Clean Architecture, Vertical Slices)
  • ORM (EF Core, Dapper)
  • Patterns needed (CQRS, Event Sourcing, Outbox, Saga)
  • Testing approach (xUnit, NUnit)

Output Artifact

Code examples for requested patterns — pipeline behavior, CQRS handler, Result type, EF optimization.

Response Format

Code-first output. Pattern name, problem statement, implementation. No preamble, no postamble.

Completion Criteria

  • MediatR pipeline behaviors ordered
  • CQRS with separate Command/Query handlers
  • Result type with implicit conversions
  • EF Core query optimization applied
  • Unit of Work pattern with SaveChangesAsync

Max Response Length

4096 tokens

Architecture Decision Trees

Pipeline Behavior Order

OrderBehaviorPurpose
1LoggingBehaviorLog request start/end with timing
2ValidationBehaviorValidate before processing
3PerformanceBehaviorWarn if > threshold (500ms)
4UnhandledExceptionBehaviorCatch and wrap unhandled
5TransactionBehaviorWrap in DB transaction

Pipeline behaviors run in registration order (first added = outer wrapper, runs first).

FluentValidation vs Data Annotations

CriterionFluentValidationData Annotations
SeparationExternal to modelOn model properties
Conditional rulesYes (When/Unless)Limited
Cross-propertyYes (Must)No
DI supportYes (inject services)No
TestabilityEasy (validator per model)Requires model instantiation

Decision: Complex validation rules → FluentValidation. Simple non-null/range checks → Data Annotations.

Workflow

Step 1: MediatR Pipeline Behaviors

// Application/Common/Behaviors/PerformanceBehavior.cs
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        const int threshold = 500;
        var sw = Stopwatch.StartNew();
        var response = await next();
        sw.Stop();

        if (sw.ElapsedMilliseconds > threshold)
        {
            _logger.LogWarning("Long running request: {Name} ({Elapsed}ms)",
                typeof(TRequest).Name, sw.ElapsedMilliseconds);
        }

        return response;
    }
}

// Application/Common/Behaviors/TransactionBehavior.cs
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly AppDbContext _context;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        if (request is IQuery<TResponse>) return await next();

        await using var transaction = await _context.Database.BeginTransactionAsync(ct);
        try
        {
            var response = await next();
            await transaction.CommitAsync(ct);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
}

Step 2: Result Pattern

// Application/Common/Models/Result.cs
public class Result<T>
{
    private readonly T? _value;
    public T Value => IsSuccess ? _value! : throw new InvalidOperationException("No value on failure");
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public Error Error { get; }

    private Result(T value)
    {
        _value = value;
        IsSuccess = true;
        Error = Error.None();
    }

    private Result(Error error)
    {
        IsSuccess = false;
        Error = error;
    }

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

    public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure)
        => IsSuccess ? onSuccess(_value!) : onFailure(Error);
}

// Application/Common/Models/Error.cs
public record Error(string Code, string Message)
{
    public static Error None() => new(string.Empty, string.Empty);
    public static Error NotFound(string entity) => new("NOT_FOUND", quot;{entity} not found");
    public static Error Validation(string message) => new("VALIDATION_ERROR", message);
    public static Error Conflict(string message) => new("CONFLICT", message);
}

// Usage in handler
public async Task<Result<OrderDto>> Handle(CreateOrderCommand request, CancellationToken ct)
{
    var existing = await _repository.FindByOrderNumber(request.OrderNumber);
    if (existing is not null)
        return Result<OrderDto>.Failure(Error.Conflict("Order number already exists"));

    var order = new Order(request.OrderNumber);
    await _repository.AddAsync(order);
    return Result<OrderDto>.Success(_mapper.Map<OrderDto>(order));
}

// Controller consumes result
[HttpPost]
public IActionResult CreateOrder(CreateOrderCommand command)
{
    var result = await _mediator.Send(command);
    return result.Match<IActionResult>(
        onSuccess: dto => CreatedAtAction(nameof(GetById), new { id = dto.Id }, dto),
        onFailure: error => error.Code switch
        {
            "NOT_FOUND" => NotFound(new { error.Message }),
            "VALIDATION_ERROR" => BadRequest(new { error.Message }),
            "CONFLICT" => Conflict(new { error.Message }),
            _ => StatusCode(500, new { error.Message }),
        }
    );
}

Step 3: EF Core Query Optimization

// Repository with optimized queries
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public async Task<Order?> GetByIdWithItemsAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .AsSplitQuery()  // Avoid cartesian explosion
            .FirstOrDefaultAsync(o => o.Id == id);
    }

    public async Task<PagedResult<OrderSummary>> GetPagedAsync(int page, int size)
    {
        var query = _context.Orders
            .AsNoTracking()
            .Select(o => new OrderSummary
            {
                Id = o.Id,
                OrderNumber = o.OrderNumber,
                Status = o.Status,
                ItemCount = o.Items.Count,
                Total = o.Items.Sum(i => i.Price * i.Quantity)
            });

        var total = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * size)
            .Take(size)
            .ToListAsync();

        return new PagedResult<OrderSummary>(items, total, page, size);
    }

    public async Task BulkUpdateStatusAsync(OrderStatus fromStatus, OrderStatus toStatus)
    {
        await _context.Orders
            .Where(o => o.Status == fromStatus)
            .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, toStatus));
    }
}

// Compiled query for hot paths
private static readonly Func<AppDbContext, Guid, Task<Order?>> GetOrderById =
    EF.CompileAsyncQuery((AppDbContext ctx, Guid id) =>
        ctx.Orders.AsNoTracking().FirstOrDefault(o => o.Id == id));

Step 4: Outbox Pattern for Reliable Messaging

// Domain/DomainEvents/IDomainEvent.cs
public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
}

// Infrastructure/Persistence/OutboxMessage.cs
public class OutboxMessage
{
    public Guid Id { get; set; }
    public string Type { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime OccurredOn { get; set; }
    public DateTime? ProcessedOn { get; set; }
    public string? Error { get; set; }
}

// Save domain events as outbox messages
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
    var domainEvents = ChangeTracker.Entries<IAggregateRoot>()
        .Select(e => e.Entity)
        .SelectMany(e =>
        {
            var events = e.DomainEvents.ToList();
            e.ClearDomainEvents();
            return events;
        })
        .ToList();

    var outboxMessages = domainEvents.Select(e => new OutboxMessage
    {
        Id = Guid.NewGuid(),
        Type = e.GetType().AssemblyQualifiedName!,
        Content = JsonSerializer.Serialize(e, _jsonOptions),
        OccurredOn = DateTime.UtcNow,
    }).ToList();

    OutboxMessages.AddRange(outboxMessages);

    return await base.SaveChangesAsync(ct);
}

Step 5: Event Sourcing with EF Core

// Domain/Events/OrderEvents.cs
public record OrderCreatedEvent(Guid OrderId, string OrderNumber, DateTime OccurredOn) : IDomainEvent;
public record OrderItemAddedEvent(Guid OrderId, string Product, decimal Price, int Quantity) : IDomainEvent;
public record OrderConfirmedEvent(Guid OrderId, DateTime OccurredOn) : IDomainEvent;

// Infrastructure/Persistence/EventStore.cs
public class EventStore : IEventStore
{
    private readonly AppDbContext _context;

    public async Task AppendEventsAsync(Guid aggregateId, IEnumerable<IDomainEvent> events, int expectedVersion)
    {
        var eventData = events.Select(e => new EventDescriptor
        {
            AggregateId = aggregateId,
            EventType = e.GetType().Name,
            EventData = JsonSerializer.Serialize(e),
            Version = expectedVersion++,
            Timestamp = DateTime.UtcNow,
        });

        await _context.EventDescriptors.AddRangeAsync(eventData);
        await _context.SaveChangesAsync();
    }

    public async Task<List<IDomainEvent>> GetEventsAsync(Guid aggregateId)
    {
        var descriptors = await _context.EventDescriptors
            .Where(d => d.AggregateId == aggregateId)
            .OrderBy(d => d.Version)
            .ToListAsync();

        return descriptors.Select(d =>
            JsonSerializer.Deserialize(d.EventData, GetEventType(d.EventType)) as IDomainEvent)
            .ToList()!;
    }

    private Type GetEventType(string eventType) => eventType switch
    {
        nameof(OrderCreatedEvent) => typeof(OrderCreatedEvent),
        nameof(OrderItemAddedEvent) => typeof(OrderItemAddedEvent),
        nameof(OrderConfirmedEvent) => typeof(OrderConfirmedEvent),
        _ => throw new InvalidOperationException(quot;Unknown event: {eventType}")
    };
}

Production Considerations

EF Core Connection Resiliency

services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        connectionString,
        sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(
                maxRetryCount: 3,
                maxRetryDelay: TimeSpan.FromSeconds(10),
                errorNumbersToAdd: null);
            sqlOptions.CommandTimeout(30);
        }));

Caching with LazyCache or IDistributedCache

public class CachedOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _inner;
    private readonly IDistributedCache _cache;

    public async Task<Order?> GetByIdAsync(Guid id)
    {
        var cacheKey = quot;order:{id}";
        var cached = await _cache.GetStringAsync(cacheKey);
        if (cached is not null) return JsonSerializer.Deserialize<Order>(cached);

        var order = await _inner.GetByIdAsync(id);
        if (order is not null)
            await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(order),
                new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
        return order;
    }
}

Anti-Patterns

Anti-PatternWhyFix
IQueryable outside repositoryLeaks DB details to applicationReturn IEnumerable<T> or List<T>
async void in ASP.NET CoreUncatchable exceptions, process crashAlways async Task
DbSet<T> in ApplicationCouples to EF CoreUse repository interfaces
Multiple Include() without AsSplitQuery()Cartesian explosion, huge result setsUse AsSplitQuery() or projection
SaveChangesAsync in each handlerNo transactional consistencyOutbox + TransactionBehavior
Direct IConfiguration injectionHard to test, violates ISPIOptions<T> pattern

Security Considerations

  • EF Core raw SQL: always use parameterization, never string interpolation
  • SQL injection via FromSqlRaw: validate all inputs, use FromSqlInterpolated
  • Encrypt sensitive columns with EF Core value converters
  • Use [EncryptColumn] attribute or custom ValueConverter for PII
  • Audit logging via EF Core SaveChangesInterceptor or outbox events

Testing Strategies

Unit Testing Pipeline Behaviors

[Fact]
public async Task ValidationBehavior_ShouldThrowWhenInvalid()
{
    var validator = new InlineValidator<TestRequest>();
    validator.RuleFor(x => x.Value).GreaterThan(0);
    var behavior = new ValidationBehavior<TestRequest, TestResponse>(
        new[] { validator });

    await Assert.ThrowsAsync<ValidationException>(() =>
        behavior.Handle(new TestRequest { Value = -1 }, () => Task.FromResult(new TestResponse()), CancellationToken.None));
}

Integration Testing EF Core

Use WebApplicationFactory<T> with in-memory or TestContainers SQL Server. Seed data in test setup. Verify with Shouldly or FluentAssertions. Use Verify for snapshot testing of complex responses.

Rules

  • MediatR pipeline order: Logging → Validation → Performance → Exception → Transaction.
  • Result pattern at Application boundary — never throw for expected failure paths.
  • Domain events dispatched via MediatR INotification, never directly.
  • Outbox pattern for reliable messaging — never publish events within same transaction.
  • EF Core: AsNoTracking for reads, AsSplitQuery for multiple includes, ExecuteUpdate for bulk.
  • Caching layer wraps repository — decorated with IDistributedCache.
  • All DB access goes through repository interface — no DbContext in handlers.

References

  • references/cqrs-mediatr.md — CQRS with MediatR
  • references/dotnet-cqrs.md — .NET CQRS Patterns
  • references/dotnet-event-sourcing.md — Event Sourcing in .NET
  • references/dotnet-pipeline-behaviors.md — MediatR Pipeline Behaviors
  • references/dotnet-result-pattern.md — Result Pattern
  • references/ef-core-patterns.md — EF Core Patterns

Handoff

Hand off to backend/dotnet/architecture/SKILL.md for architecture structure or backend/universal/api-response/SKILL.md for API response formatting.

Implementation Patterns

Factory Pattern for Module Creation

function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }

Builder Pattern for Complex Configuration

class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }

Production Considerations

Deployment Checklist

  • Production build with optimizations enabled
  • Environment variables configured per environment
  • Health check endpoint responds correctly
  • Error tracking and monitoring integrated
  • Logging level configured (not debug in production)
  • Resource limits configured
  • Database migrations applied
  • Static assets built and served from CDN or cache
  • Feature flags toggled appropriately
  • Rollback plan documented and tested

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1%CriticalRollback or fix
p95 latency> 500msWarningProfile and optimize
Uptime< 99.9%CriticalInvestigate infrastructure
Memory usage> 80%WarningCheck for leaks
CPU usage> 80%WarningScale up or optimize

Rules

  • Prefer composition over inheritance
  • Favor immutable data structures
  • Use dependency injection for testability
  • Keep functions pure when possible — no side effects
  • Fail fast with clear error messages
  • Don't repeat yourself (DRY) — extract shared logic
  • Keep it simple (KISS) — avoid unnecessary complexity
  • You aren't gonna need it (YAGNI) — build what's required
  • Separate concerns — single responsibility per module
  • Code to interfaces, not implementations
  • Write self-documenting code — clear names over comments
  • Prefer standard library over third-party dependencies
  • Handle errors explicitly — no silent failures
  • Validate inputs at boundaries
  • Log at appropriate levels (debug, info, warn, error)
Gespeicherte Skills