Introduction
Minimal APIs in .NET 8 provide a simplified way to build HTTP APIs with minimal dependencies and ceremony. This article explores how to leverage Minimal APIs for building high-performance web services.
Getting Started
Basic Setup
Create a new Minimal API project:
dotnet new webapi -minimal -n MinimalApiDemo
cd MinimalApiDemo
Simple Endpoint
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/hello", () => "Hello, World!");
app.Run();
Organizing Endpoints
Using Extension Methods
Keep your Program.cs clean by organizing endpoints:
public static class ProductEndpoints
{
public static void MapProductEndpoints(this WebApplication app)
{
var products = app.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
products.MapGet("/", GetAllProducts);
products.MapGet("/{id}", GetProductById);
products.MapPost("/", CreateProduct);
products.MapPut("/{id}", UpdateProduct);
products.MapDelete("/{id}", DeleteProduct);
}
private static async Task<IResult> GetAllProducts(
IProductRepository repository,
int? page = 1,
int? pageSize = 10)
{
var products = await repository.GetAllAsync(page.Value, pageSize.Value);
return Results.Ok(products);
}
private static async Task<IResult> GetProductById(
int id,
IProductRepository repository)
{
var product = await repository.GetByIdAsync(id);
return product is not null
? Results.Ok(product)
: Results.NotFound();
}
private static async Task<IResult> CreateProduct(
CreateProductRequest request,
IProductRepository repository,
IValidator<CreateProductRequest> validator)
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var product = new Product
{
Name = request.Name,
Price = request.Price,
Description = request.Description
};
var id = await repository.CreateAsync(product);
return Results.Created($"/api/products/{id}", new { id });
}
}
Request Validation
Using FluentValidation
public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.Price)
.GreaterThan(0)
.LessThanOrEqualTo(1000000);
RuleFor(x => x.Description)
.MaximumLength(500);
}
}
// Registration
builder.Services.AddScoped<IValidator<CreateProductRequest>, CreateProductRequestValidator>();
Custom Validation Filter
public class ValidationFilter<T> : IEndpointFilter where T : class
{
private readonly IValidator<T> _validator;
public ValidationFilter(IValidator<T> validator)
{
_validator = validator;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterContext context,
EndpointFilterDelegate next)
{
var argument = context.Arguments
.SingleOrDefault(x => x?.GetType() == typeof(T));
if (argument is T typedArgument)
{
var validationResult = await _validator.ValidateAsync(typedArgument);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
}
return await next(context);
}
}
// Usage
app.MapPost("/api/products", CreateProduct)
.AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
Authentication and Authorization
JWT Authentication
// Configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer,
ValidAudience = builder.Configuration["Jwt:Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key))
};
});
builder.Services.AddAuthorization();
// Protected endpoints
app.MapGet("/api/secure/data", () => "Secret data!")
.RequireAuthorization();
app.MapPost("/api/admin/action", () => "Admin action executed!")
.RequireAuthorization("AdminOnly");
Custom Authorization Policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// Custom requirement and handler
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");
if (dateOfBirthClaim is null)
{
return Task.CompletedTask;
}
var dateOfBirth = DateTime.Parse(dateOfBirthClaim.Value);
var age = DateTime.Today.Year - dateOfBirth.Year;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Performance Optimization
Response Caching
builder.Services.AddResponseCaching();
app.UseResponseCaching();
app.MapGet("/api/cached-data", () => new { timestamp = DateTime.UtcNow })
.CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));
Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", config =>
{
config.PermitLimit = 10;
config.Window = TimeSpan.FromMinutes(1);
config.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
config.QueueLimit = 5;
});
options.AddSlidingWindowLimiter("sliding", config =>
{
config.PermitLimit = 100;
config.Window = TimeSpan.FromMinutes(1);
config.SegmentsPerWindow = 4;
config.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
config.QueueLimit = 10;
});
});
app.UseRateLimiter();
app.MapGet("/api/limited", () => "Rate limited endpoint")
.RequireRateLimiting("fixed");
Error Handling
Global Exception Handler
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature != null)
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exceptionHandlerFeature.Error
- Unhandled exception occurred");
await context.Response.WriteAsJsonAsync(new
{
StatusCode = context.Response.StatusCode,
Message = "An error occurred while processing your request."
});
}
});
});
Problem Details
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions.Add("traceId", Activity.Current?.Id ?? context.HttpContext.TraceIdentifier);
context.ProblemDetails.Extensions.Add("timestamp", DateTimeOffset.UtcNow);
};
});
Testing Minimal APIs
Integration Testing
public class ProductApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public ProductApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsSuccessStatusCode()
{
// Act
var response = await _client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(content);
}
[Fact]
public async Task CreateProduct_WithValidData_ReturnsCreated()
{
// Arrange
var product = new CreateProductRequest
{
Name = "Test Product",
Price = 99.99m,
Description = "Test Description"
};
// Act
var response = await _client.PostAsJsonAsync("/api/products", product);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Contains("/api/products/", response.Headers.Location?.ToString());
}
}
Conclusion
Minimal APIs in .NET 8 provide a powerful and performant way to build web APIs with less ceremony and boilerplate. By following these patterns and best practices, you can create maintainable, scalable, and high-performance APIs that meet modern application requirements.
Comments