Here’s a detailed summary of the StarterTemplate Architecture Documentation, crafted specifically for Reddit, along with examples for better understanding:
StarterTemplate: A Feature-Based Vertical Slice Architecture for .NET APIs
The StarterTemplate repository is designed to simplify scalable .NET API development by adopting a Feature-Based Vertical Slice Architecture. This approach emphasizes organizing code by business features and ensuring separation of concerns.
Feature-Based Organization:
Code is grouped by business features like Users, Roles, and Authentication.
Example structure:
Api/
├── Features/ # Business features (e.g., Users, Roles)
├── Infrastructure/ # Shared utilities and cross-cutting concerns
├── Shared/ # Common helpers and utilities
├── Configuration/ # Startup and configuration logic
└── Seeder/ # Database seeding system
Vertical Slices:
Automatic Registration:
Minimal API with CQRS-like Pattern:
Decorators for Validation and Caching:
Here are some practical examples to demonstrate the architecture:
Folder Structure:
Api/Features/Products/
├── ProductFeature.cs # Main feature class
├── Models/ # Entity models
├── Dto/ # Data transfer objects
├── Slices/ # Handlers for operations
├── Validators/ # Custom validation logic
├── SeedData/ # JSON seed data files
Entity Model Example:
public class Product : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public ProductStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
Slice Handler for Creating Products:
public class CreateProduct : SliceHandler<CreateProductDto, IResult, CreateProduct>
{
private readonly StarterTemplateContext _context;
public CreateProduct(StarterTemplateContext context) => _context = context;
public override async Task<IResult> HandleAsync(CreateProductDto request, CancellationToken cancellationToken = default)
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price,
Status = request.Status,
CreatedAt = DateTime.UtcNow
};
_context.Set<Product>().Add(product);
await _context.SaveChangesAsync(cancellationToken);
return Results.Created($"/api/products/{product.Id}", product.Id);
}
}
Mapping Endpoints:
public sealed class ProductEndpoints : BaseEndpoint
{
public override string BaseUrl => "products";
public override string Tag => "Products";
public override void DefineEndpoints(IEndpointRouteBuilder app)
{
app.MapPost("", CreateProduct.Handler)
.RequireRole(AppRole.Administrator)
.WithSummary("Create a new product");
app.MapGet("", GetProducts.Handler)
.AllowAnonymous()
.WithSummary("Get all products");
app.MapGet("{id:guid}", GetProductById.Handler).AllowAnonymous();
app.MapPut("{id:guid}", UpdateProduct.Handler).RequireRole(AppRole.Administrator);
app.MapDelete("{id:guid}", DeleteProduct.Handler).RequireRole(AppRole.Administrator);
}
}
public class GetProducts : SliceHandler<IResult, GetProducts>
{
private readonly StarterTemplateContext _context;
public GetProducts(StarterTemplateContext context) => _context = context;
public override async Task<IResult> HandleAsync(CancellationToken cancellationToken = default)
{
var query = _context.Set<Product>().AsNoTracking().OrderBy(p => p.Name);
var request = QueryBinder.Bind<CursorPaginationParams>();
var paginatedResult = await query
.Select(p => new GetProductDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Status = p.Status,
CreatedAt = p.CreatedAt
})
.PaginateAsync(request, cancellationToken);
return Results.Ok(paginatedResult);
}
}