Minimal APIs Revolution: ASP.NET Core's Game-Changer for Rapid Development
Table of Contents
1. Introduction to Minimal APIs {#introduction}
Minimal APIs represent a paradigm shift in how we build APIs with ASP.NET Core. They provide a lightweight, high-performance alternative to traditional controller-based APIs while maintaining the full power of the ASP.NET Core ecosystem.
What are Minimal APIs?
Minimal APIs are a simplified approach to building HTTP APIs with minimal ceremony and boilerplate code. Introduced in .NET 6, they allow developers to create fully functional APIs with just a few lines of code.
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.MapGet("/api/users", () => new { Name = "John", Age = 30 }); app.Run();
2. Why Minimal APIs? {#why-minimal-apis}
Advantages
Reduced Boilerplate: 70% less code compared to traditional controllers
Improved Performance: Faster startup time and reduced memory footprint
Simplified Learning Curve: Easier for beginners to understand
Flexibility: Mix and match with traditional controllers
When to Use Minimal APIs
Microservices
Prototyping and proof of concepts
Simple CRUD APIs
Serverless functions
Internal tools and utilities
3. Setting Up Your First Minimal API {#setup}
Let's create a complete Minimal API project from scratch.
Project Structure
MinimalApiDemo/
├── Program.cs
├── Models/
│ ├── Product.cs
│ └── User.cs
├── Services/
│ └── IProductService.cs
├── Data/
│ └── MockData.cs
└── Properties/
└── launchSettings.jsonProgram.cs - Complete Setup
using MinimalApiDemo.Models; using MinimalApiDemo.Services; using MinimalApiDemo.Data; var builder = WebApplication.CreateBuilder(args); // Add services to the container builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IProductService, ProductService>(); var app = builder.Build(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Basic health check endpoint app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow })); app.Run();
4. Basic CRUD Operations {#basic-crud}
Complete Product Management API
Models/Product.cs
namespace MinimalApiDemo.Models { public class Product { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public int Stock { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } } public class CreateProductRequest { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public int Stock { get; set; } } public class UpdateProductRequest { public string? Name { get; set; } public string? Description { get; set; } public decimal? Price { get; set; } public int? Stock { get; set; } } }
Services/IProductService.cs
using MinimalApiDemo.Models; namespace MinimalApiDemo.Services { public interface IProductService { IEnumerable<Product> GetAllProducts(); Product? GetProductById(int id); Product CreateProduct(CreateProductRequest request); Product? UpdateProduct(int id, UpdateProductRequest request); bool DeleteProduct(int id); } public class ProductService : IProductService { private readonly List<Product> _products = new() { new Product { Id = 1, Name = "Laptop", Description = "High-performance laptop", Price = 999.99m, Stock = 10 }, new Product { Id = 2, Name = "Mouse", Description = "Wireless mouse", Price = 29.99m, Stock = 50 }, new Product { Id = 3, Name = "Keyboard", Description = "Mechanical keyboard", Price = 79.99m, Stock = 25 } }; public IEnumerable<Product> GetAllProducts() => _products; public Product? GetProductById(int id) => _products.FirstOrDefault(p => p.Id == id); public Product CreateProduct(CreateProductRequest request) { var product = new Product { Id = _products.Count + 1, Name = request.Name, Description = request.Description, Price = request.Price, Stock = request.Stock }; _products.Add(product); return product; } public Product? UpdateProduct(int id, UpdateProductRequest request) { var product = _products.FirstOrDefault(p => p.Id == id); if (product == null) return null; product.Name = request.Name ?? product.Name; product.Description = request.Description ?? product.Description; product.Price = request.Price ?? product.Price; product.Stock = request.Stock ?? product.Stock; product.UpdatedAt = DateTime.UtcNow; return product; } public bool DeleteProduct(int id) { var product = _products.FirstOrDefault(p => p.Id == id); if (product == null) return false; return _products.Remove(product); } } }
Complete CRUD Endpoints in Program.cs
using MinimalApiDemo.Models; using MinimalApiDemo.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IProductService, ProductService>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Product endpoints app.MapGet("/api/products", (IProductService productService) => { var products = productService.GetAllProducts(); return Results.Ok(products); }) .WithName("GetProducts") .WithOpenApi(); app.MapGet("/api/products/{id}", (int id, IProductService productService) => { var product = productService.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); }) .WithName("GetProduct") .WithOpenApi(); app.MapPost("/api/products", (CreateProductRequest request, IProductService productService) => { if (string.IsNullOrWhiteSpace(request.Name)) return Results.BadRequest("Product name is required"); if (request.Price <= 0) return Results.BadRequest("Price must be greater than 0"); var product = productService.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); }) .WithName("CreateProduct") .WithOpenApi(); app.MapPut("/api/products/{id}", (int id, UpdateProductRequest request, IProductService productService) => { var updatedProduct = productService.UpdateProduct(id, request); return updatedProduct != null ? Results.Ok(updatedProduct) : Results.NotFound(); }) .WithName("UpdateProduct") .WithOpenApi(); app.MapDelete("/api/products/{id}", (int id, IProductService productService) => { var result = productService.DeleteProduct(id); return result ? Results.NoContent() : Results.NotFound(); }) .WithName("DeleteProduct") .WithOpenApi(); app.Run();
5. Real-World E-Commerce Example {#ecommerce-example}
Let's build a comprehensive e-commerce API with multiple entities and relationships.
Extended Models
Models/Order.cs
namespace MinimalApiDemo.Models { public class Order { public int Id { get; set; } public int UserId { get; set; } public List<OrderItem> Items { get; set; } = new(); public decimal TotalAmount { get; set; } public OrderStatus Status { get; set; } = OrderStatus.Pending; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } } public class OrderItem { public int ProductId { get; set; } public string ProductName { get; set; } = string.Empty; public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal TotalPrice => Quantity * UnitPrice; } public class CreateOrderRequest { public int UserId { get; set; } public List<CreateOrderItem> Items { get; set; } = new(); } public class CreateOrderItem { public int ProductId { get; set; } public int Quantity { get; set; } } public enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled } }
Services/IOrderService.cs
using MinimalApiDemo.Models; namespace MinimalApiDemo.Services { public interface IOrderService { Order? CreateOrder(CreateOrderRequest request); Order? GetOrder(int id); IEnumerable<Order> GetUserOrders(int userId); Order? UpdateOrderStatus(int orderId, OrderStatus status); } public class OrderService : IOrderService { private readonly List<Order> _orders = new(); private readonly IProductService _productService; private int _nextOrderId = 1; public OrderService(IProductService productService) { _productService = productService; } public Order? CreateOrder(CreateOrderRequest request) { var orderItems = new List<OrderItem>(); decimal totalAmount = 0; foreach (var item in request.Items) { var product = _productService.GetProductById(item.ProductId); if (product == null || product.Stock < item.Quantity) return null; var orderItem = new OrderItem { ProductId = product.Id, ProductName = product.Name, Quantity = item.Quantity, UnitPrice = product.Price }; orderItems.Add(orderItem); totalAmount += orderItem.TotalPrice; } var order = new Order { Id = _nextOrderId++, UserId = request.UserId, Items = orderItems, TotalAmount = totalAmount, Status = OrderStatus.Pending }; _orders.Add(order); return order; } public Order? GetOrder(int id) => _orders.FirstOrDefault(o => o.Id == id); public IEnumerable<Order> GetUserOrders(int userId) => _orders.Where(o => o.UserId == userId); public Order? UpdateOrderStatus(int orderId, OrderStatus status) { var order = _orders.FirstOrDefault(o => o.Id == orderId); if (order == null) return null; order.Status = status; order.UpdatedAt = DateTime.UtcNow; return order; } } }
Complete E-Commerce API Endpoints
using MinimalApiDemo.Models; using MinimalApiDemo.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IOrderService, OrderService>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Product endpoints (from previous example) app.MapGet("/api/products", (IProductService productService) => Results.Ok(productService.GetAllProducts())); app.MapGet("/api/products/{id}", (int id, IProductService productService) => { var product = productService.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); }); app.MapPost("/api/products", (CreateProductRequest request, IProductService productService) => { // Validation if (string.IsNullOrWhiteSpace(request.Name)) return Results.BadRequest("Product name is required"); var product = productService.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); }); // Order endpoints app.MapPost("/api/orders", (CreateOrderRequest request, IOrderService orderService) => { var order = orderService.CreateOrder(request); return order != null ? Results.Created($"/api/orders/{order.Id}", order) : Results.BadRequest("Invalid order data or insufficient stock"); }) .WithName("CreateOrder") .WithOpenApi(); app.MapGet("/api/orders/{id}", (int id, IOrderService orderService) => { var order = orderService.GetOrder(id); return order != null ? Results.Ok(order) : Results.NotFound(); }) .WithName("GetOrder") .WithOpenApi(); app.MapGet("/api/users/{userId}/orders", (int userId, IOrderService orderService) => { var orders = orderService.GetUserOrders(userId); return Results.Ok(orders); }) .WithName("GetUserOrders") .WithOpenApi(); app.MapPut("/api/orders/{id}/status", (int id, OrderStatus status, IOrderService orderService) => { var order = orderService.UpdateOrderStatus(id, status); return order != null ? Results.Ok(order) : Results.NotFound(); }) .WithName("UpdateOrderStatus") .WithOpenApi(); app.Run();
6. Advanced Routing Techniques {#advanced-routing}
Route Groups and Organization
using MinimalApiDemo.Models; using MinimalApiDemo.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IOrderService, OrderService>(); var app = builder.Build(); // Route groups for better organization var productsGroup = app.MapGroup("/api/products"); var ordersGroup = app.MapGroup("/api/orders"); var adminGroup = app.MapGroup("/admin").RequireAuthorization(); // Product routes group productsGroup.MapGet("/", (IProductService service) => service.GetAllProducts()) .WithName("GetAllProducts") .WithOpenApi(); productsGroup.MapGet("/{id:int}", (int id, IProductService service) => { var product = service.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); }) .WithName("GetProductById") .WithOpenApi(); productsGroup.MapPost("/", (CreateProductRequest request, IProductService service) => { var product = service.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); }) .WithName("CreateProduct") .WithOpenApi(); // Order routes group ordersGroup.MapGet("/{id:int}", (int id, IOrderService service) => { var order = service.GetOrder(id); return order != null ? Results.Ok(order) : Results.NotFound(); }) .WithName("GetOrderById") .WithOpenApi(); ordersGroup.MapPost("/", (CreateOrderRequest request, IOrderService service) => { var order = service.CreateOrder(request); return order != null ? Results.Created($"/api/orders/{order.Id}", order) : Results.BadRequest("Order creation failed"); }) .WithName("CreateOrder") .WithOpenApi(); // Admin routes (protected) adminGroup.MapGet("/dashboard", () => "Admin Dashboard") .WithName("AdminDashboard") .WithOpenApi(); app.Run();
Custom Route Constraints
// Custom route constraint for product categories app.MapGet("/api/products/category/{category:regex(^[a-zA-Z]+$)}", (string category, IProductService service) => { // Implementation for category-based filtering var products = service.GetAllProducts(); return Results.Ok(products); }) .WithName("GetProductsByCategory") .WithOpenApi();
7. Input Validation and Model Binding {#validation-binding}
Advanced Validation with FluentValidation
Validators/CreateProductRequestValidator.cs
using FluentValidation; using MinimalApiDemo.Models; namespace MinimalApiDemo.Validators { public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest> { public CreateProductRequestValidator() { RuleFor(x => x.Name) .NotEmpty().WithMessage("Product name is required") .Length(3, 100).WithMessage("Product name must be between 3 and 100 characters"); RuleFor(x => x.Description) .MaximumLength(500).WithMessage("Description cannot exceed 500 characters"); RuleFor(x => x.Price) .GreaterThan(0).WithMessage("Price must be greater than 0") .LessThan(1000000).WithMessage("Price must be less than 1,000,000"); RuleFor(x => x.Stock) .GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative"); } } public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest> { public CreateOrderRequestValidator() { RuleFor(x => x.UserId) .GreaterThan(0).WithMessage("User ID must be valid"); RuleFor(x => x.Items) .NotEmpty().WithMessage("Order must contain at least one item"); RuleForEach(x => x.Items).SetValidator(new CreateOrderItemValidator()); } } public class CreateOrderItemValidator : AbstractValidator<CreateOrderItem> { public CreateOrderItemValidator() { RuleFor(x => x.ProductId) .GreaterThan(0).WithMessage("Product ID must be valid"); RuleFor(x => x.Quantity) .GreaterThan(0).WithMessage("Quantity must be at least 1") .LessThan(1000).WithMessage("Quantity cannot exceed 1000"); } } }
Validation Endpoint Filter
using FluentValidation; using MinimalApiDemo.Models; using MinimalApiDemo.Validators; var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IValidator<CreateProductRequest>, CreateProductRequestValidator>(); builder.Services.AddScoped<IValidator<CreateOrderRequest>, CreateOrderRequestValidator>(); var app = builder.Build(); // Validation endpoint filter app.MapPost("/api/products", async ( CreateProductRequest request, IProductService service, IValidator<CreateProductRequest> validator) => { var validationResult = await validator.ValidateAsync(request); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } var product = service.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); }) .WithName("CreateProductWithValidation") .WithOpenApi(); app.MapPost("/api/orders", async ( CreateOrderRequest request, IOrderService service, IValidator<CreateOrderRequest> validator) => { var validationResult = await validator.ValidateAsync(request); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } var order = service.CreateOrder(request); return order != null ? Results.Created($"/api/orders/{order.Id}", order) : Results.BadRequest("Order creation failed"); }) .WithName("CreateOrderWithValidation") .WithOpenApi(); app.Run();
8. Exception Handling Strategies {#exception-handling}
Global Exception Handling Middleware
using System.Net; using MinimalApiDemo.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); var app = builder.Build(); // Global exception handling middleware app.Use(async (context, next) => { try { await next(); } catch (ArgumentException ex) { context.Response.StatusCode = (int)HttpStatusCode.BadRequest; await context.Response.WriteAsJsonAsync(new { error = ex.Message }); } catch (KeyNotFoundException ex) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsJsonAsync(new { error = ex.Message }); } catch (Exception ex) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred" }); // Log the exception (in real application, use proper logging) Console.WriteLine($"Unhandled exception: {ex}"); } }); // Custom exception types public class ProductNotFoundException : Exception { public ProductNotFoundException(int productId) : base($"Product with ID {productId} was not found") { } } public class InsufficientStockException : Exception { public InsufficientStockException(string productName, int requested, int available) : base($"Insufficient stock for {productName}. Requested: {requested}, Available: {available}") { } } // Enhanced product service with exception handling public class EnhancedProductService : IProductService { private readonly List<Product> _products = new() { new Product { Id = 1, Name = "Laptop", Price = 999.99m, Stock = 10 } }; public Product? GetProductById(int id) { var product = _products.FirstOrDefault(p => p.Id == id); if (product == null) throw new ProductNotFoundException(id); return product; } public Product CreateProduct(CreateProductRequest request) { if (_products.Any(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase))) throw new ArgumentException($"Product with name '{request.Name}' already exists"); var product = new Product { Id = _products.Count + 1, Name = request.Name, Description = request.Description, Price = request.Price, Stock = request.Stock }; _products.Add(product); return product; } } // Exception handling in endpoints app.MapGet("/api/products/{id}", (int id, IProductService service) => { try { var product = service.GetProductById(id); return Results.Ok(product); } catch (ProductNotFoundException ex) { return Results.NotFound(new { error = ex.Message }); } }); app.MapPost("/api/products", (CreateProductRequest request, IProductService service) => { try { var product = service.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); } catch (ArgumentException ex) { return Results.BadRequest(new { error = ex.Message }); } }); app.Run();
9. Authentication and Authorization {#authentication}
JWT Authentication Setup
Models/User.cs
namespace MinimalApiDemo.Models { public class User { public int Id { get; set; } public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string PasswordHash { get; set; } = string.Empty; public string Role { get; set; } = "User"; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } public class LoginRequest { public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } public class AuthResponse { public string Token { get; set; } = string.Empty; public DateTime Expires { get; set; } public User User { get; set; } = new(); } public class RegisterRequest { public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } }
Services/IAuthService.cs
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; using MinimalApiDemo.Models; namespace MinimalApiDemo.Services { public interface IAuthService { AuthResponse? Authenticate(LoginRequest request); User? Register(RegisterRequest request); User? GetUserById(int id); } public class AuthService : IAuthService { private readonly List<User> _users = new(); private readonly string _jwtSecret; private readonly int _jwtExpiryMinutes; public AuthService(IConfiguration configuration) { _jwtSecret = configuration["Jwt:Secret"] ?? "default-secret-key-min-32-chars"; _jwtExpiryMinutes = int.Parse(configuration["Jwt:ExpiryMinutes"] ?? "60"); // Add default admin user _users.Add(new User { Id = 1, Username = "admin", Email = "admin@example.com", PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin123"), Role = "Admin" }); } public AuthResponse? Authenticate(LoginRequest request) { var user = _users.FirstOrDefault(u => u.Username == request.Username || u.Email == request.Username); if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) return null; var token = GenerateJwtToken(user); return new AuthResponse { Token = token, Expires = DateTime.UtcNow.AddMinutes(_jwtExpiryMinutes), User = user }; } public User? Register(RegisterRequest request) { if (_users.Any(u => u.Username == request.Username || u.Email == request.Email)) return null; var user = new User { Id = _users.Count + 1, Username = request.Username, Email = request.Email, PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password), Role = "User" }; _users.Add(user); return user; } public User? GetUserById(int id) => _users.FirstOrDefault(u => u.Id == id); private string GenerateJwtToken(User user) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_jwtSecret); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Role, user.Role) }), Expires = DateTime.UtcNow.AddMinutes(_jwtExpiryMinutes), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } } }
Complete Authentication Setup
using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using MinimalApiDemo.Models; using MinimalApiDemo.Services; var builder = WebApplication.CreateBuilder(args); // Configure JWT authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"] ?? "default-secret-key")), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; }); builder.Services.AddAuthorization(); // Add services builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IAuthService, AuthService>(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // Public endpoints app.MapPost("/api/auth/login", (LoginRequest request, IAuthService authService) => { var response = authService.Authenticate(request); return response != null ? Results.Ok(response) : Results.Unauthorized(); }) .WithName("Login") .WithOpenApi(); app.MapPost("/api/auth/register", (RegisterRequest request, IAuthService authService) => { var user = authService.Register(request); return user != null ? Results.Created($"/api/users/{user.Id}", user) : Results.BadRequest("Username or email already exists"); }) .WithName("Register") .WithOpenApi(); // Protected endpoints app.MapGet("/api/profile", (ClaimsPrincipal user, IAuthService authService) => { var userId = int.Parse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0"); var userProfile = authService.GetUserById(userId); return userProfile != null ? Results.Ok(userProfile) : Results.NotFound(); }) .RequireAuthorization() .WithName("GetProfile") .WithOpenApi(); // Admin-only endpoints app.MapGet("/admin/users", (IAuthService authService) => { // In real application, implement user listing return Results.Ok(new { message = "Admin access granted" }); }) .RequireAuthorization(policy => policy.RequireRole("Admin")) .WithName("AdminGetUsers") .WithOpenApi(); app.Run();
10. Performance Optimization {#performance}
Response Caching and Compression
using MinimalApiDemo.Models; using MinimalApiDemo.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddResponseCaching(); builder.Services.AddOutputCache(options => { options.AddBasePolicy(builder => builder.Cache().Expire(TimeSpan.FromMinutes(5))); options.AddPolicy("Products", builder => builder.Cache().Expire(TimeSpan.FromMinutes(2)) .SetVaryByQuery("category", "search")); }); var app = builder.Build(); app.UseResponseCaching(); app.UseOutputCache(); // Cached endpoints app.MapGet("/api/products", (IProductService service) => { var products = service.GetAllProducts(); return Results.Ok(products); }) .CacheOutput("Products") .WithName("GetCachedProducts") .WithOpenApi(); app.MapGet("/api/products/{id}", (int id, IProductService service) => { var product = service.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); }) .CacheOutput() .WithName("GetCachedProduct") .WithOpenApi(); // High-performance endpoint with direct JSON serialization app.MapGet("/api/products/optimized", (IProductService service) => { var products = service.GetAllProducts(); return TypedResults.Json(products, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false // Disable indentation for smaller payload }); }) .CacheOutput("Products") .WithName("GetOptimizedProducts") .WithOpenApi(); app.Run();
Database Connection Optimization
using Dapper; using System.Data; using Npgsql; // or Microsoft.Data.SqlClient for SQL Server public interface IDatabaseService { Task<IEnumerable<Product>> GetProductsAsync(); Task<Product?> GetProductByIdAsync(int id); Task<int> CreateProductAsync(CreateProductRequest request); } public class DatabaseService : IDatabaseService { private readonly string _connectionString; public DatabaseService(IConfiguration configuration) { _connectionString = configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string not found"); } public async Task<IEnumerable<Product>> GetProductsAsync() { using var connection = new NpgsqlConnection(_connectionString); const string sql = "SELECT id, name, description, price, stock FROM products"; return await connection.QueryAsync<Product>(sql); } public async Task<Product?> GetProductByIdAsync(int id) { using var connection = new NpgsqlConnection(_connectionString); const string sql = "SELECT * FROM products WHERE id = @Id"; return await connection.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id }); } public async Task<int> CreateProductAsync(CreateProductRequest request) { using var connection = new NpgsqlConnection(_connectionString); const string sql = @" INSERT INTO products (name, description, price, stock) VALUES (@Name, @Description, @Price, @Stock) RETURNING id"; return await connection.ExecuteScalarAsync<int>(sql, request); } }
11. Testing Minimal APIs {#testing}
Unit Tests with xUnit
Tests/ProductServiceTests.cs
using MinimalApiDemo.Models; using MinimalApiDemo.Services; using Xunit; namespace MinimalApiDemo.Tests { public class ProductServiceTests { private readonly IProductService _productService; public ProductServiceTests() { _productService = new ProductService(); } [Fact] public void GetAllProducts_ReturnsAllProducts() { // Act var products = _productService.GetAllProducts(); // Assert Assert.NotNull(products); Assert.NotEmpty(products); } [Fact] public void GetProductById_WithValidId_ReturnsProduct() { // Arrange var validId = 1; // Act var product = _productService.GetProductById(validId); // Assert Assert.NotNull(product); Assert.Equal(validId, product.Id); } [Fact] public void GetProductById_WithInvalidId_ReturnsNull() { // Arrange var invalidId = 999; // Act var product = _productService.GetProductById(invalidId); // Assert Assert.Null(product); } [Fact] public void CreateProduct_WithValidRequest_CreatesProduct() { // Arrange var request = new CreateProductRequest { Name = "Test Product", Description = "Test Description", Price = 19.99m, Stock = 10 }; // Act var product = _productService.CreateProduct(request); // Assert Assert.NotNull(product); Assert.Equal(request.Name, product.Name); Assert.Equal(request.Price, product.Price); Assert.True(product.Id > 0); } [Theory] [InlineData("", 10.99, 5)] // Empty name [InlineData("Test", -1, 5)] // Negative price [InlineData("Test", 10.99, -1)] // Negative stock public void CreateProduct_WithInvalidData_ThrowsException(string name, decimal price, int stock) { // Arrange var request = new CreateProductRequest { Name = name, Description = "Test", Price = price, Stock = stock }; // Act & Assert Assert.ThrowsAny<Exception>(() => _productService.CreateProduct(request)); } } }
Integration Tests
Tests/ProductsApiIntegrationTests.cs
using System.Net; using System.Net.Http.Json; using Microsoft.AspNetCore.Mvc.Testing; using MinimalApiDemo.Models; using Xunit; namespace MinimalApiDemo.Tests { public class ProductsApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; public ProductsApiIntegrationTests(WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); } [Fact] public async Task GetProducts_ReturnsSuccess() { // Act var response = await _client.GetAsync("/api/products"); // Assert response.EnsureSuccessStatusCode(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task GetProduct_WithValidId_ReturnsProduct() { // Arrange var productId = 1; // Act var response = await _client.GetAsync($"/api/products/{productId}"); // Assert response.EnsureSuccessStatusCode(); var product = await response.Content.ReadFromJsonAsync<Product>(); Assert.NotNull(product); Assert.Equal(productId, product.Id); } [Fact] public async Task CreateProduct_WithValidData_ReturnsCreated() { // Arrange var request = new CreateProductRequest { Name = "Integration Test Product", Description = "Test Description", Price = 29.99m, Stock = 15 }; // Act var response = await _client.PostAsJsonAsync("/api/products", request); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); var product = await response.Content.ReadFromJsonAsync<Product>(); Assert.NotNull(product); Assert.Equal(request.Name, product.Name); } [Fact] public async Task CreateProduct_WithInvalidData_ReturnsBadRequest() { // Arrange var request = new CreateProductRequest { Name = "", // Invalid empty name Description = "Test", Price = -10, // Invalid negative price Stock = 5 }; // Act var response = await _client.PostAsJsonAsync("/api/products", request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } }
12. Best Practices and Patterns {#best-practices}
Service Registration Extension Methods
Extensions/ServiceCollectionExtensions.cs
using MinimalApiDemo.Services; namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services) { services.AddScoped<IProductService, ProductService>(); services.AddScoped<IOrderService, OrderService>(); services.AddScoped<IAuthService, AuthService>(); return services; } public static IServiceCollection AddValidationServices(this IServiceCollection services) { services.AddValidatorsFromAssemblyContaining<Program>(); return services; } } }
Endpoint Registration Extension Methods
Extensions/WebApplicationExtensions.cs
using MinimalApiDemo.Endpoints; namespace Microsoft.AspNetCore.Builder { public static class WebApplicationExtensions { public static WebApplication MapProductEndpoints(this WebApplication app) { var group = app.MapGroup("/api/products"); group.MapGet("/", ProductEndpoints.GetAllProducts); group.MapGet("/{id}", ProductEndpoints.GetProductById); group.MapPost("/", ProductEndpoints.CreateProduct); group.MapPut("/{id}", ProductEndpoints.UpdateProduct); group.MapDelete("/{id}", ProductEndpoints.DeleteProduct); return app; } public static WebApplication MapOrderEndpoints(this WebApplication app) { var group = app.MapGroup("/api/orders"); group.MapGet("/{id}", OrderEndpoints.GetOrderById); group.MapPost("/", OrderEndpoints.CreateOrder); group.MapGet("/user/{userId}", OrderEndpoints.GetUserOrders); return app; } public static WebApplication MapAuthEndpoints(this WebApplication app) { var group = app.MapGroup("/api/auth"); group.MapPost("/login", AuthEndpoints.Login); group.MapPost("/register", AuthEndpoints.Register); group.MapGet("/profile", AuthEndpoints.GetProfile) .RequireAuthorization(); return app; } } }
Organized Endpoint Classes
Endpoints/ProductEndpoints.cs
using MinimalApiDemo.Models; using MinimalApiDemo.Services; namespace MinimalApiDemo.Endpoints { public static class ProductEndpoints { public static async Task<IResult> GetAllProducts(IProductService productService) { var products = productService.GetAllProducts(); return Results.Ok(products); } public static async Task<IResult> GetProductById( int id, IProductService productService) { var product = productService.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); } public static async Task<IResult> CreateProduct( CreateProductRequest request, IProductService productService) { if (string.IsNullOrWhiteSpace(request.Name)) return Results.BadRequest("Product name is required"); var product = productService.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); } public static async Task<IResult> UpdateProduct( int id, UpdateProductRequest request, IProductService productService) { var updatedProduct = productService.UpdateProduct(id, request); return updatedProduct != null ? Results.Ok(updatedProduct) : Results.NotFound(); } public static async Task<IResult> DeleteProduct( int id, IProductService productService) { var result = productService.DeleteProduct(id); return result ? Results.NoContent() : Results.NotFound(); } } }
Final Organized Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using MinimalApiDemo.Extensions; var builder = WebApplication.CreateBuilder(args); // Configuration builder.Services.Configure<JwtSettings>( builder.Configuration.GetSection("Jwt")); // Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"]!)), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true }; }); builder.Services.AddAuthorization(); // Services builder.Services.AddApplicationServices(); builder.Services.AddValidationServices(); // Swagger builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Middleware if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); // Endpoints app.MapProductEndpoints() .MapOrderEndpoints() .MapAuthEndpoints(); // Health check app.MapGet("/health", () => new { status = "Healthy", timestamp = DateTime.UtcNow }); app.Run(); // Make Program class accessible for testing public partial class Program { }
13. Migration from Traditional Controllers {#migration}
Traditional Controller Example
Controllers/ProductsController.cs
[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IProductService _productService; public ProductsController(IProductService productService) { _productService = productService; } [HttpGet] public IActionResult GetProducts() { var products = _productService.GetAllProducts(); return Ok(products); } [HttpGet("{id}")] public IActionResult GetProduct(int id) { var product = _productService.GetProductById(id); if (product == null) return NotFound(); return Ok(product); } [HttpPost] public IActionResult CreateProduct(CreateProductRequest request) { var product = _productService.CreateProduct(request); return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product); } }
Equivalent Minimal API
var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); var app = builder.Build(); app.MapGet("/api/products", (IProductService service) => service.GetAllProducts()); app.MapGet("/api/products/{id}", (int id, IProductService service) => { var product = service.GetProductById(id); return product != null ? Results.Ok(product) : Results.NotFound(); }); app.MapPost("/api/products", (CreateProductRequest request, IProductService service) => { var product = service.CreateProduct(request); return Results.Created($"/api/products/{product.Id}", product); }); app.Run();
14. Real-World Case Studies {#case-studies}
Case Study 1: E-Commerce Microservice
// Complete e-commerce microservice with Minimal APIs var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddScoped<IPaymentService, PaymentService>(); builder.Services.AddScoped<INotificationService, NotificationService>(); var app = builder.Build(); // Product catalog var catalog = app.MapGroup("/api/catalog"); catalog.MapGet("/products", GetProductsWithFilters); catalog.MapGet("/products/{id}", GetProductDetails); catalog.MapGet("/categories", GetCategories); catalog.MapGet("/search", SearchProducts); // Shopping cart var cart = app.MapGroup("/api/cart"); cart.MapGet("/", GetCart); cart.MapPost("/items", AddToCart); cart.MapPut("/items/{productId}", UpdateCartItem); cart.MapDelete("/items/{productId}", RemoveFromCart); // Orders var orders = app.MapGroup("/api/orders"); orders.MapPost("/", CreateOrder); orders.MapGet("/{orderId}", GetOrder); orders.MapPut("/{orderId}/cancel", CancelOrder); // Payments var payments = app.MapGroup("/api/payments"); payments.MapPost("/", ProcessPayment); payments.MapGet("/{paymentId}", GetPaymentStatus); app.Run(); // Endpoint implementations async Task<IResult> GetProductsWithFilters( [FromQuery] string? category, [FromQuery] decimal? minPrice, [FromQuery] decimal? maxPrice, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, IProductService productService) { var products = productService.GetAllProducts(); // Apply filters if (!string.IsNullOrEmpty(category)) products = products.Where(p => p.Category == category); if (minPrice.HasValue) products = products.Where(p => p.Price >= minPrice.Value); if (maxPrice.HasValue) products = products.Where(p => p.Price <= maxPrice.Value); // Pagination var totalCount = products.Count(); var pagedProducts = products .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return Results.Ok(new { Products = pagedProducts, TotalCount = totalCount, Page = page, PageSize = pageSize, TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize) }); }
15. Future of Minimal APIs {#future}
.NET 8 and Beyond Features
// .NET 8 enhanced Minimal APIs with IEndpointRouteBuilder extensions var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // New .NET 8 features app.MapGet("/api/products", HandleGetProducts) .WithSummary("Get all products") .WithDescription("Retrieves a paginated list of products with optional filtering") .WithTags("Products") .Produces<PagedResponse<Product>>(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); app.MapPost("/api/products", HandleCreateProduct) .AddEndpointFilter<ValidationFilter<CreateProductRequest>>() .WithOpenApi(operation => { operation.Summary = "Create a new product"; operation.Description = "Creates a new product in the catalog"; return operation; }); // New anti-forgery protection app.MapPost("/api/orders", HandleCreateOrder) .ValidateAntiForgeryToken(); // Keyed services support app.MapGet("/api/reports/sales", ([FromKeyedServices("sales")] IReportService reportService) => reportService.GenerateReport()); app.Run();

0 Comments
thanks for your comments!