# Dotnet Minimal API

> Guide for creating .NET Minimal API projects following best practices. Use this when asked to create, scaffold, or extend a .NET Minimal API application.

- Skill: `mastrauckas/dotnet-minimal-api` (Agent Skill, multi-file: 57 files)
- Install (CLI): `npx skillmds@latest add mastrauckas/dotnet-minimal-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mastrauckas/dotnet-minimal-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: mastrauckas (https://skillmd.com/u/mastrauckas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mastrauckas/dotnet-minimal-api

---


## Quick Start

1. Ask for the **solution name** and **project name** (see
   [Scaffolding](#scaffolding-a-new-project))
2. Copy `template/` into the output directory
3. Rename all src projects, all test projects, and all namespace references from
   `MyMinimalWebApp.Api` → `<ProjectName>` (see [Scaffolding](#scaffolding-a-new-project))
4. Replace `Item`/`Items` with your domain entity name
5. Run `dotnet test` to verify everything passes

## Scaffolding a New Project

When asked to create a brand-new project, ask the user for the required inputs before scaffolding.
If the user has already supplied any of these values, do not ask for them again. Only ask for
missing values:

1. **Solution name** — what should the solution be called? (required)
2. **Project name** — what should the API project be called? (required; conventionally
   `<SolutionName>.Api`)
3. **Output directory** — where should the project be created? (defaults to the current working
   directory if the user does not specify one)
4. **HTTP port** — generate a random port in the range 8000–8999 by running the appropriate command
   for the user's platform:
   - **Windows**: `Get-Random -Minimum 8000 -Maximum 8999`
   - **Linux**: `shuf -i 8000-8999 -n 1`
   - **macOS**: `jot -r 1 8000 8999`
   - **Fallback** (any platform): `python3 -c "import random; print(random.randint(8000, 8999))"` or
     `node -e "console.log(Math.floor(Math.random()*1000)+8000)"`

   Present the generated port as a choice alongside a freeform option so the user can accept or
   enter their own. Example prompt: _"Suggested HTTP port: 8432. Use this or enter your own."_

Once you have the answers:

- Copy the contents of the `template/` directory directly into the output directory (do **not**
  create an extra subdirectory — the output directory itself is the project root)
- Rename the template solution file (`MyMinimalWebApp.slnx` in the current template) to
  `<SolutionName>` while preserving its extension.
- Rename `src/MyMinimalWebApp.Api/` → `src/<ProjectName>/`
- Rename `src/MyMinimalWebApp.Api/MyMinimalWebApp.Api.csproj` →
  `src/<ProjectName>/<ProjectName>.csproj`
- Update all namespace references from `MyMinimalWebApp.Api` → `<ProjectName>` throughout all `.cs`
  files
- Update all project references in the solution file
- Update the `InternalsVisibleTo` in `src/<ProjectName>/<ProjectName>.csproj` from
  `MyMinimalWebApp.Api.IntegrationTests` → `<ProjectName>.IntegrationTests`
- Rename `tests/MyMinimalWebApp.Api.IntegrationTests/` → `tests/<ProjectName>.IntegrationTests/`
- Rename `tests/MyMinimalWebApp.Api.UnitTests/` → `tests/<ProjectName>.UnitTests/`
- Update all namespace references in test projects from `MyMinimalWebApp.Api` → `<ProjectName>`
- Replace `Item`/`Items` with the appropriate domain entity name if provided
- Replace the HTTP port `5262` with `<HttpPort>` and the HTTPS port `7105` with `<HttpPort + 1>` in:
  (Note: the template's ports `5262`/`7105` are default .NET placeholders and do not follow the `+1`
  convention — the convention only applies to generated ports for new projects.)
  - `src/<ProjectName>/Properties/launchSettings.json` (both `http` and `https` profiles)
  - `src/<ProjectName>/appsettings.json` (both `Kestrel.Endpoints.Http.Url` and
    `Kestrel.Endpoints.Https.Url`)
  - `http-files/items.http`
  - `http-files/health.http`

## Template

A complete working reference solution is included in the `template/` directory alongside this file.
When scaffolding a new project, use this template as the starting point — copy and rename it,
replacing `Item`/`Items` with the appropriate domain entity name.

```
template/
  MyMinimalWebApp.slnx                        ← current template solution; preserve extension if this ever changes
  global.json                               ← pins .NET SDK version
  Directory.Build.props                     ← shared build properties (TreatWarningsAsErrors, EnforceCodeStyleInBuild)
  Directory.Packages.props                  ← Central Package Management (all NuGet versions here)
  Directory.Build.targets                   ← placeholder for post-build targets
  .editorconfig                             ← C# coding style rules at :warning severity
  .gitignore                                ← .NET gitignore (bin, obj, logs, TestResults)
  src/
    MyMinimalWebApp.Api/
      Program.cs                            ← minimal: UseSerilog + ConfigureBuilder + ConfigureApp
      GlobalUsings.cs                       ← all global usings centralized here
      Configuration/
        BuilderConfiguration.cs             ← all service registrations (RegisterX methods)
        AppConfiguration.cs                 ← all middleware and endpoint mapping
      Endpoints/
        HttpRoutes.cs                       ← creates api root group, calls MapItemEndpoints
        ItemEndpoints.cs                    ← 5 CRUD endpoints as private static handlers
      Dtos/
        ItemDto.cs                          ← response DTO
        CreateItemRequest.cs                ← create request DTO
        UpdateItemRequest.cs                ← update request DTO
      Logging/
        Log.cs                              ← [LoggerMessage] source-generated log methods
      Middleware/
        ExceptionMiddleware.cs              ← catches unhandled exceptions, returns ProblemDetails
      Services/
        IItemService.cs
        ItemService.cs
      Properties/
        launchSettings.json                 ← Kestrel profiles, launchBrowser: false
      appsettings.json                      ← Serilog, Kestrel, Cors, ConnectionStrings, Auth, KeyVault
      appsettings.Development.json          ← DetailedErrors, Debug log level override
      appsettings.Staging.json              ← Warning log level, per-env placeholders
      appsettings.Production.json           ← Warning log level, per-env placeholders
  tests/
    MyMinimalWebApp.Api.IntegrationTests/
      Endpoints/ItemEndpointsTests.cs       ← WebApplicationFactory<Program> integration tests
      HealthChecks/HealthCheckTests.cs      ← /health, /health/live, /health/ready tests
      Middleware/ExceptionMiddlewareTests.cs ← 500 + ProblemDetails test
      Middleware/ThrowingAppFactory.cs      ← custom WebApplicationFactory for exception tests
      GlobalUsings.cs                       ← all global usings centralized here
    MyMinimalWebApp.Api.UnitTests/
      Services/ItemServiceTests.cs          ← unit tests with Bogus test data
      GlobalUsings.cs                       ← all global usings centralized here
  http-files/
    items.http                              ← all CRUD requests
    health.http                             ← health check requests
```

## Coding Conventions

- Never add `using` directives to individual C# files. Always add them to `GlobalUsings.cs`.
- Use `var` for all local variables — enforced by `csharp_style_var_elsewhere = true:warning`;
  explicit types for locals are a build error
- For method declarations and calls with 2+ parameters: first parameter stays on the same line as
  the method name, each additional parameter on its own line
- Method chains with 2+ dots → each `.` on its own line
- `TreatWarningsAsErrors = true` + `EnforceCodeStyleInBuild = true` — many style rules are enforced
  at build time, but not every convention in this skill is machine-enforced
- ILogger aliases in GlobalUsings: `ILogger` = `Microsoft.Extensions.Logging.ILogger`,
  `SerilogLogger` = `Serilog.ILogger`

## Key Patterns

### Program.cs

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.ConfigureBuilder();

var app = builder.Build();
app.ConfigureApp();
app.Run();

public partial class Program { }
```

`UseSerilog` is called inside `RegisterLogging()` in `BuilderConfiguration.cs` — do **not** add it
to `Program.cs`.

### BuilderConfiguration.cs

Extension block on `WebApplicationBuilder` — one `RegisterX()` method per concern:

```csharp
public static class BuilderConfigurationExtensions
{
    extension(WebApplicationBuilder builder)
    {
        public void ConfigureBuilder()
        {
            builder.RegisterOpenApi();
            builder.RegisterAuthentication();
            builder.RegisterCors();
            builder.RegisterRateLimiting();
            builder.RegisterHealthChecks();
            builder.RegisterProblemDetails();
            builder.RegisterLogging();
            builder.RegisterDatabase();
            builder.RegisterValidation();
            builder.RegisterServices();
        }

        public void RegisterCors()
        {
            string[] allowedOrigins = builder
                .Configuration
                .GetSection("Cors:AllowedOrigins")
                .Get<string[]>() ?? [];

            builder.Services.AddCors(options => { ... });
        }

        public void RegisterHealthChecks()
        {
            builder.Services
                .AddHealthChecks()
                .AddCheck("live", () => HealthCheckResult.Healthy(), tags: ["live"]);
                // Add dependency checks tagged "ready" for readiness probe
        }

        public void RegisterProblemDetails() => builder.Services.AddProblemDetails();
        public void RegisterValidation() => builder.Services.AddValidation();

#pragma warning disable IDE0022
        public void RegisterServices()
        {
            builder.Services.AddSingleton<IItemService, ItemService>();
        }
#pragma warning restore IDE0022
    }
}
```

### AppConfiguration.cs

Extension block on `WebApplication` — middleware pipeline and endpoint mapping:

```csharp
public static class AppConfigurationExtensions
{
    extension(WebApplication app)
    {
        public void ConfigureApp()
        {
            // Must be first — processes X-Forwarded-For / X-Forwarded-Proto
            app.UseForwardedHeaders();
            app.UseMiddleware<ExceptionMiddleware>();
            app.UseSerilogRequestLogging();

            if (app.Environment.IsDevelopment())
                app.UseCors("AllowLocalAngularDevelopment");

            app.UseAuthentication();
            app.UseAuthorization();
            app.UseRateLimiter();
            app.MapOpenApi();

            app.MapHealthChecks("/health");
            app.MapHealthChecks("/health/live", new HealthCheckOptions
            {
                Predicate = check => check.Tags.Contains("live")
            });
            app.MapHealthChecks("/health/ready", new HealthCheckOptions
            {
                Predicate = check => check.Tags.Contains("ready")
            });

            app.ConfigureHttpRoutes();
        }
    }
}
```

### HttpRoutes.cs + Endpoints

`HttpRoutes.cs` creates the `api` root group and delegates to feature endpoint mappers:

```csharp
extension(WebApplication app)
{
    public void ConfigureHttpRoutes()
    {
        RouteGroupBuilder root = app.MapGroup("api");
        app.MapItemEndpoints(root);
    }
}
```

`ItemEndpoints.cs` extends `WebApplication`, receives the root group:

```csharp
extension(WebApplication app)
{
    public void MapItemEndpoints(RouteGroupBuilder root)
    {
        RouteGroupBuilder group = root
            .MapGroup("/items")
            .WithTags("Items");

        group.MapGet("/", GetAllItems).WithName("ListItems")...;
        group.MapGet("/{id:int}", GetItemById).WithName("GetItem")...;
        group.MapPost("/", CreateItem).WithName("CreateItem")...;
        group.MapPut("/{id:int}", UpdateItem).WithName("UpdateItem")...;
        group.MapDelete("/{id:int}", DeleteItem).WithName("DeleteItem")...;
    }
}

// Handlers are private static methods — TypedResults infers OpenAPI responses automatically
// Do NOT add .Produces<T>() — TypedResults + Results<T1,T2> return types handle it
private static async Task<Ok<IEnumerable<ItemDto>>> GetAllItems(IItemService service)
{
    IEnumerable<ItemDto> items = await service.GetAllAsync();
    return TypedResults.Ok(items);
}
```

### Serilog (appsettings.json)

Configured entirely via `appsettings.json` — no code changes needed to add sinks:

```json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": { "path": "logs/api-.log", "rollingInterval": "Day", "retainedFileCountLimit": 10 }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}
```

Additional sinks (Seq, App Insights, Blob Storage, Cosmos DB, Elasticsearch, Loki, Datadog, SQL
Server, MongoDB) are documented as comments in `RegisterLogging()` inside `BuilderConfiguration.cs`.

### Testing

Two test projects:

- **`MyMinimalWebApp.Api.IntegrationTests`** — `WebApplicationFactory<Program>`, tests HTTP
  endpoints and middleware
- **`MyMinimalWebApp.Api.UnitTests`** — tests service layer directly with Bogus for test data,
  NSubstitute for mocks

`public partial class Program {}` at the bottom of `Program.cs` is required for
`WebApplicationFactory<Program>`.

```csharp
public class ItemEndpointsTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetAllItems_ReturnsOk()
    {
        HttpResponseMessage response = await factory.CreateClient().GetAsync("/api/items");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}
```

