ASP.NET Core skill for Codex
Typed results, honest DI lifetimes, lean EF Core. Codex adds .NET endpoints that document themselves through typed results, fail with problem details from one IExceptionHandler, and validate configuration before the first request. It catches captive dependencies, keeps DbContext off background threads, projects read queries with AsNoTracking, reviews generated migrations for drop-and-add renames, and proves changes with WebApplicationFactory tests on a real database.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- aspnet-core Installs to ~/.agents/skills/aspnet-core/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install aspnet-core The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project aspnet-core Installs to .agents/skills/aspnet-core/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/aspnet-core.zipunzip -o aspnet-core.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) aspnet-core PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $aspnet-core, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds ASP.NET Core APIs on .NET 8-10 - minimal API route groups with TypedResults, or controllers when the project already uses them, DI lifetimes and the options pattern with ValidateOnStart, ProblemDetails with IExceptionHandler, built-in OpenAPI, EF Core DbContext usage, migrations and query performance, and WebApplicationFactory integration tests. Use when adding endpoints or services to a .NET web API, fixing a captive dependency or DbContext threading error, writing EF Core migrations, or setting up integration tests.
Use it when
- Adding endpoints, services or configuration to an ASP.NET Core API
- DI lifetime errors, DbContext threading errors or slow EF Core queries appear
- Writing or reviewing EF Core migrations
- A .NET API needs consistent problem details and accurate OpenAPI output
Use something else when
- You need general C# language features rather than web concerns: use csharp-modern
- The task is deploying the app to Azure rather than writing it: use azure-app-service
How to check the result
dotnet build with warnings as errors and dotnet test pass, the OpenAPI document lists each endpoint's status codes, and an integration test receives application/problem+json for a missing resource.
The mistake it prevents
Injecting a scoped DbContext into a singleton or hosted service, which works in a quick test and then corrupts state or throws under concurrent requests.
Done well means: endpoints return typed results so OpenAPI is accurate, every failure is an RFC 9457 problem details response, service lifetimes are correct (no scoped service captured by a singleton), configuration is validated at startup, EF Core queries are projected and untracked on reads, and an integration test boots the real pipeline.
Before changing anything
- Read
AGENTS.md, the.csproj(TargetFramework, package versions),Program.csandappsettings*.json. Checkglobal.jsonfor a pinned SDK. - Detect style:
rg -n "MapGroup|MapGet|ControllerBase" --type cs. Follow it. New services default to minimal APIs; do not convert existing controllers unasked. - Baseline:
dotnet buildanddotnet test. Restore needs network for new packages; in an offline sandbox use the local NuGet cache or ask before installing.
Program.cs shape
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<DomainExceptionHandler>();
builder.Services.AddOpenApi(); // Microsoft.AspNetCore.OpenApi, .NET 9+
builder.Services.AddValidation(); // .NET 10 minimal API validation
builder.Services.AddOptions<PaymentOptions>()
.BindConfiguration(PaymentOptions.Section)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddDbContext<AppDb>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddHealthChecks().AddDbContextCheck<AppDb>();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment()) app.MapOpenApi();
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz");
app.MapOrders();
app.Run();
Middleware order matters: exception handling first, then routing-dependent pieces
(CORS, authentication, authorization), then endpoints. On .NET 8 without AddValidation,
validate with a small endpoint filter or FluentValidation instead.
Minimal API endpoints
public static class OrderEndpoints
{
public static IEndpointRouteBuilder MapOrders(this IEndpointRouteBuilder app)
{
var g = app.MapGroup("/api/orders").WithTags("Orders").RequireAuthorization();
g.MapGet("/{id:guid}", GetOrder).WithName("GetOrder");
g.MapPost("/", CreateOrder);
return app;
}
static async Task<Results<Ok<OrderDto>, NotFound>> GetOrder(
Guid id, AppDb db, CancellationToken ct)
{
var dto = await db.Orders.AsNoTracking()
.Where(o => o.Id == id)
.Select(o => new OrderDto(o.Id, o.Status, o.Lines.Sum(l => l.Quantity * l.UnitPrice)))
.SingleOrDefaultAsync(ct);
return dto is null ? TypedResults.NotFound() : TypedResults.Ok(dto);
}
static async Task<Created<OrderDto>> CreateOrder(
CreateOrderRequest req, IOrderService orders, CancellationToken ct)
{
var dto = await orders.CreateAsync(req, ct);
return TypedResults.Created($"/api/orders/{dto.Id}", dto);
}
}
public record CreateOrderRequest([Required, StringLength(32)] string Sku, [Range(1, 100)] int Quantity);
Results<T1, T2>withTypedResultsdocuments every status in OpenAPI;Results.Ok(untypedIResult) does not.- Accept
CancellationTokenand pass it to every async call. - Put cross-cutting checks in endpoint filters (
AddEndpointFilter) on the group.
Controllers: inherit ControllerBase, mark [ApiController] (automatic 400 problem
details on model validation), return ActionResult<T>, keep logic in services.
Errors as problem details
public sealed class DomainExceptionHandler(IProblemDetailsService problems) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
{
var (status, title) = ex switch
{
NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
ConflictException => (StatusCodes.Status409Conflict, "Conflict"),
_ => (0, "")
};
if (status == 0) return false; // fall through to the default 500
ctx.Response.StatusCode = status;
return await problems.TryWriteAsync(new()
{
HttpContext = ctx,
ProblemDetails = { Status = status, Title = title, Detail = ex.Message },
Exception = ex
});
}
}
For expected outcomes (not found, validation), return results directly; reserve exceptions for exceptional paths. Never put stack traces in responses outside Development.
Dependency injection
| Lifetime | Use for | Never |
|---|---|---|
| Singleton | Stateless helpers, caches, HttpClient factories, options | Depend on a scoped service (captive dependency) |
| Scoped | DbContext, unit of work, per-request user context | Use from a background thread without creating a scope |
| Transient | Lightweight stateless services | Hold disposable resources you expect to share |
- Enable
ValidateScopesandValidateOnBuild(on by default in Development) to catch captive dependencies at startup. BackgroundServiceand singletons get aDbContextthroughIServiceScopeFactory:using var scope = factory.CreateScope();per unit of work, orIDbContextFactory<AppDb>.- Outbound HTTP:
AddHttpClient<TClient>()typed clients; nevernew HttpClient()per call. Add resilience withMicrosoft.Extensions.Http.Resilience(AddStandardResilienceHandler()).
EF Core
- Reads:
AsNoTracking()plusSelectinto DTOs. Loading full entities to map them in memory pulls every column and tracks objects you will not save. - Avoid N+1: project in one query, or
IncludewithAsSplitQuery()when including several collections (cartesian explosion otherwise). DbContextis not thread-safe. “A second operation was started on this context” means parallel awaits on one context (Task.WhenAllover queries); run them sequentially or use separate contexts.- Concurrency: add a row version (
[Timestamp]on SQL Server,xminmapping on Npgsql) and turnDbUpdateConcurrencyExceptioninto 409.
Migrations (the dotnet-ef tool must be installed: dotnet tool install --global dotnet-ef):
dotnet ef migrations add AddOrderStatus --project src/Infra --startup-project src/Api
dotnet ef migrations script --idempotent -o artifacts/migrate.sql # review before prod
dotnet ef database update # local/dev databases only
Read the generated migration before committing: renames often come out as drop plus add, which loses data. Applying migrations to shared or production databases is a deployment step; ask the user before running anything against a non-local connection string.
Integration tests
public class OrderApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Get_unknown_order_returns_404_problem()
{
var client = factory.WithWebHostBuilder(b => b.ConfigureTestServices(s =>
{
s.RemoveAll<DbContextOptions<AppDb>>();
s.AddDbContext<AppDb>(o => o.UseNpgsql(TestDb.ConnectionString));
})).CreateClient();
var res = await client.GetAsync($"/api/orders/{Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, res.StatusCode);
Assert.Equal("application/problem+json", res.Content.Headers.ContentType?.MediaType);
}
}
- Projects on .NET 9 or earlier need
public partial class Program;at the end ofProgram.csso the test project can reference it. - Test against the real database engine (Testcontainers), not the EF in-memory provider, which ignores constraints, transactions and SQL translation.
Checklist
- Style matches the project (minimal APIs vs controllers)
- Typed results; OpenAPI document shows every status code
- Options validated on start; no secrets in
appsettings.json(user-secrets or env vars) - No captive dependencies; background work creates scopes
- Read queries untracked and projected; migration reviewed
-
dotnet build -warnaserroranddotnet testpass
Gotchas
async voidhandlers crash the process on exception; always returnTask..Result/.Wait()on tasks inside request code risks thread-pool starvation.- Route constraint
{id:int}rejecting a value returns 404, not 400. UseCorsmust sit afterUseRouting(implicit in minimal hosting) and beforeUseAuthorization, or preflight requests fail.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "ASP.NET Core"
short_description: "Typed results, honest DI lifetimes, lean EF Core"
default_prompt: "$aspnet-core Add a minimal API endpoint group for orders with validation and EF Core" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.