A complete consultant compensation management system built with .NET 8 in this project l follow Clean Architecture principles and CQRS pattern with using MediatR.
The overview of the architecture is as follows:
┌─────────────────────────────────────────────────────────────┐
│ API Layer │
│ ASP.NET Core Web API - RESTful Controllers + Swagger │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Application Layer │
│ MediatR Commands/Queries + Handlers + DTOs + Validators │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Domain Layer │
│ Entities + Domain Services + Business Rules │
└────────────────▲────────────────────────────────────────────┘
│
┌────────────────┴────────────────────────────────────────────┐
│ Infrastructure Layer │
│ EF Core + Repositories + DbContext + SQL Server │
└─────────────────────────────────────────────────────────────┘
- ✅ Consultant management with profile images
- ✅ Role-based salary calculation (Level 1, Level 2, custom roles)
- ✅ Task creation and assignment
- ✅ Multiple consultants per task
- ✅ Multiple tasks per consultant
- ✅ 12-hour daily limit enforcement
- ✅ Historical rate preservation (changes don't affect past work)
- ✅ Timeframe-based earnings calculation
- ✅ Work hours tracking per day per task
- Clean Architecture with CQRS pattern
- MediatR for command/query separation
- Entity Framework Core with SQL Server
- Repository and Unit of Work patterns
- AutoMapper for DTO mapping
- Swagger/OpenAPI documentation
- Comprehensive unit tests (87 tests)
- FluentValidation ready
- CORS enabled
- Keycloak Authentication
Mercury.ConsultantCompensation/
├── Mercury.ConsultantCompensation.Domain/ # Domain Layer
│ ├── Entities/
│ │ ├── Consultant.cs # Consultant aggregate
│ │ ├── ConsultantRole.cs # Role with rates
│ │ ├── WorkTask.cs # Task entity
│ │ ├── WorkEntry.cs # Work tracking (historical)
│ │ └── TaskAssignment.cs # Many-to-many
│ └── Services/
│ └── WorkEntryService.cs # 12-hour limit validation
│
├── Mercury.ConsultantCompensation.Application/ # Application Layer
│ ├── DTOs/ # Data Transfer Objects
│ ├── Features/ # CQRS Commands & Queries
│ │ ├── Consultants/
│ │ ├── ConsultantRoles/
│ │ ├── WorkTasks/
│ │ └── WorkEntries/
│ ├── Interfaces/ # Repository interfaces
│ └── Mappings/ # AutoMapper profiles
│
├── Mercury.ConsultantCompensation.Infrastructure/ # Infrastructure Layer
│ ├── Data/
│ │ ├── ConsultantCompensationDbContext.cs # EF Core DbContext
│ │ └── Configurations/ # Fluent API configs
│ └── Repositories/ # Repository implementations
│
├── Mercury.ConsultantCompensation.API/ # API Layer
│ ├── Controllers/ # RESTful API controllers
│ ├── Program.cs # DI configuration
│ └── appsettings.json # Configuration
│
└── Mercury.ConsultantCompensation.Domain.Tests/ # Unit Tests
├── Entities/ # Entity tests
└── Services/ # Service tests
- .NET 8.0 SDK
- SQL Server (LocalDB or full)
- Visual Studio 2022 or VS Code
-
Clone the repository
git clone https://github.com/Didza/SalaryCalculator.git cd SalaryCalculator/Mercury.ConsultantCompensation -
Update connection string in
appsettings.json:{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ConsultantCompensationDb;Trusted_Connection=true" } } -
Install EF Core tools (if not already installed):
dotnet tool install --global dotnet-ef
-
Create database migration:
cd Mercury.ConsultantCompensation.Infrastructure dotnet ef migrations add InitialCreate --startup-project ../Mercury.ConsultantCompensation.API -
Update database:
dotnet ef database update --startup-project ../Mercury.ConsultantCompensation.API
-
Run the API:
cd ../Mercury.ConsultantCompensation.API dotnet run -
Access Swagger UI:
- Navigate to:
https://localhost:5001/swagger
- Navigate to:
-
Create Keycloak realm and client for authentication (optional):
- Create realm:
mercury-consultant-compensation - Create client:
public-client - create roles for client:
Management,Consultant
- Create realm:
- Build and run with Docker Compose:
Then follow sets 8 to create the Keycloak realm and client for authentication.
docker-compose up --build -d
cd Mercury.ConsultantCompensation
dotnet testExpected output: 87 tests passed
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/consultants |
List all consultants |
| GET | /api/consultants/{id} |
Get consultant by ID |
| POST | /api/consultants |
Create new consultant |
| PUT | /api/consultants/{id} |
Update consultant |
| PUT | /api/consultants/{id}/role |
Update consultant role |
| PUT | /api/consultants/{id}/profile-image |
Upload profile image |
| DELETE | /api/consultants/{id} |
Delete consultant |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/consultantroles |
List all roles |
| GET | /api/consultantroles/{id} |
Get role by ID |
| POST | /api/consultantroles |
Create new role |
| PUT | /api/consultantroles/{id} |
Update role name |
| PUT | /api/consultantroles/{id}/rate |
Update hourly rate |
| DELETE | /api/consultantroles/{id} |
Delete role |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/worktasks |
List all tasks |
| GET | /api/worktasks/{id} |
Get task by ID |
| POST | /api/worktasks |
Create new task |
| PUT | /api/worktasks/{id} |
Update task |
| POST | /api/worktasks/{taskId}/consultants/{consultantId} |
Assign consultant to task |
| DELETE | /api/worktasks/{taskId}/consultants/{consultantId} |
Remove consultant from task |
| DELETE | /api/worktasks/{id} |
Delete task |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/workentries |
List all work entries |
| GET | /api/workentries/{id} |
Get work entry by ID |
| GET | /api/workentries/consultant/{id} |
Get entries for consultant |
| GET | /api/workentries/consultant/{id}/earnings |
Calculate earnings for period |
| POST | /api/workentries |
Create work entry (validates 12-hour limit) |
| DELETE | /api/workentries/{id} |
Delete work entry |
POST /api/consultantroles
Content-Type: application/json
{
"name": "Consultant Level 1",
"ratePerHour": 150.00
}POST /api/consultants
Content-Type: application/json
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"consultantRoleId": "guid-from-step-1"
}POST /api/worktasks
Content-Type: application/json
{
"name": "Develop API",
"description": "Build REST API endpoints",
"estimatedDurationHours": 40
}POST /api/worktasks/{taskId}/consultants/{consultantId}POST /api/workentries
Content-Type: application/json
{
"consultantId": "guid",
"taskId": "guid",
"workDate": "2024-02-18",
"hoursWorked": 8.0
}Note: This validates the 12-hour daily limit automatically
GET /api/workentries/consultant/{consultantId}/earnings?startDate=2024-02-01&endDate=2024-02-28Response:
{
"consultantId": "guid",
"consultantName": "John Doe",
"startDate": "2024-02-01",
"endDate": "2024-02-28",
"totalHoursWorked": 160.0,
"totalEarnings": 24000.00,
"workEntries": [...]
}Work entries capture the rate and role name at the time of work. Changing current rates doesn't affect previously logged hours.
public class WorkEntry
{
public decimal RatePerHourAtTimeOfWork { get; private set; } // Immutable
public string RoleNameAtTimeOfWork { get; private set; } // Immutable
public decimal CalculatePayment()
=> HoursWorked * RatePerHourAtTimeOfWork; // Always uses historical rate
}Enforced via domain service before creating work entries:
public class WorkEntryService
{
private const decimal MaxDailyHours = 12m;
public void ValidateDailyHoursLimit(Consultant consultant, DateTime workDate, decimal newHours)
{
var existingHours = consultant.GetTotalHoursForDate(workDate);
if (existingHours + newHours > MaxDailyHours)
throw new InvalidOperationException($"Would exceed {MaxDailyHours} hour daily limit");
}
}Commands and queries are separated for better maintainability:
// Command
public record CreateConsultantCommand(CreateConsultantDto Dto) : IRequest<ConsultantDto>;
// Query
public record GetConsultantByIdQuery(Guid Id) : IRequest<ConsultantDto?>;
// Handler
public class CreateConsultantCommandHandler : IRequestHandler<CreateConsultantCommand, ConsultantDto>
{
public async Task<ConsultantDto> Handle(CreateConsultantCommand request, CancellationToken ct)
{
// Implementation
}
}- 113 tests covering all entities and services
- Entity validation tests
- Business rule enforcement tests
- 12-hour limit tests
- Historical rate preservation tests
- Calculation accuracy tests
- Application Layer
Run tests:
dotnet test --verbosity normal- .NET 8.0 - Framework
- ASP.NET Core - Web API
- Entity Framework Core 8.0 - ORM
- SQL Server - Database
- MediatR - CQRS implementation
- AutoMapper - Object mapping
- FluentValidation - Validation framework
- Swagger/OpenAPI - API documentation
- xUnit - Testing framework
- Docker - Containerization