A sample demonstration of my approach to building distributed systems with .NET. Features multi-tenant microservices architecture using Clean Architecture, DDD, and CQRS patterns. Includes event-driven communication via Wolverine and Kafka, tenant isolation, and orchestration with Aspireβshowcasing my ability to design scalable, maintainable enterprise systems.
This project demonstrates a production-ready multi-tenant microservices architecture built with modern .NET practices. It showcases:
- Clean Architecture with Domain-Driven Design (DDD)
- CQRS Pattern using Wolverine for command/query separation
- Event-Driven Communication with Wolverine and Apache Kafka
- Multi-Tenancy with tenant isolation at database and application levels
- Service Orchestration using .NET Aspire
- Strongly Typed IDs and Value Objects for type safety
- Repository Pattern with Unit of Work
- Result Pattern for consistent error handling
- Features
- Technology Stack
- Project Structure
- Prerequisites
- Getting Started
- Configuration
- Architecture Patterns
- API Documentation
- Development
- Security
- Complete tenant isolation
- Tenant-specific credentials management
- Subscription and usage tracking
- Plan-based feature access control
- JWT-based authentication
- Role-based authorization
- Secure password handling
- User profile management
- Asynchronous messaging with Wolverine
- Kafka integration for inter-service communication
- Outbox pattern for reliable messaging
- Event sourcing capabilities
- Hot reload support
- Comprehensive logging
- Redis caching layer
- .NET Aspire dashboard for monitoring
- .NET 9 - Latest .NET framework
- ASP.NET Core - Web API framework
- Entity Framework Core - ORM with PostgreSQL provider
- Wolverine - CQRS and messaging framework
- Apache Kafka - Distributed event streaming
- MassTransit - Service bus abstraction
- PostgreSQL - Primary database
- Redis - Caching and distributed locking
- Entity Framework Outbox - Reliable message delivery
- .NET Aspire - Cloud-native application orchestration
- Swagger/OpenAPI - API documentation
- Serilog - Structured logging
MultitenancyMicroservices/
βββ AppHost/ # .NET Aspire orchestration host
β βββ Program.cs # Service configuration and composition
β βββ appsettings.json # Centralized configuration
β
βββ Common/ # Shared kernel across all services
β βββ Common.Application/ # Shared application layer
β β βββ Authentication/ # JWT and auth abstractions
β β βββ Interfaces/ # Common interfaces (IUnitOfWork, etc.)
β β βββ Result/ # Result pattern implementation
β βββ Common.Domain/ # Shared domain layer
β β βββ Abstractions/ # Base entity, value objects
β β βββ Constants/ # Domain constants
β β βββ GlobalUser/ # Global user context
β βββ Common.Infrastructure/ # Shared infrastructure
β βββ Common.Presentation/ # Shared presentation logic
β
βββ TenantService/ # Tenant management microservice
β βββ TenantService.Api/ # REST API endpoints
β βββ TenantService.Application/ # CQRS handlers
β βββ TenantService.Contracts/ # DTOs and requests
β βββ TenantService.Domain/ # Domain entities and logic
β βββ TenantService.Persistence/ # Data access layer
β βββ TenantService.Infrastructure/ # External service integrations
β
βββ UserManagement/ # User management microservice
βββ UserManagement.Api/ # REST API endpoints
βββ UserManagement.Application/ # CQRS handlers
βββ UserManagement.Contracts/ # DTOs and requests
βββ UserManagement.Domain/ # Domain entities and logic
βββ UserManagement.Persistence/ # Data access layer
βββ UserManagement.Infrastructure/ # External service integrations
- Domain: Pure business logic, entities, value objects, domain events
- Application: CQRS handlers, use case orchestration, repository interfaces
- Contracts: Request/Response DTOs, shared between API and Application
- Infrastructure: External service implementations (messaging, caching, etc.)
- Persistence: EF Core DbContext, repositories, migrations
- API: RESTful endpoints, controllers, Swagger configuration
- .NET 9 SDK
- PostgreSQL (or cloud provider)
- Redis (or cloud provider)
- Apache Kafka (or cloud provider like Redpanda)
-
Clone the repository
git clone https://github.com/yourusername/MultitenancyMicroservices.git cd MultitenancyMicroservices -
Restore dependencies
dotnet restore
-
Configure connection strings
Create service-specific
appsettings.jsonfiles from the examples:UserManagement Service - Create
UserManagement/UserManagement.Api/appsettings.json:{ "ConnectionStrings": { "SQL": "Host=localhost;Port=5432;Database=usermanagement;Username=postgres;Password=your-password" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } }TenantService - Create
TenantService/TenantService.Api/appsettings.json:{ "ConnectionStrings": { "SQL": "Host=localhost;Port=5432;Database=tenantservice;Username=postgres;Password=your-password" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } }AppHost Configuration - Create
AppHost/appsettings.json(not tracked by git):{ "UserManagement": { "ConnectionStrings": { "SQL": "Host=localhost;Port=5432;Database=usermanagement;Username=postgres;Password=your-password" } }, "TenantService": { "ConnectionStrings": { "SQL": "Host=localhost;Port=5432;Database=tenantservice;Username=postgres;Password=your-password" } }, "JwtSettings": { "Secret": "your-secret-key-must-be-at-least-32-characters-long", "ExpiryMinutes": 60, "Issuer": "YourApp", "Audience": "YourApp" }, "ConnectionStrings": { "redis": "localhost:6379,password=your-redis-password,ssl=false" }, "Kafka": { "BootstrapServers": "localhost:9092", "SecurityProtocol": "Plaintext" } } -
Apply database migrations
# User Management Service cd UserManagement/UserManagement.Api dotnet ef database update --project ../UserManagement.Persistence # Tenant Service cd ../../TenantService/TenantService.Api dotnet ef database update --project ../TenantService.Persistence
-
Run the application via AppHost
cd ../../AppHost dotnet runOr with hot reload:
dotnet watch run
The AppHost will start both microservices and the Aspire dashboard:
- Aspire Dashboard: http://localhost:15888
- UserManagement API: http://localhost:5001 | https://localhost:7001
- TenantService API: http://localhost:5002 | https://localhost:7002
For development, you can run services individually:
# User Management
cd UserManagement/UserManagement.Api
dotnet run
# Tenant Service
cd TenantService/TenantService.Api
dotnet runThe application uses a hierarchical configuration system:
appsettings.json- Base configuration with placeholders (in source control)appsettings.Development.json- Development overridesappsettings.local.json- Local secrets (NOT in source control)
The AppHost/appsettings.json contains placeholder values for:
- Database connection strings (PostgreSQL)
- JWT settings
- Redis connection
- Kafka configuration
IMPORTANT: Never commit real credentials. Always use appsettings.local.json for local development.
You can override configuration using environment variables:
# Windows PowerShell
$env:ConnectionStrings__SQL = "your-connection-string"
$env:JwtSettings__Secret = "your-jwt-secret"
# Linux/macOS
export ConnectionStrings__SQL="your-connection-string"
export JwtSettings__Secret="your-jwt-secret"- Sealed classes with private constructors
- Strongly typed IDs for type safety
- Static factory methods for creation
public sealed class User : Entity<UserId>
{
private User(UserId id, string name, string email) : base(id)
{
Name = name;
Email = email;
}
public string Name { get; private set; }
public string Email { get; private set; }
public static User Create(string name, string email)
{
return new User(new UserId(Guid.NewGuid()), name, email);
}
}
public record UserId(Guid Value)
{
public static implicit operator Guid(UserId id) => id.Value;
public static implicit operator UserId(Guid value) => new(value);
}- Implemented as records for immutability
- Encapsulate domain logic
public record CreateUserCommand(string Name, string Email, string Password);
public class CreateUserHandler
{
private readonly IUserRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public async Task<Result<UserResponse>> Handle(
CreateUserCommand command,
CancellationToken ct)
{
var user = User.Create(command.Name, command.Email, command.Password);
await _repository.AddAsync(user, ct);
await _unitOfWork.SaveChangesAsync(ct);
return Result<UserResponse>.Success(new UserResponse(user.Id));
}
}public record GetUserQuery(Guid UserId) : IQuery;
public class GetUserHandler
{
public async Task<Result<UserResponse>> Handle(
GetUserQuery query,
CancellationToken ct)
{
var user = await _repository.GetByIdAsync(query.UserId, ct);
if (user is null)
return Result<UserResponse>.Failure(new NotFoundError("User not found"));
return Result<UserResponse>.Success(new UserResponse(user.Id));
}
}Consistent error handling across all operations:
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public Error? Error { get; }
public static Result<T> Success(T value) => new(true, value, null);
public static Result<T> Failure(Error error) => new(false, default, error);
}Once running, access Swagger documentation:
- UserManagement: http://localhost:5001/swagger
- TenantService: http://localhost:5002/swagger
POST /api/users - Create new user
GET /api/users/{id} - Get user by ID
PUT /api/users/{id} - Update user
DELETE /api/users/{id} - Soft delete user
POST /api/auth/login - Authenticate user
POST /api/auth/refresh - Refresh JWT token
POST /api/tenants - Create new tenant
GET /api/tenants/{id} - Get tenant by ID
PUT /api/tenants/{id} - Update tenant
GET /api/plans - Get available plans
POST /api/subscriptions - Create subscription
GET /api/usage - Get usage statistics
dotnet build# Run all tests
dotnet test
# Run tests for specific service
cd UserManagement/UserManagement.Tests
dotnet testEach service manages its own database:
# Create migration
cd UserManagement/UserManagement.Api
dotnet ef migrations add MigrationName -p ../UserManagement.Persistence
# Update database
dotnet ef database update -p ../UserManagement.Persistence-
Define Request/Response in Contracts
public record CreateUserRequest( [Required] string Name, [EmailAddress] string Email, [MinLength(6)] string Password );
-
Create Controller Endpoint
[ApiController] [Route("api/[controller]")] public class UserController : ControllerBase { [HttpPost] public async Task<IActionResult> CreateUser( [FromBody] CreateUserRequest request, CancellationToken ct) { var result = await _messageBus.InvokeAsync<Result<UserResponse>>(request, ct); return result.ToActionResult(this); } }
-
Implement Handler in Application
public class CreateUserHandler { public async Task<Result<UserResponse>> Handle( CreateUserRequest request, CancellationToken ct) { // Implementation } }
CRITICAL: This repository has been sanitized for public sharing.
- β All sensitive data replaced with placeholders
- β
Git history cleaned (see
remove-sensitive-data.ps1) - β
.gitignoreupdated to prevent future credential commits
- Never commit real credentials
- Use
appsettings.local.jsonfor local development (already in.gitignore) - Use environment variables or secret management for production
- Use Azure Key Vault, AWS Secrets Manager, or similar
- Enable encryption at rest and in transit
- Rotate credentials regularly
- Use managed identities when possible
- Port conflicts: Ensure ports 5001, 5002, 7001, 7002 are available
- Database connection: Verify PostgreSQL is running and credentials are correct
- Redis/Kafka: Check service availability and network access
- Migrations: Ensure connection strings are set before running EF migrations
This project is licensed under the MIT License - see below for details.
MIT License
Copyright (c) 2025 Akash Jaiswal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Your Name
- GitHub: @Akash Jaiswal
- .NET Aspire team for orchestration framework
- Wolverine for CQRS implementation
- The .NET community
Note: This is a demonstration project showcasing architecture patterns and best practices for building distributed systems with .NET. It represents my approach to solving complex software engineering challenges in enterprise environments.