Agent skill
aspnet-core
ASP.NET Core framework guardrails, patterns, and best practices for AI-assisted development. Use when working with ASP.NET Core projects, or when the user mentions ASP.NET Core. Provides Minimal APIs, EF Core, authentication, middleware, and deployment guidelines.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/aspnet-core-ar4mirez-samuel
Metadata
Additional technical details for this skill
- author
- samuel
- version
- 1.0
- category
- framework
- language
- csharp
- extensions
- .cs
SKILL.md
ASP.NET Core Guide
Applies to: ASP.NET Core 8.x (LTS), C# 12, Minimal APIs, MVC, Web APIs
Core Principles
- Clean Architecture: Separate API, Core (domain), Infrastructure, and Contracts layers
- Dependency Injection: Built-in DI container for all service registrations
- Minimal APIs First: Prefer Minimal APIs for new endpoints; use controllers for complex scenarios
- Async Everywhere: All I/O-bound operations must be async with CancellationToken
- Records for DTOs: Immutable data transfer objects using C# records
Guardrails
Version & Dependencies
- Target
net8.0(LTS) with<Nullable>enable</Nullable>and<ImplicitUsings>enable</ImplicitUsings> - Enable
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>inDirectory.Build.props - Use Central Package Management (
Directory.Packages.props) for version consistency - Include StyleCop.Analyzers with
<AnalysisLevel>latest-recommended</AnalysisLevel>
Code Style
- File-scoped namespaces:
namespace MyApp.Core.Entities; - Primary constructors for DI:
public class UserService(IUserRepository repo, IMapper mapper) - Use
requiredkeyword for mandatory properties on entities - Records for all request/response DTOs
- Avoid
async void-- always returnTaskorTask<T>
Error Handling
- Use a global exception middleware (not per-controller try/catch)
- Define domain exception hierarchy:
DomainException->NotFoundException,ConflictException,ValidationException - Map domain exceptions to HTTP status codes in middleware
- Log unhandled exceptions at Error level; log expected exceptions at Warning
- Never expose stack traces in production responses
Security
- Never hardcode connection strings or secrets (use
appsettings.json+ environment overrides) - Always validate JWT
Issuer,Audience,Lifetime, andIssuerSigningKey - Use
[Authorize]attribute on all endpoints that require authentication - Role-based authorization:
[Authorize(Roles = "Admin")] - Use HTTPS in production; enforce with
UseHttpsRedirection()
Project Structure
MyApp/
├── MyApp.Api/ # Web API project
│ ├── Controllers/ # API controllers (MVC pattern)
│ ├── Endpoints/ # Minimal API endpoints (alternative)
│ ├── Middleware/ # Custom middleware
│ ├── Filters/ # Action filters
│ ├── Validators/ # FluentValidation validators
│ ├── Mappings/ # Mapster/AutoMapper configurations
│ ├── Extensions/ # Service collection extensions
│ ├── Program.cs # Entry point and DI configuration
│ ├── appsettings.json # Configuration
│ └── appsettings.Development.json
├── MyApp.Core/ # Domain/business logic (no dependencies)
│ ├── Entities/ # Domain entities
│ ├── Interfaces/ # Repository and service interfaces
│ ├── Services/ # Business logic implementations
│ └── Exceptions/ # Domain exception types
├── MyApp.Infrastructure/ # Data access, external services
│ ├── Data/ # DbContext and EF configurations
│ │ └── Configurations/ # IEntityTypeConfiguration<T>
│ └── Repositories/ # Repository implementations
├── MyApp.Contracts/ # DTOs, API contracts (shared)
│ ├── Requests/ # Input DTOs
│ └── Responses/ # Output DTOs
├── tests/
│ ├── MyApp.UnitTests/ # xUnit + Moq + FluentAssertions
│ └── MyApp.IntegrationTests/ # WebApplicationFactory + Testcontainers
├── MyApp.sln
├── Directory.Build.props # Shared build settings
├── Directory.Packages.props # Central package management
└── docker-compose.yml
Layer rules:
Corehas zero external dependencies (no EF Core, no ASP.NET references)InfrastructurereferencesCoreonlyApireferencesCore,Infrastructure, andContractsContractshas no project references (shareable with clients)
Minimal APIs
Endpoint Group Pattern
public static class UserEndpoints
{
public static IEndpointRouteBuilder MapUserEndpoints(
this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/users")
.WithTags("Users")
.WithOpenApi();
group.MapGet("/", GetAll)
.RequireAuthorization()
.Produces<PagedResponse<UserResponse>>();
group.MapGet("/{id:long}", GetById)
.RequireAuthorization()
.Produces<UserResponse>()
.Produces(StatusCodes.Status404NotFound);
group.MapPost("/", Create)
.Produces<UserResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest);
return routes;
}
private static async Task<IResult> GetById(
long id, IUserService service, CancellationToken ct)
{
var user = await service.GetByIdAsync(id, ct);
return Results.Ok(user);
}
private static async Task<IResult> Create(
CreateUserRequest request,
IUserService service,
IValidator<CreateUserRequest> validator,
CancellationToken ct)
{
var validation = await validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return Results.BadRequest(validation.Errors);
var user = await service.CreateAsync(request, ct);
return Results.Created($"/api/users/{user.Id}", user);
}
}
Register in Program.cs: app.MapUserEndpoints();
Controllers
Standard REST Controller
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly IValidator<CreateUserRequest> _validator;
public UsersController(
IUserService userService,
IValidator<CreateUserRequest> validator)
{
_userService = userService;
_validator = validator;
}
[HttpGet("{id:long}")]
[Authorize]
[ProducesResponseType(typeof(UserResponse), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<UserResponse>> GetById(
long id, CancellationToken ct)
{
var user = await _userService.GetByIdAsync(id, ct);
return Ok(user);
}
[HttpPost]
[ProducesResponseType(typeof(UserResponse), 201)]
[ProducesResponseType(400)]
public async Task<ActionResult<UserResponse>> Create(
[FromBody] CreateUserRequest request, CancellationToken ct)
{
var validation = await _validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return BadRequest(validation.Errors);
var user = await _userService.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
}
Guidelines:
- Always use
[ApiController]for automatic model binding and validation - Use
CancellationTokenon every async action - Annotate with
[ProducesResponseType]for OpenAPI documentation - Use route constraints:
{id:long},{slug:alpha},{page:int:min(1)}
Entity Framework Core
DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(AppDbContext).Assembly);
}
public override Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<User>())
{
if (entry.State == EntityState.Modified)
entry.Entity.UpdatedAt = DateTime.UtcNow;
}
return base.SaveChangesAsync(cancellationToken);
}
}
Entity Configuration
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Email).HasMaxLength(255).IsRequired();
builder.HasIndex(u => u.Email).IsUnique();
builder.Property(u => u.Role).HasConversion<string>().HasMaxLength(50);
builder.Property(u => u.Active).HasDefaultValue(true);
}
}
EF Core rules:
- Use
IEntityTypeConfiguration<T>for all configurations (not inline inOnModelCreating) - Use
AsNoTracking()for read-only queries - Always include
CancellationTokenin async EF methods - Use snake_case for database column names via configuration
Middleware
Exception Handling Middleware
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(
RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(
HttpContext context, Exception exception)
{
var (statusCode, response) = exception switch
{
NotFoundException ex => (404, new { ex.Message }),
ConflictException ex => (409, new { ex.Message }),
ValidationException ex => (400, new { ex.Message, ex.Errors }),
_ => (500, (object)new { Message = "An unexpected error occurred" })
};
if (statusCode == 500)
_logger.LogError(exception, "Unhandled exception");
context.Response.ContentType = "application/json";
context.Response.StatusCode = statusCode;
await context.Response.WriteAsJsonAsync(response);
}
}
Register: app.UseMiddleware<ExceptionMiddleware>(); (first in pipeline)
Dependency Injection & Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, cfg) =>
cfg.ReadFrom.Configuration(ctx.Configuration));
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
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:Secret"]!))
};
});
builder.Services.AddAuthorization();
builder.Services.AddHealthChecks().AddDbContextCheck<AppDbContext>();
var app = builder.Build();
app.UseMiddleware<ExceptionMiddleware>();
app.UseSerilogRequestLogging();
app.UseAuthentication();
app.UseAuthorization();
app.MapUserEndpoints();
app.MapHealthChecks("/health");
app.Run();
public partial class Program { } // For integration test access
DI lifetimes:
Scoped: repositories, services, DbContext (per-request)Singleton: configuration objects, mapping configs, HttpClient factoriesTransient: lightweight stateless services
Validation (FluentValidation)
public class CreateUserRequestValidator
: AbstractValidator<CreateUserRequest>
{
public CreateUserRequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().EmailAddress().MaximumLength(255);
RuleFor(x => x.Password)
.NotEmpty().MinimumLength(8)
.Matches("[A-Z]").WithMessage("Must contain uppercase")
.Matches("[0-9]").WithMessage("Must contain digit")
.Matches("[^a-zA-Z0-9]").WithMessage("Must contain special char");
RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
}
}
Register: builder.Services.AddValidatorsFromAssemblyContaining<Program>();
Testing
Unit Test Pattern (xUnit + Moq + FluentAssertions)
public class UserServiceTests
{
private readonly Mock<IUserRepository> _repoMock = new();
private readonly Mock<IMapper> _mapperMock = new();
private readonly Mock<ILogger<UserService>> _loggerMock = new();
private readonly UserService _sut;
public UserServiceTests()
{
_sut = new UserService(
_repoMock.Object, _mapperMock.Object, _loggerMock.Object);
}
[Fact]
public async Task GetByIdAsync_WhenNotFound_ThrowsNotFoundException()
{
_repoMock.Setup(r => r.GetByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((User?)null);
var act = () => _sut.GetByIdAsync(999);
await act.Should().ThrowAsync<NotFoundException>();
}
}
Integration Tests (WebApplicationFactory + Testcontainers)
public class UsersControllerTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine").Build();
private WebApplicationFactory<Program> _factory = null!;
private HttpClient _client = null!;
public async Task InitializeAsync()
{
await _postgres.StartAsync();
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(b => b.ConfigureServices(services =>
services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(_postgres.GetConnectionString()))));
_client = _factory.CreateClient();
}
public async Task DisposeAsync()
{
await _factory.DisposeAsync();
await _postgres.DisposeAsync();
}
[Fact]
public async Task Create_ValidRequest_ReturnsCreated()
{
var request = new CreateUserRequest("[email protected]", "Password123!", "John", "Doe");
var response = await _client.PostAsJsonAsync("/api/users", request);
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
}
Commands
# Restore and build
dotnet restore
dotnet build
# Run with hot reload
dotnet watch run --project MyApp.Api
# Run tests
dotnet test
dotnet test --collect:"XPlat Code Coverage"
# EF Core migrations
dotnet tool install --global dotnet-ef
dotnet ef migrations add MigrationName -p MyApp.Infrastructure -s MyApp.Api
dotnet ef database update -p MyApp.Infrastructure -s MyApp.Api
# Format and lint
dotnet format
# Publish and containerize
dotnet publish -c Release -o ./publish
docker build -t myapp:latest .
Best Practices
DO: Central Package Management | CancellationToken everywhere | Records for DTOs |
FluentValidation | Clean Architecture layers | Health checks (/health) | Serilog structured logging | Testcontainers for integration tests
DON'T: Expose entities in API responses | Synchronous DB calls | Catch-and-swallow exceptions |
Hardcode secrets | Skip API validation | Magic strings (use nameof())
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- EF Core advanced patterns, Identity/Security, SignalR, Blazor integration, testing strategies, deployment
External References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?