CLI tool for scaffolding .NET modular monolith solutions with built-in CQRS mediator, messaging, and Aspire support.
$ dotnet add package ModulusKit.CliA CLI tool and library suite for scaffolding .NET modular monolith solutions — with a built-in CQRS mediator, messaging with transactional outbox, and optional Aspire integration.
Modulus helps you build modular monoliths in .NET. Instead of starting with microservices, you start with a single deployable unit where each feature lives in its own module with clear boundaries. When the time comes, any module can be extracted into a standalone service.
Modulus provides:
dotnet tool install --global ModulusKit.Cli
modulus init EShop --aspire --transport rabbitmq
This creates a full solution with building blocks (Domain, Application, Infrastructure layers), a WebApi host, test projects, and Aspire orchestration.
cd EShop
modulus add-module Catalog
modulus add-module Orders
Each module gets its own Domain, Application, Infrastructure, Api, and Integration layers plus unit, integration, and architecture test projects.
modulus list-modules
graph TB
subgraph Host["EShop.WebApi"]
API[Minimal API Endpoints]
REG[Module Registration]
end
subgraph Modules
subgraph Catalog["Catalog Module"]
CA[Catalog.Api]
CAP[Catalog.Application]
CD[Catalog.Domain]
CI[Catalog.Infrastructure]
CINT[Catalog.Integration]
end
subgraph Orders["Orders Module"]
OA[Orders.Api]
OAP[Orders.Application]
OD[Orders.Domain]
OI[Orders.Infrastructure]
OINT[Orders.Integration]
end
end
subgraph BuildingBlocks["Building Blocks"]
BB_D[Domain<br/>AggregateRoot, Entity, ValueObject]
BB_A[Application<br/>UnitOfWork, Pagination]
BB_I[Infrastructure<br/>BaseDbContext, Repository]
BB_INT[Integration<br/>IntegrationEvent base types]
end
subgraph Libraries["Modulus Libraries"]
MED[Modulus.Mediator<br/>CQRS + Pipeline]
MSG[Modulus.Messaging<br/>MassTransit + Outbox]
end
API --> CA
API --> OA
CA --> CAP --> CD
CA --> CI
OA --> OAP --> OD
OA --> OI
CI --> MED
OI --> MED
CI --> MSG
OI --> MSG
CD --> BB_D
OD --> BB_D
CAP --> BB_A
OAP --> BB_A
CI --> BB_I
OI --> BB_I
CINT --> BB_INT
OINT --> BB_INT
Modulus includes a custom CQRS mediator — no MediatR dependency required. It provides commands, queries, domain events, and streaming queries with a configurable pipeline.
// Command (no return value)
public record PlaceOrder(string CustomerId, List<OrderItem> Items) : ICommand;
// Command (with return value)
public record CreateProduct(string Name, decimal Price) : ICommand<Guid>;
// Query
public record GetOrderById(Guid Id) : IQuery<OrderDto>;
public class PlaceOrderHandler : ICommandHandler<PlaceOrder>
{
public async Task<Result> Handle(PlaceOrder command, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items);
await _repository.Add(order, ct);
return Result.Success();
}
}
public class GetOrderByIdHandler : IQueryHandler<GetOrderById, OrderDto>
{
public async Task<Result<OrderDto>> Handle(GetOrderById query, CancellationToken ct)
{
var order = await _repository.GetById(query.Id, ct);
if (order is null)
return Error.NotFound("Order.NotFound", "Order was not found");
return Result<OrderDto>.Success(order.ToDto());
}
}
var result = await mediator.Send(new PlaceOrder("cust-1", items));
if (result.IsFailure)
return Results.BadRequest(result.Errors);
Behaviors wrap every request in a middleware-style pipeline, executing in registration order:
Request → UnhandledExceptionBehavior → LoggingBehavior → ValidationBehavior → Handler → Response
| Behavior | Purpose |
|---|---|
UnhandledExceptionBehavior | Catches exceptions and converts to failure Results |
LoggingBehavior | Logs request timing and success/failure |
ValidationBehavior | Runs FluentValidation validators, short-circuits on errors |
Register the pipeline:
services.AddModulusMediator(typeof(Program).Assembly);
services.AddPipelineBehavior(typeof(UnhandledExceptionBehavior<,>));
services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
services.AddPipelineBehavior(typeof(ValidationBehavior<,>));
Every command and query returns a Result or Result<T>, making error handling explicit and composable:
// Error types map to HTTP status codes
Error.Validation(code, description) // → 400 Bad Request
Error.Unauthorized(code, description) // → 401 Unauthorized
Error.Forbidden(code, description) // → 403 Forbidden
Error.NotFound(code, description) // → 404 Not Found
Error.Conflict(code, description) // → 409 Conflict
Error.Failure(code, description) // → 500 Internal Server Error
A typical request flows through the full pipeline from HTTP request to HTTP response:
HTTP Request
↓
Minimal API Endpoint
↓
mediator.Send(command) / mediator.Query(query)
↓
┌─────────────────────────────────┐
│ UnhandledExceptionBehavior │ ← catches exceptions → Result.Failure
│ ┌───────────────────────────┐ │
│ │ LoggingBehavior │ │ ← logs timing + outcome
│ │ ┌─────────────────────┐ │ │
│ │ │ ValidationBehavior │ │ │ ← FluentValidation → ValidationResult (short-circuits)
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Handler │ │ │ │ ← business logic → Result.Success / Result.Failure
│ │ │ └───────────────┘ │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
↓
UnitOfWork commit (on success)
↓
Result → HTTP Response
├── IsSuccess → 200 OK / 201 Created
├── Validation error → 400 Bad Request
├── NotFound error → 404 Not Found
└── Failure error → 500 Internal Server Error
Modulus provides a messaging abstraction over MassTransit for cross-module communication with pluggable transports.
public record OrderShipped(Guid OrderId, DateTime ShippedAt)
: IntegrationEvent;
// Publish directly
await messageBus.Publish(new OrderShipped(orderId, DateTime.UtcNow));
// Or store in the outbox for reliable delivery
await outboxStore.Save(new OrderShipped(orderId, DateTime.UtcNow));
public class OrderShippedHandler : IIntegrationEventHandler<OrderShipped>
{
public async Task Handle(OrderShipped @event, CancellationToken ct)
{
// React to the event in another module
}
}
Configure the transport at startup — no handler code changes required:
services.AddModulusMessaging(options =>
{
options.Transport = Transport.RabbitMq; // or InMemory, AzureServiceBus
options.ConnectionString = "amqp://localhost";
options.Assemblies.Add(typeof(Program).Assembly);
});
The outbox pattern ensures events are published reliably even if the message broker is temporarily unavailable. Events are stored in your database within the same transaction as your business data, then a background processor publishes them:
OutboxProcessor polls for pending events (default: every 5 seconds)Pass --aspire when initializing to include .NET Aspire projects:
modulus init EShop --aspire
This adds an AppHost and ServiceDefaults project. The WebApi host automatically registers service defaults and health check endpoints.
Each module follows a clean architecture layout:
src/Modules/Catalog/
├── src/
│ ├── Catalog.Api/ # Minimal API endpoints
│ ├── Catalog.Application/ # Commands, queries, handlers, validators
│ ├── Catalog.Domain/ # Entities, value objects, domain events
│ ├── Catalog.Infrastructure/ # DbContext, repositories, module registration
│ └── Catalog.Integration/ # Integration events shared with other modules
└── tests/
├── Catalog.Tests.Unit/
├── Catalog.Tests.Integration/
└── Catalog.Tests.Architecture/
Modules communicate with each other only through integration events — never by direct project references.
Because modules have clear boundaries, extracting one to a standalone service is straightforward:
InMemory to RabbitMq or AzureServiceBus in both solutionsNo handler or business logic changes are needed — the mediator and messaging abstractions remain the same.
| Package | Description |
|---|---|
ModulusKit.Cli | CLI tool for scaffolding modular monolith solutions |
ModulusKit.Mediator | CQRS mediator with pipeline behaviors and Result pattern |
ModulusKit.Mediator.Abstractions | Mediator interfaces, Result types, and pipeline contracts |
ModulusKit.Messaging | MassTransit messaging with multi-transport and outbox support |
ModulusKit.Messaging.Abstractions | Messaging interfaces and integration event contracts |
Contributions are welcome! Please open an issue or pull request on GitHub.
git checkout -b feature/my-feature)dotnet test)This project is licensed under the MIT License.