Getting Started with ASP.NET Core Web APIs: RESTful Services Made Easy

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

ASP.NET Core Architecture
OpenAPI Diagram
Polly Resilience Flow

References