Security
dotnet-expert
Use when building .NET 8/9 applications, ASP.NET Core APIs
---
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($"/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)
security
By
Comments
Sign in to leave a comment