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.

  • Backend & APIs
  • $aspnet-core
  • 211 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- aspnet-core

Installs to ~/.agents/skills/aspnet-core/ for every project on this machine.

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.

~/.agents/skills/aspnet-core/SKILL.md

--- name: aspnet-core description: 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… license: MIT ---

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

  1. Read AGENTS.md, the .csproj (TargetFramework, package versions), Program.cs and appsettings*.json. Check global.json for a pinned SDK.
  2. Detect style: rg -n "MapGroup|MapGet|ControllerBase" --type cs. Follow it. New services default to minimal APIs; do not convert existing controllers unasked.
  3. Baseline: dotnet build and dotnet 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> with TypedResults documents every status in OpenAPI; Results.Ok (untyped IResult) does not.
  • Accept CancellationToken and 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

LifetimeUse forNever
SingletonStateless helpers, caches, HttpClient factories, optionsDepend on a scoped service (captive dependency)
ScopedDbContext, unit of work, per-request user contextUse from a background thread without creating a scope
TransientLightweight stateless servicesHold disposable resources you expect to share
  • Enable ValidateScopes and ValidateOnBuild (on by default in Development) to catch captive dependencies at startup.
  • BackgroundService and singletons get a DbContext through IServiceScopeFactory: using var scope = factory.CreateScope(); per unit of work, or IDbContextFactory<AppDb>.
  • Outbound HTTP: AddHttpClient<TClient>() typed clients; never new HttpClient() per call. Add resilience with Microsoft.Extensions.Http.Resilience (AddStandardResilienceHandler()).

EF Core

  • Reads: AsNoTracking() plus Select into 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 Include with AsSplitQuery() when including several collections (cartesian explosion otherwise).
  • DbContext is not thread-safe. “A second operation was started on this context” means parallel awaits on one context (Task.WhenAll over queries); run them sequentially or use separate contexts.
  • Concurrency: add a row version ([Timestamp] on SQL Server, xmin mapping on Npgsql) and turn DbUpdateConcurrencyException into 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 of Program.cs so 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 -warnaserror and dotnet test pass

Gotchas

  • async void handlers crash the process on exception; always return Task.
  • .Result / .Wait() on tasks inside request code risks thread-pool starvation.
  • Route constraint {id:int} rejecting a value returns 404, not 400.
  • UseCors must sit after UseRouting (implicit in minimal hosting) and before UseAuthorization, or preflight requests fail.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem