Introduction
ASP.NET Core makes building high-performance, cross-platform HTTP APIs straightforward. This guide progresses from a minimal endpoint to an enterprise-ready service including layered architecture, validation, authentication/authorization, OpenAPI, versioning, caching, resilience, CI/CD and observability.
You will learn:
- Minimal API vs Controller-based choices
- Routing, DTO mapping, and validation
- Data persistence (EF Core) and migrations
- Authentication & authorization (JWT / Entra ID)
- API versioning & OpenAPI documentation
- Performance (response compression, caching, pagination)
- Resilience (retry, circuit breaker, timeout)
- Automated tests (unit, integration, contract)
- CI/CD pipeline (GitHub Actions)
- Observability (structured logging, tracing, metrics)
Time to Read: 60–90 minutes
Skill Level: Beginner → Intermediate
High-Level Architecture
flowchart LR
Client[Client Apps / Mobile / SPA] --> GW[API Gateway (Optional)]
GW --> API[ASP.NET Core API]
subgraph API
EP[Endpoints]
SVC[Services]
REPO[Repository]
VAL[Validation]
MAP[Mapping]
end
API --> DB[(Database)]
API --> Cache[(Redis Cache)]
API --> Auth[Entra ID / Identity Provider]
API --> Obs[Telemetry: App Insights + OpenTelemetry]
Prerequisites
| Item | Details |
|---|---|
| .NET SDK | 8.x or later |
| Tools | VS Code, Git, Docker (optional) |
| Database | Local SQL Server / SQLite for demo |
| Azure | Subscription (for deployment) |
Install tooling (PowerShell):
winget install Microsoft.DotNet.SDK.8
winget install Git.Git
winget install Microsoft.VisualStudioCode
Project Bootstrap
mkdir Contoso.Api
cd Contoso.Api
dotnet new webapi --use-minimal-apis -n Contoso.Api
Domain & DTOs
public record Product(int Id, string Name, decimal Price);
public record CreateProductRequest(string Name, decimal Price);
Minimal Endpoints
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var products = new List<Product>();
app.MapPost("/products", (CreateProductRequest req) => {
var p = new Product(products.Count + 1, req.Name, req.Price);
products.Add(p);
return Results.Created($"/products/{p.Id}", p);
});
app.MapGet("/products", () => products);
app.Run();
Validation with FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
public class CreateProductValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Price).GreaterThan(0);
}
}
// Registration
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductValidator>();
Persistence (EF Core)
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> opt) : base(opt) {}
public DbSet<Product> Products => Set<Product>();
}
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(builder.Configuration["ConnectionStrings:Db"]));
Migration:
dotnet ef migrations add InitialCreate
dotnet ef database update
Authentication (JWT / Entra ID)
dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.MicrosoftGraph
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd");
app.UseAuthentication();
app.UseAuthorization();
Protected endpoint:
app.MapGet("/profile", [Authorize] (ClaimsPrincipal user) => new { user.Identity?.Name });
Authorization & Policies
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("CanWrite", p => p.RequireClaim("scope", "api.write"));
});
app.MapPost("/secure-products", [Authorize(Policy="CanWrite")] (CreateProductRequest req) => {/*...*/});
OpenAPI & Versioning
dotnet add package Swashbuckle.AspNetCore
dotnet add package Asp.Versioning.Http
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddApiVersioning(o => { o.DefaultApiVersion = new ApiVersion(1,0); o.AssumeDefaultVersionWhenUnspecified=true; });
app.UseSwagger();
app.UseSwaggerUI();
Performance Enhancements
builder.Services.AddResponseCompression();
app.UseResponseCompression();
Caching example:
app.MapGet("/products/{id}", async (int id, AppDbContext db, IMemoryCache cache) => {
if(cache.TryGetValue(id, out Product p)) return p;
p = await db.Products.FindAsync(id);
if(p!=null) cache.Set(id, p, TimeSpan.FromMinutes(5));
return p is null ? Results.NotFound() : Results.Ok(p);
});
Resilience (Polly)
dotnet add package Polly.Extensions.Http
builder.Services.AddHttpClient("crm")
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(new[]{TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(2),TimeSpan.FromSeconds(4)}));
Logging & Telemetry
dotnet add package OpenTelemetry.Extensions.Hosting
builder.Services.AddOpenTelemetry().WithTracing(b => b
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation());
Kusto Query (slow requests):
requests
| where duration > 5000
| summarize count() by operation_Name
Testing
dotnet new xunit -n Contoso.Api.Tests
public class ProductTests
{
[Fact]
public void Price_MustBePositive()
{
var validator = new CreateProductValidator();
var result = validator.Validate(new CreateProductRequest("Test", -5));
Assert.False(result.IsValid);
}
}
CI/CD (GitHub Actions)
name: build-api
on:
push:
branches: [ main ]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore Contoso.Api/Contoso.Api.csproj
- name: Build
run: dotnet build Contoso.Api/Contoso.Api.csproj -c Release --no-restore
- name: Test
run: dotnet test Contoso.Api.Tests/Contoso.Api.Tests.csproj -c Release --no-build --logger trx
- name: Publish
run: dotnet publish Contoso.Api/Contoso.Api.csproj -c Release -o publish
- name: Deploy (WebApp)
uses: azure/webapps-deploy@v3
with:
app-name: contoso-api-prod
package: publish
Troubleshooting Matrix
| Symptom | Cause | Diagnosis | Resolution |
|---|---|---|---|
| 500 errors | Unhandled exceptions | App Insights traces | Add global exception middleware |
| High latency | N+1 DB queries | EF Profiler / logs | Introduce projection + caching |
| 404 versioned route | Missing version mapping | Swagger doc review | Add ApiVersion attribute |
| Auth failures | Invalid token scopes | JWT validation logs | Adjust app registration scopes |
| Memory growth | Large caches | Metrics / dotnet-counters | Evict & size TTLs |
Image References


