Aspnet Core Apis
ASP.NET Core API development with minimal APIs, controllers, middleware, OpenAPI, and best practices
by thelobbi · MIT
The skill
ASP.NET Core API development with minimal APIs, controllers, middleware, OpenAPI, and best practices
ASP.NET Core API Development
Minimal APIs (.NET 10 Preferred)
Endpoint Groups
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete);
return group;
}
private static async Task<IResult> GetAll(
[AsParameters] ProductQuery query,
IProductService service,
CancellationToken ct) =>
TypedResults.Ok(await service.GetAllAsync(query, ct));
private static async Task<IResult> GetById(int id, IProductService service, CancellationToken ct) =>
await service.GetByIdAsync(id, ct) is { } product
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
Program.cs Registration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddOutputCache();
builder.Services.AddRateLimiter(opts => opts.AddFixedWindowLimiter("api", o =>
{
o.Window = TimeSpan.FromMinutes(1);
o.PermitLimit = 100;
}));
var app = builder.Build();
app.UseOutputCache();
app.UseRateLimiter();
app.MapOpenApi();
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.Run();
Middleware Pipeline
Request → UseExceptionHandler → UseHsts → UseHttpsRedirection
→ UseStaticFiles → UseRouting → UseCors → UseAuthentication
→ UseAuthorization → UseOutputCache → UseRateLimiter → Endpoints
Custom Middleware
public sealed class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try
{
await next(context);
}
finally
{
sw.Stop();
logger.LogInformation("Request {Method} {Path} completed in {Elapsed}ms",
context.Request.Method, context.Request.Path, sw.ElapsedMilliseconds);
}
}
}
// Register: app.UseMiddleware<RequestTimingMiddleware>();
Endpoint Filters
public sealed class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
if (validator is null) return await next(context);
var model = context.GetArgument<T>(0);
var result = await validator.ValidateAsync(model);
return result.IsValid
? await next(context)
: TypedResults.ValidationProblem(result.ToDictionary());
}
}
OpenAPI / Swagger
// .NET 10 built-in OpenAPI
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((doc, ctx, ct) =>
{
doc.Info.Title = "My API";
doc.Info.Version = "v1";
return Task.CompletedTask;
});
});
app.MapOpenApi(); // Serves at /openapi/v1.json
app.MapScalarApiReference(); // Interactive API explorer UI
Error Handling
// Global exception handler with Problem Details
builder.Services.AddProblemDetails();
app.UseExceptionHandler(error =>
{
error.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var problem = exception switch
{
NotFoundException => new ProblemDetails
{
Status = 404, Title = "Not Found", Detail = exception.Message
},
ValidationException ve => new ProblemDetails
{
Status = 400, Title = "Validation Error",
Extensions = { ["errors"] = ve.Errors }
},
_ => new ProblemDetails
{
Status = 500, Title = "Internal Server Error"
}
};
context.Response.StatusCode = problem.Status ?? 500;
await context.Response.WriteAsJsonAsync(problem);
});
});
Rate Limiting (from official docs)
builder.Services.AddRateLimiter(options =>
{
// Fixed Window - simple time-based limit
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 4;
opt.Window = TimeSpan.FromSeconds(12);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 2;
});
// Sliding Window - smoother rate control
options.AddSlidingWindowLimiter("sliding", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromSeconds(30);
opt.SegmentsPerWindow = 3;
});
// Token Bucket - burst-friendly
options.AddTokenBucketLimiter("token", opt =>
{
opt.TokenLimit = 100;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
opt.TokensPerPeriod = 20;
opt.AutoReplenishment = true;
});
// Concurrency - limits concurrent requests, not rate
options.AddConcurrencyLimiter("concurrency", opt =>
{
opt.PermitLimit = 50;
opt.QueueLimit = 10;
});
// Custom rejection response
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
await context.HttpContext.Response.WriteAsync("Rate limit exceeded.", ct);
};
});
// IMPORTANT: UseRouting MUST come before UseRateLimiter for endpoint-specific limiters
app.UseRouting();
app.UseRateLimiter();
// Apply to endpoints
app.MapGet("/api/limited", () => "OK").RequireRateLimiting("fixed");
// Partitioned by API key with tiered limits
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
string apiKey = context.Request.Headers["X-API-Key"].ToString() ?? "default";
return apiKey switch
{
"premium-key" => RateLimitPartition.GetFixedWindowLimiter(apiKey,
_ => new FixedWindowRateLimiterOptions { PermitLimit = 1000, Window = TimeSpan.FromMinutes(1) }),
_ => RateLimitPartition.GetFixedWindowLimiter(apiKey,
_ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1) })
};
});
Dependency Injection Patterns (from official docs)
// Lifetimes
builder.Services.AddTransient<ITransientService, TransientService>(); // New each time
builder.Services.AddScoped<IScopedService, ScopedService>(); // Per request
builder.Services.AddSingleton<ISingletonService, SingletonService>(); // Once
// Factory registration
builder.Services.AddScoped<IMyService>(sp =>
new MyService(sp.GetRequiredService<IDependency>()));
// Keyed services (multiple implementations of same interface)
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");
// Inject keyed service in minimal API
app.MapGet("/data", ([FromKeyedServices("redis")] ICache cache) => cache.Get("key"));
// Extension method pattern for clean DI registration
public static class MyServiceExtensions
{
public static IServiceCollection AddMyServices(this IServiceCollection services, IConfiguration config)
{
services.Configure<MyOptions>(config.GetSection("MyOptions"));
services.AddScoped<IMyService, MyService>();
return services;
}
}
Middleware Pipeline Order (from official docs)
// Correct order for ASP.NET Core 10:
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHttpsRedirection();
app.UseStaticFiles(); // or app.MapStaticAssets()
app.UseRouting(); // MUST come before rate limiter
app.UseRateLimiter();
app.UseCors(); // MUST come before auth and after routing
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery(); // After auth
app.UseResponseCaching(); // After CORS
app.UseResponseCompression();
// Endpoints last
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.MapControllers();
Native AOT Support
// Use CreateSlimBuilder for AOT-compatible apps
var builder = WebApplication.CreateSlimBuilder(args);
// JSON source generator required for AOT
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default));
var app = builder.Build();
app.MapGet("/todos", () => new Todo(1, "Walk dog", false));
app.Run();
[JsonSerializable(typeof(Todo[]))]
[JsonSerializable(typeof(Todo))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
public record Todo(int Id, string Title, bool IsComplete);
<!-- In .csproj for AOT publishing -->
<PublishAot>true</PublishAot>
Options Pattern (from official docs)
// Bind configuration to strongly-typed class
public sealed class SmtpOptions
{
public const string SectionName = "Smtp";
[Required] public string Host { get; set; } = "";
[Range(1, 65535)] public int Port { get; set; } = 587;
public string? Username { get; set; }
}
// Register with validation
builder.Services.AddOptions<SmtpOptions>()
.BindConfiguration(SmtpOptions.SectionName)
.ValidateDataAnnotations() // Validates [Required], [Range], etc.
.ValidateOnStart(); // Fail fast at startup if invalid
// Inject: IOptions<T> (singleton), IOptionsSnapshot<T> (scoped, reloads),
// IOptionsMonitor<T> (singleton, notifies on change)
public sealed class EmailService(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions _smtp = options.Value;
}
Best Practices
- Use
TypedResultsfor compile-time response type checking - Always pass and respect
CancellationToken - Use
[AsParameters]for complex query objects - Group endpoints by feature/resource with
MapGroup() - Use endpoint filters instead of controller action filters
- Prefer
IResultreturn type for all handlers - Add OpenAPI annotations for documentation
- Use output caching for read-heavy endpoints
- UseRouting MUST come before UseRateLimiter
- CORS MUST come before UseResponseCaching
- Use
CreateSlimBuilder+ JSON source generators for Native AOT - Use
ValidateOnStart()with Options pattern to fail fast
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: thelobbi/aspnet-core-apis-22384272402.
Details
- Publisher
- thelobbi
- Category
- Software Development › APIs & backend
- License
- MIT
- Updated
- 2026-10-08
References
More from thelobbi
- Agentic Design Patterns Catalog of 21 agentic AI design patterns (prompt chaining, routing, parallelization, reflection, too
- Atlassianapi Atlassian API integration for Jira and Confluence automation.
- Authentication Authentication and authorization including JWT, OAuth2, OIDC, sessions, RBAC, and security analysis.
- Aws AWS cloud services including EC2, EKS, S3, Lambda, RDS, and IAM.