Back to Skills
    šŸ¦ž

    dotnet-expert

    Use when building .NET 8/9 applications, ASP.NET Core APIs

    By @jgarrison929
    View on GitHub
    SKILL.md
    ---
    name: dotnet-expert
    version: 1.0.0
    description: Use when building .NET 8/9 applications, ASP.NET Core APIs, Entity Framework Core, MediatR CQRS, modular monolith architecture, FluentValidation, Result pattern, JWT authentication, or any C# backend development question.
    triggers:
      - .NET
      - dotnet
      - C#
      - ASP.NET
      - Entity Framework
      - EF Core
      - MediatR
      - CQRS
      - FluentValidation
      - Minimal API
      - controller
      - DbContext
      - migration
      - Pitbull
      - modular monolith
      - Result pattern
    role: specialist
    scope: implementation
    output-format: code
    ---
    
    # .NET Expert
    
    Senior .NET 9 / ASP.NET Core specialist with expertise in clean architecture, CQRS, and modular monolith patterns.
    
    ## Role Definition
    
    You are a senior .NET engineer building production-grade APIs with ASP.NET Core, Entity Framework Core 9, MediatR, and FluentValidation. You follow clean architecture principles with a pragmatic approach.
    
    ## Core Principles
    
    1. **Result pattern over exceptions** for business logic — exceptions for infrastructure only
    2. **CQRS with MediatR** — separate commands (writes) from queries (reads)
    3. **FluentValidation** for all input validation in the pipeline
    4. **Modular monolith** — organized by feature/domain, not by technical layer
    5. **Strongly-typed IDs** to prevent primitive obsession
    6. **Async all the way** — never `.Result` or `.Wait()`
    
    ---
    
    ## Project Structure (Modular Monolith)
    
    ```
    src/
    ā”œā”€ā”€ Api/                          # ASP.NET Core host
    │   ā”œā”€ā”€ Program.cs
    │   ā”œā”€ā”€ appsettings.json
    │   └── Endpoints/                # Minimal API endpoint definitions
    ā”œā”€ā”€ Modules/
    │   ā”œā”€ā”€ Users/
    │   │   ā”œā”€ā”€ Users.Core/           # Domain entities, interfaces
    │   │   ā”œā”€ā”€ Users.Application/    # Commands, queries, handlers
    │   │   └── Users.Infrastructure/ # EF Core, external services
    │   ā”œā”€ā”€ Orders/
    │   │   ā”œā”€ā”€ Orders.Core/
    │   │   ā”œā”€ā”€ Orders.Application/
    │   │   └── Orders.Infrastructure/
    │   └── Shared/
    │       ā”œā”€ā”€ Shared.Core/          # Common abstractions
    │       └── Shared.Infrastructure/# Cross-cutting concerns
    └── Tests/
        ā”œā”€ā”€ Users.Tests/
        └── Orders.Tests/
    ```
    
    ---
    
    ## Minimal API Patterns
    
    ### Basic Endpoint Group
    
    ```csharp
    // Api/Endpoints/UserEndpoints.cs
    public static class UserEndpoints
    {
        public static void MapUserEndpoints(this IEndpointRouteBuilder app)
        {
            var group = app.MapGroup("/api/users")
                .WithTags("Users")
                .RequireAuthorization();
    
            group.MapGet("/", GetUsers);
            group.MapGet("/{id:guid}", GetUserById);
            group.MapPost("/", CreateUser);
            group.MapPut("/{id:guid}", UpdateUser);
            group.MapDelete("/{id:guid}", DeleteUser);
        }
    
        private static async Task<IResult> GetUsers(
            [AsParameters] GetUsersQuery query,
            ISender mediator,
            CancellationToken ct)
        {
            var result = await mediator.Send(query, ct);
            return result.Match(
                success => Results.Ok(success),
                error => Results.Problem(error.ToProblemDetails()));
        }
    
        private static async Task<IResult> GetUserById(
            Guid id,
            ISender mediator,
            CancellationToken ct)
        {
            var result = await mediator.Send(new GetUserByIdQuery(id), ct);
            return result.Match(
                success => Results.Ok(success),
                error => error.Type == ErrorType.NotFound
                    ? Results.NotFound()
                    : Results.Problem(error.ToProblemDetails()));
        }
    
        private static async Task<IResult> CreateUser(
            CreateUserCommand command,
            ISender mediator,
            CancellationToken ct)
        {
            var result = await mediator.Send(command, ct);
            return result.Match(
                success => Results.Created(
    quot;/api/users/{success.Id}", success), error => Results.Problem(error.ToProblemDetails())); } } ``` ### Program.cs Setup ```csharp var builder = WebApplication.CreateBuilder(args); // Add modules builder.Services.AddUsersModule(builder.Configuration); builder.Services.AddOrdersModule(builder.Configuration); // Add shared infrastructure builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( typeof(UsersModule).Assembly, typeof(OrdersModule).Assembly)); builder.Services.AddValidatorsFromAssemblies(new[] { typeof(UsersModule).Assembly, typeof(OrdersModule).Assembly, }); // Add validation pipeline behavior builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)), }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapUserEndpoints(); app.MapOrderEndpoints(); app.Run(); ``` --- ## Result Pattern ### Result Type ```csharp // Shared.Core/Result.cs public sealed class Result<T> { public T? Value { get; } public Error? Error { get; } public bool IsSuccess { get; } private Result(T value) { Value = value; IsSuccess = true; } private Result(Error error) { Error = error; IsSuccess = false; } public static Result<T> Success(T value) => new(value); public static Result<T> Failure(Error error) => new(error); public TResult Match<TResult>( Func<T, TResult> onSuccess, Func<Error, TResult> onFailure) => IsSuccess ? onSuccess(Value!) : onFailure(Error!); } public sealed record Error(string Code, string Message, ErrorType Type = ErrorType.Failure) { public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound); public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation); public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict); public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden); public ProblemDetails ToProblemDetails() => new() { Title = Code, Detail = Message, Status = Type switch { ErrorType.NotFound => StatusCodes.Status404NotFound, ErrorType.Validation => StatusCodes.Status400BadRequest, ErrorType.Conflict => StatusCodes.Status409Conflict, ErrorType.Forbidden => StatusCodes.Status403Forbidden, _ => StatusCodes.Status500InternalServerError, }, }; } public enum ErrorType { Failure, NotFound, Validation, Conflict, Forbidden } ``` ### Usage in Handlers ```csharp // No exceptions for business logic! public sealed class CreateUserHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>> { private readonly AppDbContext _db; public CreateUserHandler(AppDbContext db) => _db = db; public async Task<Result<UserResponse>> Handle( CreateUserCommand command, CancellationToken ct) { // Business rule validation returns errors, not exceptions var existingUser = await _db.Users .AnyAsync(u => u.Email == command.Email, ct); if (existingUser) return Result<UserResponse>.Failure( Error.Conflict("User.DuplicateEmail", "A user with this email already exists")); var user = new User { Id = Guid.NewGuid(), Email = command.Email, Name = command.Name, CreatedAt = DateTime.UtcNow, }; _db.Users.Add(user); await _db.SaveChangesAsync(ct); return Result<UserResponse>.Success(user.ToResponse()); } } ``` --- ## MediatR CQRS ### Commands (Write Operations) ```csharp // Users.Application/Commands/CreateUserCommand.cs public sealed record CreateUserCommand( string Email, string Name, string Password) : IRequest<Result<UserResponse>>; ``` ### Queries (Read Operations) ```csharp // Users.Application/Queries/GetUsersQuery.cs public sealed record GetUsersQuery( int Page = 1, int PageSize = 20, string? Search = null) : IRequest<Result<PagedResult<UserResponse>>>; public sealed class GetUsersHandler : IRequestHandler<GetUsersQuery, Result<PagedResult<UserResponse>>> { private readonly AppDbContext _db; public GetUsersHandler(AppDbContext db) => _db = db; public async Task<Result<PagedResult<UserResponse>>> Handle( GetUsersQuery query, CancellationToken ct) { var dbQuery = _db.Users.AsNoTracking(); if (!string.IsNullOrWhiteSpace(query.Search)) dbQuery = dbQuery.Where(u => u.Name.Contains(query.Search) || u.Email.Contains(query.Search)); var total = await dbQuery.CountAsync(ct); var users = await dbQuery .OrderBy(u => u.Name) .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) .Select(u => u.ToResponse()) .ToListAsync(ct); return Result<PagedResult<UserResponse>>.Success( new PagedResult<UserResponse>(users, total, query.Page, query.PageSize)); } } ``` ### Validation Pipeline Behavior ```csharp public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators; public async Task<TRespon ... (truncated)