# Modular Solution Structure Best practices for organizing your Svrnty.CQRS application into clean, maintainable layers. ## Overview For production applications, organize your code into separate projects with clear responsibilities and dependencies. This approach provides: - ✅ **Separation of concerns** - Each project has a single responsibility - ✅ **Dependency control** - Clear, one-way dependencies - ✅ **Testability** - Easy to test each layer in isolation - ✅ **Reusability** - Share domain logic across multiple APIs - ✅ **Team scalability** - Different teams can own different projects ## Recommended Structure ``` YourSolution/ ├── src/ │ ├── YourApp.Api/ # HTTP/gRPC endpoints (entry point) │ ├── YourApp.CQRS/ # Commands, queries, handlers │ ├── YourApp.Domain/ # Domain models, business logic │ ├── YourApp.Infrastructure/ # Data access, external services │ └── YourApp.Contracts/ # Shared DTOs (optional) ├── tests/ │ ├── YourApp.CQRS.Tests/ # Unit tests for handlers │ ├── YourApp.Domain.Tests/ # Unit tests for domain logic │ └── YourApp.Api.Tests/ # Integration tests └── YourSolution.sln ``` ## Project Responsibilities ### 1. YourApp.Api (Presentation Layer) **Purpose:** HTTP/gRPC endpoints, configuration, startup logic **Contains:** - Program.cs - appsettings.json - Proto files (for gRPC) - Middleware configuration - Service registration - Authentication/authorization setup **Dependencies:** ``` YourApp.Api → YourApp.CQRS → YourApp.Infrastructure → Svrnty.CQRS.MinimalApi (or .Grpc) ``` **Example structure:** ``` YourApp.Api/ ├── Program.cs ├── appsettings.json ├── appsettings.Development.json ├── Protos/ │ └── services.proto └── Extensions/ ├── ServiceRegistrationExtensions.cs └── ConfigurationExtensions.cs ``` ### 2. YourApp.CQRS (Application Layer) **Purpose:** Commands, queries, handlers, validators, application logic **Contains:** - Command definitions - Query definitions - Command handlers - Query handlers - FluentValidation validators - Application services - DTOs (or reference Contracts project) **Dependencies:** ``` YourApp.CQRS → YourApp.Domain → YourApp.Contracts (optional) → Svrnty.CQRS.Abstractions → FluentValidation ``` **Example structure:** ``` YourApp.CQRS/ ├── Commands/ │ ├── Users/ │ │ ├── CreateUserCommand.cs │ │ ├── CreateUserCommandHandler.cs │ │ └── CreateUserCommandValidator.cs │ └── Orders/ │ ├── PlaceOrderCommand.cs │ ├── PlaceOrderCommandHandler.cs │ └── PlaceOrderCommandValidator.cs ├── Queries/ │ ├── Users/ │ │ ├── GetUserQuery.cs │ │ ├── GetUserQueryHandler.cs │ │ └── GetUserQueryValidator.cs │ └── Orders/ │ ├── GetOrderQuery.cs │ └── GetOrderQueryHandler.cs ├── DTOs/ │ ├── UserDto.cs │ └── OrderDto.cs └── Services/ └── EmailService.cs ``` ### 3. YourApp.Domain (Domain Layer) **Purpose:** Business logic, domain entities, domain events, business rules **Contains:** - Domain entities (aggregates, value objects) - Domain events - Domain services - Business rules - Interfaces for repositories (abstractions) **Dependencies:** ``` YourApp.Domain → (No dependencies - pure domain logic) ``` **Example structure:** ``` YourApp.Domain/ ├── Entities/ │ ├── User.cs │ ├── Order.cs │ └── OrderLine.cs ├── ValueObjects/ │ ├── Email.cs │ └── Address.cs ├── Events/ │ ├── UserCreatedEvent.cs │ └── OrderPlacedEvent.cs ├── Services/ │ └── OrderPricingService.cs └── Repositories/ ├── IUserRepository.cs └── IOrderRepository.cs ``` ### 4. YourApp.Infrastructure (Infrastructure Layer) **Purpose:** Data access, external services, cross-cutting concerns **Contains:** - EF Core DbContext - Repository implementations - External API clients - File storage - Email services - Caching - Logging configuration **Dependencies:** ``` YourApp.Infrastructure → YourApp.Domain → Entity Framework Core → External SDK packages ``` **Example structure:** ``` YourApp.Infrastructure/ ├── Data/ │ ├── ApplicationDbContext.cs │ ├── Migrations/ │ └── Configurations/ │ ├── UserConfiguration.cs │ └── OrderConfiguration.cs ├── Repositories/ │ ├── UserRepository.cs │ └── OrderRepository.cs ├── ExternalServices/ │ ├── SendGridEmailService.cs │ └── StripePaymentService.cs └── Caching/ └── RedisCacheService.cs ``` ### 5. YourApp.Contracts (Shared DTOs - Optional) **Purpose:** Shared data transfer objects used across layers **Contains:** - Request DTOs - Response DTOs - Shared view models **Dependencies:** ``` YourApp.Contracts → (No dependencies) ``` **Example structure:** ``` YourApp.Contracts/ ├── Users/ │ ├── UserDto.cs │ └── CreateUserRequest.cs └── Orders/ ├── OrderDto.cs └── PlaceOrderRequest.cs ``` ## Dependency Flow ``` ┌─────────────┐ │ YourApp.Api│ └──────┬──────┘ │ ▼ ┌──────────────┐ ┌────────────────────┐ │YourApp.CQRS │─────▶│ YourApp.Contracts │ └──────┬───────┘ └────────────────────┘ │ ▼ ┌──────────────┐ │YourApp.Domain│◀──────┐ └──────────────┘ │ │ ┌────────┴──────────┐ │YourApp.Infrastructure│ └───────────────────┘ ``` **Key principle:** Dependencies flow downward and inward. Domain has no dependencies. ## Complete Example ### Create the Solution ```bash # Create solution dotnet new sln -n YourApp # Create projects dotnet new webapi -n YourApp.Api -o src/YourApp.Api dotnet new classlib -n YourApp.CQRS -o src/YourApp.CQRS dotnet new classlib -n YourApp.Domain -o src/YourApp.Domain dotnet new classlib -n YourApp.Infrastructure -o src/YourApp.Infrastructure dotnet new classlib -n YourApp.Contracts -o src/YourApp.Contracts # Create test projects dotnet new xunit -n YourApp.CQRS.Tests -o tests/YourApp.CQRS.Tests dotnet new xunit -n YourApp.Domain.Tests -o tests/YourApp.Domain.Tests dotnet new xunit -n YourApp.Api.Tests -o tests/YourApp.Api.Tests # Add projects to solution dotnet sln add src/YourApp.Api/YourApp.Api.csproj dotnet sln add src/YourApp.CQRS/YourApp.CQRS.csproj dotnet sln add src/YourApp.Domain/YourApp.Domain.csproj dotnet sln add src/YourApp.Infrastructure/YourApp.Infrastructure.csproj dotnet sln add src/YourApp.Contracts/YourApp.Contracts.csproj dotnet sln add tests/YourApp.CQRS.Tests/YourApp.CQRS.Tests.csproj dotnet sln add tests/YourApp.Domain.Tests/YourApp.Domain.Tests.csproj dotnet sln add tests/YourApp.Api.Tests/YourApp.Api.Tests.csproj ``` ### Add Project References ```bash # Api references cd src/YourApp.Api dotnet add reference ../YourApp.CQRS/YourApp.CQRS.csproj dotnet add reference ../YourApp.Infrastructure/YourApp.Infrastructure.csproj # CQRS references cd ../YourApp.CQRS dotnet add reference ../YourApp.Domain/YourApp.Domain.csproj dotnet add reference ../YourApp.Contracts/YourApp.Contracts.csproj # Infrastructure references cd ../YourApp.Infrastructure dotnet add reference ../YourApp.Domain/YourApp.Domain.csproj # Test references cd ../../tests/YourApp.CQRS.Tests dotnet add reference ../../src/YourApp.CQRS/YourApp.CQRS.csproj cd ../YourApp.Domain.Tests dotnet add reference ../../src/YourApp.Domain/YourApp.Domain.csproj cd ../YourApp.Api.Tests dotnet add reference ../../src/YourApp.Api/YourApp.Api.csproj ``` ### Install NuGet Packages ```bash # Api cd ../../src/YourApp.Api dotnet add package Svrnty.CQRS.MinimalApi dotnet add package Svrnty.CQRS.FluentValidation # CQRS cd ../YourApp.CQRS dotnet add package Svrnty.CQRS.Abstractions dotnet add package FluentValidation # Domain cd ../YourApp.Domain # No packages needed (pure domain logic) # Infrastructure cd ../YourApp.Infrastructure dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design ``` ## Example Implementation ### Domain Layer ```csharp // YourApp.Domain/Entities/User.cs namespace YourApp.Domain.Entities; public class User { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public static User Create(string name, string email) { // Business rules if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name is required", nameof(name)); if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Email is required", nameof(email)); return new User { Name = name, Email = email }; } } // YourApp.Domain/Repositories/IUserRepository.cs namespace YourApp.Domain.Repositories; public interface IUserRepository { Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task GetByEmailAsync(string email, CancellationToken cancellationToken = default); Task AddAsync(User user, CancellationToken cancellationToken = default); Task UpdateAsync(User user, CancellationToken cancellationToken = default); } ``` ### Contracts Layer ```csharp // YourApp.Contracts/Users/UserDto.cs namespace YourApp.Contracts.Users; public record UserDto { public int Id { get; init; } public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; public DateTime CreatedAt { get; init; } } ``` ### CQRS Layer ```csharp // YourApp.CQRS/Commands/Users/CreateUserCommand.cs using YourApp.Domain.Entities; using YourApp.Domain.Repositories; using Svrnty.CQRS.Abstractions; using FluentValidation; namespace YourApp.CQRS.Commands.Users; public record CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; } public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { // Use domain logic var user = User.Create(command.Name, command.Email); return await _userRepository.AddAsync(user, cancellationToken); } } public class CreateUserCommandValidator : AbstractValidator { private readonly IUserRepository _userRepository; public CreateUserCommandValidator(IUserRepository userRepository) { _userRepository = userRepository; RuleFor(x => x.Name).NotEmpty().MaximumLength(100); RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MustAsync(BeUniqueEmail).WithMessage("Email already exists"); } private async Task BeUniqueEmail(string email, CancellationToken cancellationToken) { var exists = await _userRepository.GetByEmailAsync(email, cancellationToken); return exists == null; } } // YourApp.CQRS/Queries/Users/GetUserQuery.cs using YourApp.Contracts.Users; using YourApp.Domain.Repositories; using Svrnty.CQRS.Abstractions; namespace YourApp.CQRS.Queries.Users; public record GetUserQuery { public int UserId { get; init; } } public class GetUserQueryHandler : IQueryHandler { private readonly IUserRepository _userRepository; public GetUserQueryHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(query.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException($"User {query.UserId} not found"); return new UserDto { Id = user.Id, Name = user.Name, Email = user.Email, CreatedAt = user.CreatedAt }; } } ``` ### Infrastructure Layer ```csharp // YourApp.Infrastructure/Data/ApplicationDbContext.cs using Microsoft.EntityFrameworkCore; using YourApp.Domain.Entities; namespace YourApp.Infrastructure.Data; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet Users => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); } } // YourApp.Infrastructure/Repositories/UserRepository.cs using Microsoft.EntityFrameworkCore; using YourApp.Domain.Entities; using YourApp.Domain.Repositories; using YourApp.Infrastructure.Data; namespace YourApp.Infrastructure.Repositories; public class UserRepository : IUserRepository { private readonly ApplicationDbContext _context; public UserRepository(ApplicationDbContext context) { _context = context; } public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { return await _context.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); } public async Task GetByEmailAsync(string email, CancellationToken cancellationToken = default) { return await _context.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Email == email, cancellationToken); } public async Task AddAsync(User user, CancellationToken cancellationToken = default) { _context.Users.Add(user); await _context.SaveChangesAsync(cancellationToken); return user.Id; } public async Task UpdateAsync(User user, CancellationToken cancellationToken = default) { _context.Users.Update(user); await _context.SaveChangesAsync(cancellationToken); } } ``` ### Api Layer ```csharp // YourApp.Api/Program.cs using Microsoft.EntityFrameworkCore; using YourApp.CQRS.Commands.Users; using YourApp.CQRS.Queries.Users; using YourApp.Domain.Repositories; using YourApp.Infrastructure.Data; using YourApp.Infrastructure.Repositories; var builder = WebApplication.CreateBuilder(args); // Infrastructure builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); // CQRS builder.Services.AddSvrntyCQRS(); builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); builder.Services.AddCommand(); builder.Services.AddQuery(); var app = builder.Build(); // Map endpoints app.UseSvrntyCqrs(); app.Run(); ``` ## Benefits of Modular Structure ### 1. Clear Responsibilities Each project has one job: - Api: Expose endpoints - CQRS: Application logic - Domain: Business rules - Infrastructure: Technical concerns ### 2. Testability Test each layer in isolation: ```csharp // YourApp.Domain.Tests/Entities/UserTests.cs [Fact] public void Create_WithValidData_ReturnsUser() { var user = User.Create("Alice", "alice@example.com"); Assert.Equal("Alice", user.Name); Assert.Equal("alice@example.com", user.Email); } // YourApp.CQRS.Tests/Commands/CreateUserCommandHandlerTests.cs [Fact] public async Task HandleAsync_WithValidCommand_CreatesUser() { var mockRepo = new Mock(); mockRepo.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(123); var handler = new CreateUserCommandHandler(mockRepo.Object); var command = new CreateUserCommand { Name = "Alice", Email = "alice@example.com" }; var result = await handler.HandleAsync(command, CancellationToken.None); Assert.Equal(123, result); } ``` ### 3. Reusability Share domain logic across multiple APIs: ``` YourApp.PublicApi ──┐ ├──▶ YourApp.CQRS ──▶ YourApp.Domain YourApp.AdminApi ──┘ ``` ### 4. Team Scalability Different teams can own different projects: - Team A: Domain & CQRS - Team B: Infrastructure - Team C: API ## Migration from Single Project If you started with a single project, migrate gradually: ### Step 1: Extract Domain 1. Create YourApp.Domain project 2. Move domain entities 3. Move domain interfaces (IUserRepository, etc.) 4. Update references ### Step 2: Extract Infrastructure 1. Create YourApp.Infrastructure project 2. Move DbContext 3. Move repository implementations 4. Move external service clients 5. Update references ### Step 3: Extract CQRS 1. Create YourApp.CQRS project 2. Move commands, queries, handlers 3. Move validators 4. Update references ### Step 4: Keep Only Presentation in Api 1. Keep Program.cs 2. Keep configuration files 3. Keep middleware 4. Delete everything else (moved to other projects) ## Best Practices ### ✅ DO - Keep domain layer pure (no dependencies) - Use interfaces in domain, implementations in infrastructure - Put DTOs in CQRS or Contracts layer - Keep Api layer thin (configuration only) - Use dependency injection - Follow one-way dependencies (downward/inward) ### ❌ DON'T - Don't reference Infrastructure from Domain - Don't put business logic in Api layer - Don't reference Api from other projects - Don't create circular dependencies - Don't mix presentation and business logic ## What's Next? - **[Dependency Injection](dependency-injection.md)** - DI patterns for handlers - **[Tutorials: Modular Solution](../tutorials/modular-solution/README.md)** - Step-by-step guide ## See Also - [CQRS Pattern](cqrs-pattern.md) - Understanding CQRS - [Best Practices: Testing](../best-practices/testing.md) - Testing strategies - [Best Practices: Deployment](../best-practices/deployment.md) - Production deployment