Your First Solution
This walkthrough creates an EShop modular monolith from scratch. By the end, you will have a running API with two modules, a domain entity, CQRS commands and queries, and a minimal endpoint -- all generated by the Modulus CLI.
Time estimate
This tutorial takes about 10 minutes to complete.
Step 1 -- Initialize the Solution
Create a new solution with Aspire integration and RabbitMQ as the messaging transport:
modulus init EShop --aspire --transport rabbitmqThis generates a full solution structure with shared building blocks, a host web API, Aspire orchestration projects, and test scaffolding.
Generated Directory Structure
EShop/
├── EShop.slnx
├── Directory.Build.props
├── Directory.Packages.props
├── .editorconfig
├── .gitignore
├── src/
│ ├── BuildingBlocks/
│ │ ├── Domain/ # Entity, AggregateRoot, ValueObject, DomainEvent
│ │ ├── Application/ # IUnitOfWork, IRepository, Pagination
│ │ ├── Infrastructure/ # BaseDbContext, EfRepository, IModuleRegistration
│ │ └── Integration/ # IIntegrationEvent base types
│ ├── EShop.WebApi/ # Host application (Program.cs, endpoints)
│ ├── EShop.AppHost/ # Aspire orchestrator (when --aspire used)
│ └── EShop.ServiceDefaults/ # Aspire defaults (when --aspire used)
└── tests/
├── EShop.Tests.Unit/
├── EShop.Tests.Integration/
└── EShop.Tests.Architecture/Solution file format
Modulus generates an .slnx file, which is the modern XML-based solution format introduced in .NET 10. It replaces the older .sln format and is easier to read, merge, and diff.
Key pieces of the scaffold:
Directory.Build.propsandDirectory.Packages.props-- Central package management so every project uses consistent dependency versions.- BuildingBlocks -- Shared base classes and interfaces used across all modules.
- EShop.WebApi -- The single deployable host that composes all modules via
IModuleRegistration. - Aspire projects --
AppHostandServiceDefaultsprovide distributed tracing, health checks, and the developer dashboard. - Test projects -- Unit, integration, and architecture test projects are ready from day one.
Navigate into the solution directory for the remaining steps:
cd EShopStep 2 -- Add Modules
Add two feature modules to the solution:
modulus add-module Catalog
modulus add-module OrdersEach module is created with five layers and three dedicated test projects:
src/Modules/Catalog/
├── EShop.Modules.Catalog.Api/ # Endpoints & module registration
├── EShop.Modules.Catalog.Application/ # Commands, queries, handlers, DTOs
├── EShop.Modules.Catalog.Domain/ # Entities, value objects, domain events
├── EShop.Modules.Catalog.Infrastructure/ # EF Core, repositories, outbox
└── EShop.Modules.Catalog.Integration/ # Integration events (shared contracts)
tests/Modules/Catalog/
├── EShop.Modules.Catalog.Tests.Unit/
├── EShop.Modules.Catalog.Tests.Integration/
└── EShop.Modules.Catalog.Tests.Architecture/Layer responsibilities
| Layer | Purpose | References |
|---|---|---|
| Domain | Entities, aggregate roots, value objects, domain events | BuildingBlocks.Domain only |
| Application | Commands, queries, handlers, DTOs, interfaces | Domain, BuildingBlocks.Application |
| Infrastructure | EF Core DbContext, repositories, outbox, external services | Application, BuildingBlocks.Infrastructure |
| Api | Minimal API endpoints, module registration | Application, Infrastructure |
| Integration | Integration event contracts shared between modules | BuildingBlocks.Integration only |
The Orders module follows the same structure. Both modules are automatically wired into the host project through IModuleRegistration.
Step 3 -- Add an Entity
Scaffold a Product aggregate root in the Catalog module:
modulus add-entity Product --module Catalog --aggregate --properties "Name:string,Price:decimal"This generates:
Product.csin the Domain layer -- anAggregateRootwith the specified properties and a staticCreatefactory method.ProductConfiguration.csin the Infrastructure layer -- an EF Core entity type configuration.- The entity is registered in the module's
DbContext.
Aggregate roots vs. plain entities
Use the --aggregate flag for entities that serve as aggregate roots. Aggregate roots can raise domain events and are the entry point for all state changes within the aggregate boundary. Omit the flag for child entities within an aggregate.
Step 4 -- Add a Command
Generate a CreateProduct command with a Guid result type:
modulus add-command CreateProduct --module Catalog --result-type GuidThis produces two files in the Application layer:
CreateProduct.cs-- a record implementingICommand<Guid>.CreateProductHandler.cs-- a handler implementingICommandHandler<CreateProduct, Guid>with a skeletonHandlemethod.
Step 5 -- Add a Query
Generate a GetProductById query:
modulus add-query GetProductById --module Catalog --result-type ProductDtoThis produces:
GetProductById.cs-- a record implementingIQuery<ProductDto>.GetProductByIdHandler.cs-- a handler implementingIQueryHandler<GetProductById, ProductDto>with a skeletonHandlemethod.
Step 6 -- Add an Endpoint
Wire the command to an HTTP endpoint:
modulus add-endpoint CreateProduct --module Catalog --method POST --route / --command CreateProduct --result-type GuidThis generates a minimal API endpoint in the Api layer that:
- Accepts the
CreateProductcommand as the request body. - Sends it through the mediator.
- Returns the result.
Step 7 -- Fill in Handler Logic
The scaffolded handlers contain TODO placeholders. Open CreateProductHandler.cs in the Application layer and add the business logic:
using EShop.Modules.Catalog.Domain;
namespace EShop.Modules.Catalog.Application.Products.Commands.CreateProduct;
public class CreateProductHandler : ICommandHandler<CreateProduct, Guid>
{
private readonly IRepository<Product> _repository;
private readonly IUnitOfWork _unitOfWork;
public CreateProductHandler(IRepository<Product> repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(
CreateProduct command,
CancellationToken cancellationToken)
{
var product = Product.Create(command.Name, command.Price);
await _repository.AddAsync(product, cancellationToken);
await _unitOfWork.CommitAsync(cancellationToken);
return Result<Guid>.Success(product.Id);
}
}Similarly, fill in the GetProductByIdHandler:
namespace EShop.Modules.Catalog.Application.Products.Queries.GetProductById;
public class GetProductByIdHandler : IQueryHandler<GetProductById, ProductDto>
{
private readonly IRepository<Product> _repository;
public GetProductByIdHandler(IRepository<Product> repository)
{
_repository = repository;
}
public async Task<Result<ProductDto>> Handle(
GetProductById query,
CancellationToken cancellationToken)
{
var product = await _repository.GetByIdAsync(query.Id, cancellationToken);
if (product is null)
{
return Result<ProductDto>.Failure("Product.NotFound", "Product not found.");
}
return Result<ProductDto>.Success(new ProductDto(
product.Id,
product.Name,
product.Price));
}
}Result pattern
All handlers return Result<T> rather than throwing exceptions for expected failure cases. Use Result<T>.Success(value) for the happy path and Result<T>.Failure(code, message) for known errors. The pipeline and endpoint infrastructure handle the mapping to appropriate HTTP status codes.
Step 8 -- Run the Solution
Build and run the host:
dotnet run --project src/EShop.WebApiRunning with Aspire
If you initialized with --aspire, you can run through the Aspire AppHost instead to get the developer dashboard, distributed tracing, and health check UI:
dotnet run --project src/EShop.AppHostThe Aspire dashboard is available at https://localhost:17222 by default.
Once running, test the endpoint:
# Create a product
curl -X POST https://localhost:5001/catalog \
-H "Content-Type: application/json" \
-d '{"name": "Widget", "price": 9.99}'You should receive a 201 Created response with the new product's ID.
Summary
In this walkthrough you:
- Initialized a full modular monolith solution with Aspire and RabbitMQ support.
- Added two feature modules with clean architecture layers.
- Scaffolded a domain entity as an aggregate root.
- Generated CQRS command and query pairs with handlers.
- Wired an HTTP endpoint to the command through the mediator.
- Implemented business logic in the generated handlers.
- Ran the solution and verified the API.
What's Next
Dive deeper into specific areas of Modulus:
- Architecture Overview -- Understand the modular monolith structure, module boundaries, and dependency rules.
- Mediator -- Learn about pipeline behaviors, domain events, streaming queries, and the Result pattern.
- Messaging -- Set up integration events, configure transports, and enable the transactional outbox.
- CLI Reference -- Full reference for every CLI command and flag.