![api]()
Module Sequence: Part 8 of 40 - Intermediate Level Core Development Series
Table of Contents
Introduction to Minimal APIs
Why Minimal APIs?
Setting Up Your First Minimal API
Basic CRUD Operations
Real-World E-Commerce Example
Advanced Routing Techniques
Input Validation and Model Binding
Exception Handling Strategies
Authentication and Authorization
Performance Optimization
Testing Minimal APIs
Best Practices and Patterns
Migration from Traditional Controllers
Real-World Case Studies
Future of Minimal APIs
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
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.json
Program.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 = "[email protected]",
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();