Quick Start
- Ask for the solution name and project name (see Scaffolding)
- Copy
template/into the output directory - Rename all src projects, all test projects, and all namespace references from
MyMinimalWebApp.Api→<ProjectName>(see Scaffolding) - Replace
Item/Itemswith your domain entity name - Run
dotnet testto 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:
Solution name — what should the solution be called? (required)
Project name — what should the API project be called? (required; conventionally
<SolutionName>.Api)Output directory — where should the project be created? (defaults to the current working directory if the user does not specify one)
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))"ornode -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."
- Windows:
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.slnxin 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.csfiles - Update all project references in the solution file
- Update the
InternalsVisibleToinsrc/<ProjectName>/<ProjectName>.csprojfromMyMinimalWebApp.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/Itemswith the appropriate domain entity name if provided - Replace the HTTP port
5262with<HttpPort>and the HTTPS port7105with<HttpPort + 1>in: (Note: the template's ports5262/7105are default .NET placeholders and do not follow the+1convention — the convention only applies to generated ports for new projects.)src/<ProjectName>/Properties/launchSettings.json(bothhttpandhttpsprofiles)src/<ProjectName>/appsettings.json(bothKestrel.Endpoints.Http.UrlandKestrel.Endpoints.Https.Url)http-files/items.httphttp-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
usingdirectives to individual C# files. Always add them toGlobalUsings.cs. - Use
varfor all local variables — enforced bycsharp_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
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:
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:
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:
extension(WebApplication app)
{
public void ConfigureHttpRoutes()
{
RouteGroupBuilder root = app.MapGroup("api");
app.MapItemEndpoints(root);
}
}
ItemEndpoints.cs extends WebApplication, receives the root group:
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:
{
"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 middlewareMyMinimalWebApp.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>.
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);
}
}