On this page

Introduction to ASP.NET Core 10 and Minimal APIs

14 min read TextCh. 4 — ASP.NET Core

What is ASP.NET Core?

ASP.NET Core is .NET's web framework for building APIs, web apps, WebSockets, and microservices. It is open-source, cross-platform, and one of the fastest web frameworks in the world according to TechEmpower benchmarks.

ASP.NET Core 10 brings:

  • Mature Minimal APIs optimized for performance
  • Native AOT improvements for ultra-small binaries
  • OpenAPI 3.1 with automatic documentation generation
  • Native rate limiting
  • Improved WebSockets

Creating a web project

# Create a Minimal API
dotnet new webapi -n CatalogApi -f net10.0 --use-minimal-apis

# Create an API with controllers
dotnet new webapi -n CatalogApi -f net10.0

# Run in development mode
dotnet run --launch-profile https

# Run with hot reload
dotnet watch run

The generated project has this structure:

CatalogApi/
├── Program.cs              ← Entry point + configuration
├── CatalogApi.csproj       ← Project file
├── appsettings.json        ← App configuration
├── appsettings.Development.json ← Dev configuration
└── Properties/
    └── launchSettings.json ← Local run profiles

The ASP.NET Core builder pattern

ASP.NET Core uses the builder pattern to configure the application in two phases:

Phase 1: builder.Services.*  → Register services (DI container)
           ↓
        builder.Build()      → Create the application
           ↓
Phase 2: app.Use*            → Configure middleware
        app.Map*             → Define endpoints
           ↓
        app.Run()            → Start the server
var builder = WebApplication.CreateBuilder(args);

// Phase 1: Services
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddScoped<IProductRepo, ProductRepo>();
builder.Services.AddTransient<IEmailService, SmtpEmailService>();

var app = builder.Build();

// Phase 2: Middleware and endpoints
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", () => "Welcome to the API!");
app.Run();

Service lifetimes

Lifetime Created When to use
Singleton Once per application Cache, configuration, HTTP clients
Scoped Once per HTTP request DbContext, Unit of Work
Transient Every time it is injected Lightweight, stateless services
builder.Services.AddSingleton<IConfiguration, Configuration>();  // shared
builder.Services.AddScoped<IUserService, UserService>();          // per request
builder.Services.AddTransient<IEmailSender, EmailSender>();       // new instance each time

Minimal API endpoints

Minimal APIs define endpoints with lambda functions directly:

// Simple GET — returns plain text
app.MapGet("/", () => "API is online");

// GET with route parameter and query string
app.MapGet("/products/{id:int}", (int id, bool? includeDetail) =>
{
    // id comes from route, includeDetail from query string (?includeDetail=true)
    return Results.Ok(new { id, includeDetail });
});

// POST with JSON body
app.MapPost("/products", (CreateProductDto dto) =>
{
    // ASP.NET deserializes JSON automatically
    Console.WriteLine($"Creating: {dto.Name}");
    return Results.Created("/products/1", dto);
});

// Group endpoints with RouteGroupBuilder
var group = app.MapGroup("/api/v1/products");
group.MapGet("/",     GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/",    Create);

Dependency injection in endpoints

// DI works through parameters in endpoints
app.MapGet("/users", (IUserService userService, ILogger<Program> logger) =>
{
    logger.LogInformation("Querying users");
    return Results.Ok(userService.GetAll());
});

// You can also resolve from the container directly
app.MapGet("/config", (IServiceProvider sp) =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return Results.Ok(new { version = config["App:Version"] });
});

Configuration (appsettings.json)

{
  "App": {
    "Name": "Catalog API",
    "Version": "1.0.0",
    "MaxResults": 50
  },
  "ConnectionStrings": {
    "Default": "Host=localhost;Database=catalog;Username=app;Password=pass"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
// Strongly typed configuration
public class AppSettings
{
    public string Name       { get; set; } = string.Empty;
    public string Version    { get; set; } = string.Empty;
    public int    MaxResults { get; set; } = 20;
}

builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("App"));

// Inject in endpoints
app.MapGet("/info", (IOptions<AppSettings> opts) =>
    Results.Ok(new { opts.Value.Name, opts.Value.Version }));

Environments and environment variables

// Detect the current environment
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseDeveloperExceptionPage();
}
else if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

// Environment variables override appsettings.json
// ASPNETCORE_ENVIRONMENT=Production
// ConnectionStrings__Default=...

Practice

  1. Complete API: Create a Minimal API with full CRUD endpoints for a Task(int Id, string Title, bool Completed) entity using an in-memory service.
  2. Configuration: Add an "Api": { "MaxItems": 10 } section to appsettings.json and use it in the GET /tasks endpoint to limit results.
  3. Swagger: Ensure your API has Swagger enabled in development. Use .WithName() and .WithSummary() to document the endpoints.

In the next lesson we will learn how to organize larger APIs using controllers, routing attributes, and validation with DataAnnotations.

Minimal APIs for small projects and microservices
Minimal APIs are perfect for microservices, single-purpose APIs, and projects that prioritize simplicity. For large APIs with many endpoints, consider using Controllers (ApiController) which offer better organization through inheritance and filters.
Results helpers
ASP.NET Core 10 includes the static Results type with helpers for the most common responses: Results.Ok(), Results.Created(), Results.NotFound(), Results.BadRequest(), Results.NoContent(), Results.Unauthorized(), Results.Problem(). Always use these helpers instead of returning data directly.
Register services in the correct order
Services are registered BEFORE calling builder.Build(). Middleware is configured AFTER Build(). If you register a service after Build(), you will get a runtime exception. The separation between service configuration and middleware is fundamental in ASP.NET Core.
// Program.cs — ASP.NET Core 10 complete Minimal API
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

// ── Register services (Dependency Injection) ─────────
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IProductService, ProductService>();

var app = builder.Build();

// ── Configure middleware pipeline ─────────────────────
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// ── Define endpoints ──────────────────────────────────
app.MapGet("/products", (IProductService svc) =>
    Results.Ok(svc.GetAll()))
    .WithName("GetProducts")
    .WithOpenApi();

app.MapGet("/products/{id:int}", (int id, IProductService svc) =>
{
    var product = svc.GetById(id);
    return product is null
        ? Results.NotFound(new { message = $"Product {id} not found" })
        : Results.Ok(product);
});

app.MapPost("/products", (CreateProductDto dto, IProductService svc) =>
{
    var product = svc.Create(dto);
    return Results.Created($"/products/{product.Id}", product);
});

app.MapPut("/products/{id:int}", (int id, UpdateProductDto dto, IProductService svc) =>
{
    var updated = svc.Update(id, dto);
    return updated is null ? Results.NotFound() : Results.Ok(updated);
});

app.MapDelete("/products/{id:int}", (int id, IProductService svc) =>
    svc.Delete(id) ? Results.NoContent() : Results.NotFound());

app.Run();