A .NET 8 Clean Architecture Microservices Solution Template designed to help you quickly scaffold modular, testable, and scalable microservices.
-
Clean Architecture principles (Domain, Application, Infrastructure, API layers).
-
Microservices-ready solution structure.
-
Dependency Injection configured by default with helper classes for easy service registration.
-
Entity Framework Core setup with migrations support.
-
Docker-ready for containerized deployment.
-
Generic Repository & Unit of Work Pattern(v2) Centralized repository access with async CRUD operations. UnitOfWork ensures transaction consistency across multiple repositories.
-
Advanced Pagination (v2) PaginateRequest & PaginateResult For paging, searching, and sorting.Dynamic sortableColumns mapping for flexible queries. Domain Events (v2) Aggregate root support with AddDomainEvent.Events implemented with IDomainEvent (MediatR INotification).Automatic dispatching of events after persistence.
-
Unit Tests and Integration Tests projects included (
BuildingBlock.Tests
&BuildingBlock.IntegrationTests
). -
Helper utilities for tests:
TestAssertions
– simplified assertions.TestServiceProvider
– minimal DI container for tests.MockFactory
– quick creation of Moq mocks.TestLogger
– lightweight logger for testing.TestDataSeeder
– reusable generic test data generator.
-
MediatR pipeline behaviors included for logging, validation, and authorization.
-
Architecture tests using
NetArchTest.Rules
andFluentAssertions
to enforce layer dependencies. -
Follows best practices for maintainability and scalability.
├── .template.config
├── BuildingBlock
│ ├── BuildingBlock
│ │ ├── Behaviour/Interface
│ │ ├── CQRS
│ │ ├── Exceptions/Handler
│ │ └── Pagination
│ ├── BuildingBlock.Messaging
│ │ ├── Consumer
│ │ ├── Events
│ │ └── Producer
│ └── BuildingBlock.Tests
│ ├── Base
│ ├── Helper
├── Template
│ ├── Template.API
│ │ ├── Controllers
│ │ └── Properties
│ ├── Template.Application
│ │ ├── Common/Exceptions
│ │ ├── DTO/Logs, Request, Response
│ │ ├── Interface
│ │ ├── Mapping
│ │ └── Restaurant
│ ├── Template.Domain
│ └── Template.Infrastructure
│ ├── Data/Extensions, Seed
│ ├── Repository
│ └── Services
└── Template.Tests
├── Template.Application.UnitTests
├── Template.Architecture.Tests
├── Template.Domain.UnitTests
└── Template.Integration.Tests
BuildingBlock
– reusable building blocks (Behaviour, CQRS, Exceptions, Pagination).BuildingBlock.Messaging
– consumer, producer, and events infrastructure.BuildingBlock.Tests
– unit and integration test helpers.Template.API
– entry point for your API.Template.Application
– application layer with DTOs, interfaces, and business logic.Template.Domain
– domain entities and models.Template.Infrastructure
– database, repository, and services implementations.Template.Tests
– unit, integration, and architecture tests.
Install the template globally from NuGet:
dotnet new install CleanArchitecture.Template.Microservices
Create a new project from the template:
dotnet new cleanarch -n MyMicroservice
cd MyMicroservice
Unit tests and integration tests are already configured with xUnit and Moq.
Run all tests with:
dotnet test
This release is a major enhancement to the Clean Architecture Microservices Template.
Unlike a simple patch, these updates significantly improve flexibility, maintainability, and scalability of the solution.
-
Advanced Pagination Support
- Added
PaginateRequest
record with page, size, search, and sorting options. - Updated
PaginateResult<TEntity>
withHasNextPage
,HasPreviousPage
, and async creation.
- Added
-
Generic Repository Enhancements
- New
GetPaginateAsync
method supports:- Filtering with
Expression<Func<TEntity, bool>>
- Search using delegates for custom queries
- Dynamic sorting using
sortableColumns
dictionary
- Filtering with
- Provides standardized query handling across all repositories.
- New
-
Unit of Work Pattern
- Added
UnitOfWork
implementation to manage repository transactions. - Ensures transaction consistency with a single
SaveChangeAsync()
call. - Keeps repositories coordinated under one
DbContext
.
- Added
-
Domain Abstractions (DDD-ready)
- Introduced
Entity<T>
with auditing fields (CreatedAt
,UpdatedAt
, etc.). - Added
Aggregate<TId>
for handling domain events. - Added
IDomainEvent
interface based on MediatR’sINotification
. - Enables event-driven workflows in the Domain layer.
- Introduced
// Query
public record GetUsersQuery(int PageIndex, int PageSize, string? Search, string? SortColumn, string? SortOrder)
: IRequest<PaginateResult<User>>;
// Handler
public class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, PaginateResult<User>>
{
private readonly IUnitOfWork _unitOfWork;
public GetUsersQueryHandler(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<PaginateResult<User>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
{
return await _unitOfWork.Repository<User>().GetPaginateAsync(
new PaginateRequest(request.PageIndex, request.PageSize, request.SortColumn, request.SortOrder),
filter: u => u.IsActive,
searchFilter: q => q.Where(u => u.Name.Contains(request.Search ?? "")
|| u.Email.Contains(request.Search ?? "")),
sortableColumns: new()
{
{ "name", u => u.Name },
{ "email", u => u.Email }
});
}
}
// User Add Command and handler Using UnitOfWork
// Command
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
// Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
private readonly IUnitOfWork _unitOfWork;
public CreateUserCommandHandler(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User
{
Id = Guid.NewGuid(),
Name = request.Name,
Email = request.Email,
IsActive = true
};
await _unitOfWork.Repository<User>().AddAsync(user);
await _unitOfWork.SaveChangeAsync();
return user.Id;
}
}
// Domain Event
public record UserCreatedEvent(User User) : IDomainEvent;
// Aggregate Root
public class User : Aggregate<Guid>
{
public string Name { get; set; }
public string Email { get; set; }
public bool IsActive { get; set; }
public static User Create(string name, string email)
{
var user = new User
{
Id = Guid.NewGuid(),
Name = name,
Email = email,
IsActive = true
};
user.AddDomainEvent(new UserCreatedEvent(user));
return user;
}
}
// Event Handler (MediatR)
public class UserCreatedEventHandler : INotificationHandler<UserCreatedEvent>
{
public Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
{
Console.WriteLine($"New user created: {notification.User.Name} ({notification.User.Email})");
return Task.CompletedTask;
}
}
var user = User.Create("User", "[email protected]");
// Get and publish events via MediatR
var events = user.ClearDomainEvents();
foreach (var domainEvent in events)
{
await _mediator.Publish(domainEvent);
}
Using Helpers
// Simplify assertions:
TestAssertions.ShouldBeEmpty(new List<int>());
TestAssertions.ShouldContain(users, users[0]);
// Build a minimal DI container for tests:
var provider = TestServiceProvider.Build(services =>
{
services.AddScoped<IMyService, MyService>();
});
var myService = provider.GetRequiredService<IMyService>();
// Quickly create Moq mocks:
var mockRepo = MockFactory.CreateMock<IRepository>();
mockRepo.Setup(x => x.GetAll()).Returns(new List<Entity>());
// Generate reusable sample data:
var users = TestDataSeeder.Seed(5, i => new User
{
Id = i,
Name = $"User {i}",
Email = $"user{i}@example.com"
});
// Lightweight logger for testing:
var logger = TestLogger.CreateLogger<MyService>();
logger.LogInformation("Test log message");
// Handles authorization before processing a request:
var authBehavior = new AuthorizationBehavior<MyCommand, MyResponse>(authorizationService, httpContextAccessor);
// Logs requests and measures execution time:
var loggingBehavior = new LoggingBehaviour<MyCommand, MyResponse>(logger, loggerService);
// Validates requests using FluentValidation before processing:
var validationBehavior = new ValidationBehaviour<MyCommand, MyResponse>(validators);
Commands
public interface ICommand : ICommand<Unit> { }
public interface ICommand<out TResponse> : IRequest<TResponse> where TResponse : notnull { }
Command Handlers
public interface ICommandHandler<in TCommand> : ICommandHandler<TCommand, Unit> where TCommand : ICommand<Unit> { }
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
where TResponse : notnull { }
Queries
public interface IQuery<out TResponse> : IRequest<TResponse> where TResponse : notnull { }
Authorization & Logging
public interface IAuthorizationService<TRequest>
{
Task Authorize(TRequest request, CancellationToken cancellationToken);
}
public interface ILoggerService<TRequest>
{
Task Log(TRequest request, string result);
}
//Marker Interface
public interface IRequireAuthorization { }
public record PaginateRequest(int PageIndex = 0, int PageSize = 10,string? Search = null,string? SortColumn = null, string SortOrder = "asc");
}
Generic pagination class:
// Pagination Requets
public class PaginateResult<TEntity>
where TEntity : class
{
public PaginateResult(int pageIndex, int pageSize, long count, List<TEntity> data)
{
PageIndex = pageIndex;
PageSize = pageSize;
Count = count;
Data = data;
}
public int PageIndex { get; }
public int PageSize { get; }
public long Count { get; }
public List<TEntity> Data { get; }
public bool HasNextPage => (PageIndex * PageSize) < Count;
public bool HasPreviousPage => PageIndex > 1;
public static async Task<PaginateResult<TEntity>> CreateAsync(IQueryable<TEntity> query, int pageIndex, int pageSize)
{
int totalCount = await query.CountAsync();
var data = await query
.Skip((pageIndex - 1) * pageSize) // Skip items from previous pages
.Take(pageSize) // Take the items for the current page
.ToListAsync();
// Return a new PaginateResult instance with the fetched data.
return new(pageIndex, pageSize, totalCount, data);
}
}
// Custom exception handler for consistent API responses:
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
app.UseExceptionHandler();
Supports:
InternalServerException → 500
ValidationException → 400
BadRequestException → 400
ConflictException → 409
NotFoundException → 404
- Returns ProblemDetails with trace identifiers and validation errors.
-
Domain Abstractions
-
IEntity & IEntity → Define common properties for all entities (with auditing fields).
-
Entity → Abstract base entity with audit fields (CreatedAt, CreatedBy, etc.).
-
Aggregate → Extends Entity and manages domain events.
-
IAggregate & IAggregate → Contracts for aggregates and domain event handling.
-
IDomainEvent → Implements INotification (MediatR), enabling event-driven workflows.
public interface IDomainEvent : INotification
{
Guid EventId => Guid.NewGuid();
DateTime OccurredOn => DateTime.Now;
string EventType => GetType().AssemblyQualifiedName;
}
-
Your template includes layered architecture tests:
-
Domain layer must not depend on other layers.
-
Application layer depends only on Domain.
-
Infrastructure depends on Domain/Application only.
-
API layer depends on Application and MediatR, but not on Infrastructure.
-
Run these tests using dotnet test to enforce architectural rules.