ASP.NET Core  

Minimal APIs Revolution: ASP.NET Core Rapid API Development Guide & Examples : Part 9 of 40

api

Module Sequence: Part 8 of 40 - Intermediate Level Core Development Series

Table of Contents

  1. Introduction to Minimal APIs

  2. Why Minimal APIs?

  3. Setting Up Your First Minimal API

  4. Basic CRUD Operations

  5. Real-World E-Commerce Example

  6. Advanced Routing Techniques

  7. Input Validation and Model Binding

  8. Exception Handling Strategies

  9. Authentication and Authorization

  10. Performance Optimization

  11. Testing Minimal APIs

  12. Best Practices and Patterns

  13. Migration from Traditional Controllers

  14. Real-World Case Studies

  15. 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

  • 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.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();