This template implements a clean, maintainable REST API architecture in .NET based on domain-centric design principles (also known as ports and adapters pattern or hexagonal architecture).
The application is structured into three main layers:
YourProject/
├── Domain/ # Core business logic
│ ├── Abstract/ # Core interfaces (IDomainService, IDataService)
│ ├── Contracts/ # Data transfer objects (DTOs)
│ ├── Extensions/ # Mapping utilities between contracts and domain models
│ ├── Model/ # Domain entities
│ ├── Registrations/ # Domain-layer dependency registrations
│ └── Services/ # Implementations of domain interfaces
├── Application/ # API's public interface
│ └── Endpoints/ # API endpoint definitions
└── Infrastructure/ # External system interactions
└── Data/ # Database interactions
├── Context/ # Entity Framework context
├── Registrations/ # Data-layer dependency registrations
└── Services/ # Implementations of data interfaces
- Domain-Centric Architecture: Dependencies point inward, with the domain layer at the center
- Clean Separation of Concerns: Each layer has clear responsibilities
- Interface-Based Design: Components interact through interfaces, facilitating testing and flexibility
- Data Transformation: Clear mapping between API contracts and domain models
git clone <repository-url>
cd <project-name>- Rename solution and project files to match your project name
- Update namespaces throughout the codebase
Create a user secret for your database connection string:
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "your-connection-string"
⚠️ Important: Never store connection strings inappsettings.json- use secrets management instead.
- Create your entity classes in
Domain/Model/ - Make sure to mark appropriate properties as required or nullable
- Consider adding a base entity class for common properties (Id, CreatedAt, etc.)
Example:
public sealed class Product
{
public int Id { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public decimal Price { get; set; }
}Define your API contracts in Domain/Contracts/:
public sealed record AddProductRequest(string Name, string? Description, decimal Price);
public sealed record ProductResponse(int Id, string Name, string? Description, decimal Price);Create extension methods in Domain/Extensions/ to map between contracts and domain models:
public static class ProductExtensions
{
public static Product ToEntity(this AddProductRequest request)
{
return new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price
};
}
public static ProductResponse ToResponse(this Product product)
{
return new ProductResponse(
product.Id,
product.Name,
product.Description,
product.Price);
}
}Add your operations to Domain/Abstract/IDomainService.cs and Domain/Abstract/IDataService.cs:
// IDomainService
Task<ProductResponse> AddProduct(AddProductRequest request);
Task<IEnumerable<ProductResponse>> GetAllProducts();
// IDataService
Task<Product> AddProduct(Product product);
Task<IEnumerable<Product>> GetAllProducts();Implement the interfaces in Domain/Services/DomainService.cs:
public async Task<ProductResponse> AddProduct(AddProductRequest request)
{
// Validate request if needed
Product product = request.ToEntity();
product = await _dataService.AddProduct(product);
return product.ToResponse();
}Add your entity to the DbContext in Infrastructure/Data/Context/ and add a configuration class for each entity:
// ApplicationDbContext.cs
public DbSet<Product> Products => Set<Product>();
// Add configuration class for your entity
public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).IsRequired();
builder.Property(p => p.Price).HasPrecision(18, 2);
}
}Add your data operations in Infrastructure/Data/Services/DataService.cs:
public async Task<Product> AddProduct(Product product)
{
_context.Add(product);
await _context.SaveChangesAsync();
return product;
}
public async Task<IEnumerable<Product>> GetAllProducts()
{
return await _context.Products.ToListAsync();
}Define your endpoints in Application/Endpoints/:
public static class ProductEndpoints
{
public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder builder)
{
RouteGroupBuilder group = builder.MapGroup("/products").WithTags("Products");
group.MapPost("/",
async Task<Results<Ok<ProductResponse>, BadRequest>> (
AddProductRequest request,
IDomainService service) =>
{
ProductResponse response = await service.AddProduct(request);
return response is null
? TypedResults.BadRequest()
: TypedResults.Ok(response);
})
.WithName("AddProduct")
.WithOpenApi();
// Add more endpoints as needed
return builder;
}
}If you create additional services, make sure to register them in Domain/Registrations/DomainRegistrations.cs and Infrastructure/Data/Registrations/DataRegistrations.cs.
Add your endpoint mappings in Program.cs:
app.MapProductEndpoints();Create and apply database migrations:
dotnet ef migrations add InitialCreate
dotnet ef database updatedotnet run- Keep the domain layer independent - It should not reference Application or Infrastructure
- Use records for DTOs - They provide immutability and value-based equality
- Validate early - Add validation in domain services before processing
- Use nullable reference types - Mark properties as nullable where appropriate
- Document interfaces - Add XML comments to explain contract requirements
- Keep endpoints focused - Each endpoint should do one thing well
- Use meaningful names - Name services and methods according to their purpose
- Test each layer independently - Write unit tests targeting specific layers