diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..ec3e155b8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: CI
+
+on:
+ push:
+ branches: [ "*" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Run unit tests
+ run: dotnet test --no-build --configuration Release --verbosity normal
\ No newline at end of file
diff --git a/CarRental.Api/CarRental.Api.csproj b/CarRental.Api/CarRental.Api.csproj
new file mode 100644
index 000000000..6c96eaf1f
--- /dev/null
+++ b/CarRental.Api/CarRental.Api.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs
new file mode 100644
index 000000000..894cebba2
--- /dev/null
+++ b/CarRental.Api/Controllers/AnalyticsController.cs
@@ -0,0 +1,126 @@
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Analytics;
+using CarRental.Application.Contracts.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// Provides specialized API endpoints for data analytics and business reporting
+///
+[ApiController]
+[Route("api/[controller]")]
+public class AnalyticsController(IAnalyticsService analyticsService, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves a list of clients who have rented cars associated with a specific model name
+ ///
+ /// The name of the car model to filter by
+ [HttpGet("clients-by-model")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetClientsByModel([FromQuery] string modelName)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {string} parameter", nameof(GetClientsByModel), GetType().Name, modelName);
+ try
+ {
+ var result = await analyticsService.ReadClientsByModelName(modelName);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetClientsByModel), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetClientsByModel), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Returns details of cars that are currently on lease at the specified date and time
+ ///
+ /// The point in time to check for active rentals
+ [HttpGet("cars-in-rent")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetCarsInRent([FromQuery] DateTime atTime)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {parameterName} = {parameterValue}", nameof(GetCarsInRent), GetType().Name, nameof(atTime), atTime);
+ try
+ {
+ var result = await analyticsService.ReadCarsInRent(atTime);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetCarsInRent), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetCarsInRent), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Returns the top 5 most popular cars based on total rental frequency
+ ///
+ [HttpGet("top-5-rented-cars")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetTop5Cars()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Cars), GetType().Name);
+ try
+ {
+ var result = await analyticsService.ReadTop5MostRentedCars();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Cars), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetTop5Cars), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Provides a comprehensive list of all cars and how many times each has been rented
+ ///
+ [HttpGet("all-cars-with-rental-count")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAllCarsWithCount()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAllCarsWithCount), GetType().Name);
+ try
+ {
+ var result = await analyticsService.ReadAllCarsWithRentalCount();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAllCarsWithCount), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAllCarsWithCount), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Returns the top 5 clients who have contributed the most to total revenue
+ ///
+ [HttpGet("top-5-clients-by-money")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetTop5Clients()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Clients), GetType().Name);
+ try
+ {
+ var result = await analyticsService.ReadTop5ClientsByTotalAmount();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Clients), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetTop5Clients), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs
new file mode 100644
index 000000000..e48fc0769
--- /dev/null
+++ b/CarRental.Api/Controllers/CarControllers.cs
@@ -0,0 +1,135 @@
+using CarRental.Application.Contracts.Car;
+using CarRental.Application.Contracts.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// API controller for managing the car fleet (CRUD operations)
+///
+[ApiController]
+[Route("api/[controller]")]
+public class CarController(IApplicationService carService, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves a list of all cars available in the system
+ ///
+ [HttpGet]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name);
+ try
+ {
+ var cars = await carService.ReadAll();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name);
+ return Ok(cars);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Retrieves details of a specific car by its identifier
+ ///
+ /// The unique identifier of the car
+ [HttpGet("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(500)]
+ public async Task> Get(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id);
+ try
+ {
+ var car = await carService.Read(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(car);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Registers a new car in the fleet
+ ///
+ /// The data for the new car record
+ [HttpPost]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(500)]
+ public async Task> Create([FromBody] CarCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto);
+ try
+ {
+ var createdCar = await carService.Create(dto);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name);
+ return CreatedAtAction(nameof(this.Create), createdCar);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Updates an existing car's information
+ ///
+ /// The unique identifier of the car to update
+ /// The updated data
+ [HttpPut("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task> Update(Guid id, [FromBody] CarCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto);
+ try
+ {
+ var updatedCar = await carService.Update(dto, id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name);
+ return Ok(updatedCar);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Removes a car from the system
+ ///
+ /// The unique identifier of the car to delete
+ [HttpDelete("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(500)]
+ public async Task Delete(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id);
+ try
+ {
+ var result = await carService.Delete(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name);
+ return result ? Ok() : NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs
new file mode 100644
index 000000000..e82727799
--- /dev/null
+++ b/CarRental.Api/Controllers/CarModelController.cs
@@ -0,0 +1,142 @@
+using CarRental.Application.Contracts.CarModel;
+using CarRental.Application.Contracts.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// API Controller for managing car models
+///
+/// The application service for car model operations
+/// The logger instance for diagnostics and activity tracking
+[ApiController]
+[Route("api/[controller]")]
+public class CarModelController(IApplicationService service, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves a list of all car models
+ ///
+ /// A collection of car model DTOs
+ [HttpGet]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name);
+ try
+ {
+ var carModel = await service.ReadAll();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name);
+ return Ok(carModel);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Retrieves a specific car model by its unique identifier
+ ///
+ /// The GUID of the car model
+ /// The requested car model DTO
+ [HttpGet("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(500)]
+ public async Task> Get(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(Get), GetType().Name);
+ try
+ {
+ var result = await service.Read(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Creates a new car model entry
+ ///
+ /// The data for the new car model
+ /// The created car model DTO
+ [HttpPost]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(500)]
+ public async Task> Create(CarModelCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto);
+ try
+ {
+ var result = await service.Create(dto);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name);
+ return CreatedAtAction(nameof(this.Create), result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Updates an existing car model
+ ///
+ /// The GUID of the car model to update
+ /// The updated information
+ /// The result of the update operation
+ [HttpPut("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task Update(Guid id, CarModelCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto);
+ try
+ {
+ var result = await service.Update(dto, id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Deletes a car model from the system
+ ///
+ /// The GUID of the car model to remove
+ /// An OK result if deleted, or NoContent if not found
+ [HttpDelete("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(500)]
+ public async Task Delete(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id);
+ try
+ {
+ var result = await service.Delete(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name);
+ return result ? Ok() : NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs
new file mode 100644
index 000000000..3a8668890
--- /dev/null
+++ b/CarRental.Api/Controllers/CarModelGenerationController.cs
@@ -0,0 +1,142 @@
+using CarRental.Application.Contracts.CarModelGeneration;
+using CarRental.Application.Contracts.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// API Controller for managing car model generations
+///
+/// The application service for car model generation logic
+/// The logger instance for diagnostics
+[ApiController]
+[Route("api/[controller]")]
+public class CarModelGenerationsController(IApplicationService service, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves all car model generations
+ ///
+ /// A list of car model generation DTOs
+ [HttpGet]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name);
+ try
+ {
+ var carModel = await service.ReadAll();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name);
+ return Ok(carModel);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Retrieves a specific car model generation by its identifier
+ ///
+ /// The unique identifier of the car model generation
+ /// The requested car model generation DTO
+ [HttpGet("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(500)]
+ public async Task> Get(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(Get), GetType().Name);
+ try
+ {
+ var result = await service.Read(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Creates a new car model generation
+ ///
+ /// The data transfer object containing car model generation details
+ /// The created car model generation DTO
+ [HttpPost]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(500)]
+ public async Task> Create(CarModelGenerationCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto);
+ try
+ {
+ var result = await service.Create(dto);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name);
+ return CreatedAtAction(nameof(this.Create), result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Updates an existing car model generation
+ ///
+ /// The unique identifier of the generation to update
+ /// The updated data for the car model generation
+ /// An IActionResult indicating the result of the operation
+ [HttpPut("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task Update(Guid id, CarModelGenerationCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto);
+ try
+ {
+ var result = await service.Update(dto, id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Deletes a car model generation by its identifier
+ ///
+ /// The unique identifier of the generation to delete
+ /// An IActionResult indicating the result of the deletion
+ [HttpDelete("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(500)]
+ public async Task Delete(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id);
+ try
+ {
+ var result = await service.Delete(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name);
+ return result ? Ok() : NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs
new file mode 100644
index 000000000..f78a440a7
--- /dev/null
+++ b/CarRental.Api/Controllers/ClientController.cs
@@ -0,0 +1,135 @@
+using Microsoft.AspNetCore.Mvc;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Application.Contracts.Client;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// API controller for managing client records and personal data (CRUD operations)
+///
+[ApiController]
+[Route("api/[controller]")]
+public class ClientController(IApplicationService clientService, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves a list of all registered clients
+ ///
+ [HttpGet]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name);
+ try
+ {
+ var result = await clientService.ReadAll();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name);
+ return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}");
+ }
+ }
+
+ ///
+ /// Retrieves a specific client by their unique identifier
+ ///
+ /// The unique identifier of the client
+ [HttpGet("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(500)]
+ public async Task> Get(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id);
+ try
+ {
+ var client = await clientService.Read(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(client);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Registers a new client and returns the created record
+ ///
+ /// The client information to create
+ [HttpPost]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(500)]
+ public async Task> Create(ClientCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto);
+ try
+ {
+ var createdClient = await clientService.Create(dto);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name);
+ return CreatedAtAction(nameof(this.Create), createdClient);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Updates an existing client's information
+ ///
+ /// The ID of the client to update
+ /// The updated client data
+ [HttpPut("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task Update(Guid id, ClientCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto);
+ try
+ {
+ var updatedClient = await clientService.Update(dto, id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name);
+ return Ok(updatedClient);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Removes a client from the system by their ID
+ ///
+ /// The unique identifier of the client to delete
+ [HttpDelete("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(500)]
+ public async Task Delete(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id);
+ try
+ {
+ var result = await clientService.Delete(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name);
+ return result ? Ok() : NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs
new file mode 100644
index 000000000..13767876d
--- /dev/null
+++ b/CarRental.Api/Controllers/RentController.cs
@@ -0,0 +1,135 @@
+using Microsoft.AspNetCore.Mvc;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Application.Contracts.Rent;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// API controller for managing car rental agreements and lease transactions
+///
+[ApiController]
+[Route("api/[controller]")]
+public class RentController(IApplicationService rentService, ILogger logger) : ControllerBase
+{
+ ///
+ /// Retrieves a list of all rental records, including calculated costs and linked entity names
+ ///
+ [HttpGet]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task>> GetAll()
+ {
+ logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name);
+ try
+ {
+ var result = await rentService.ReadAll();
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Retrieves a specific rental agreement by its identifier.
+ ///
+ /// The unique identifier of the rental record.
+ [HttpGet("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(500)]
+ public async Task> Get(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id);
+ try
+ {
+ var rent = await rentService.Read(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name);
+ return Ok(rent);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return NotFound();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Creates a new rental agreement after verifying the existence of the client and car.
+ ///
+ /// The rental details, including CarId and ClientId.
+ [HttpPost]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(500)]
+ public async Task> Create(RentCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto);
+ try
+ {
+ var createdRent = await rentService.Create(dto);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name);
+ return CreatedAtAction(nameof(this.Create), createdRent);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Updates the details of an existing rental agreement.
+ ///
+ /// The ID of the rental to update.
+ /// The updated rental data.
+ [HttpPut("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(500)]
+ public async Task Update(Guid id, RentCreateUpdateDto dto)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto);
+ try
+ {
+ var updatedRent = await rentService.Update(dto, id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name);
+ return Ok(updatedRent);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+
+ ///
+ /// Deletes a rental record from the system.
+ ///
+ /// The unique identifier of the rental to remove.
+ [HttpDelete("{id}")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(500)]
+ public async Task Delete(Guid id)
+ {
+ logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id);
+ try
+ {
+ var result = await rentService.Delete(id);
+ logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name);
+ return result ? Ok() : NoContent();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name);
+ return StatusCode(500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs
new file mode 100644
index 000000000..d34aa4d3a
--- /dev/null
+++ b/CarRental.Api/Program.cs
@@ -0,0 +1,134 @@
+using CarRental.Api;
+using CarRental.Application.Contracts.Car;
+using CarRental.Application.Contracts.CarModel;
+using CarRental.Application.Contracts.CarModelGeneration;
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Application.Contracts.Rent;
+using CarRental.Application.Services;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.DataSeed;
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Infrastructure;
+using CarRental.Infrastructure.Repository;
+using CarRental.ServiceDefaults;
+using Mapster;
+using MapsterMapper;
+using Microsoft.EntityFrameworkCore;
+using MongoDB.Driver;
+using System.Reflection;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+builder.AddMongoDBClient("CarRentalDb");
+
+builder.Services.AddDbContext((serviceProvider, options) =>
+{
+ var db = serviceProvider.GetRequiredService();
+ options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName);
+});
+
+builder.Services.AddSingleton(sp =>
+{
+ var client = sp.GetRequiredService();
+ return client.GetDatabase("car-rental");
+});
+
+var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
+typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
+builder.Services.AddSingleton(typeAdapterConfig);
+builder.Services.AddScoped();
+
+builder.Services.AddSingleton();
+
+builder.Services.AddScoped, DbCarModelRepository>();
+builder.Services.AddScoped, DbCarModelGenerationRepository>();
+builder.Services.AddScoped, DbCarRepository>();
+builder.Services.AddScoped, DbClientRepository>();
+builder.Services.AddScoped, DbRentRepository>();
+
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.Services.AddScoped, CarService>();
+builder.Services.AddScoped, ClientService>();
+builder.Services.AddScoped, RentService>();
+builder.Services.AddScoped, CarModelService>();
+builder.Services.AddScoped, CarModelGenerationService>();
+
+builder.Services.AddScoped();
+
+builder.AddGeneratorService();
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
+ });
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("CarRental"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+using (var scope = app.Services.CreateScope())
+{
+ var services = scope.ServiceProvider;
+ try
+ {
+ var context = services.GetRequiredService();
+ var dataseed = services.GetRequiredService();
+
+ if (await context.CarModels.AnyAsync()) return;
+
+ await context.CarModels.AddRangeAsync(dataseed.Models);
+ await context.SaveChangesAsync();
+
+ await context.ModelGenerations.AddRangeAsync(dataseed.Generations);
+ await context.SaveChangesAsync();
+
+ await context.Cars.AddRangeAsync(dataseed.Cars);
+ await context.SaveChangesAsync();
+
+ await context.Clients.AddRangeAsync(dataseed.Clients);
+ await context.SaveChangesAsync();
+
+ await context.Rents.AddRangeAsync(dataseed.Rents);
+ await context.SaveChangesAsync();
+ }
+ catch (Exception ex)
+ {
+ var logger = services.GetRequiredService>();
+ logger.LogError(ex, "An error occurred while seeding the database.");
+ }
+}
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
\ No newline at end of file
diff --git a/CarRental.Api/Properties/launchSettings.json b/CarRental.Api/Properties/launchSettings.json
new file mode 100644
index 000000000..6e8b0f447
--- /dev/null
+++ b/CarRental.Api/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:39741",
+ "sslPort": 44397
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5175",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7277;http://localhost:5175",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRental.Api/WebApplicationBuilderExtensions.cs b/CarRental.Api/WebApplicationBuilderExtensions.cs
new file mode 100644
index 000000000..3e4987e76
--- /dev/null
+++ b/CarRental.Api/WebApplicationBuilderExtensions.cs
@@ -0,0 +1,32 @@
+using CarRental.Infrastructure.Kafka;
+using Confluent.Kafka;
+
+namespace CarRental.Api;
+
+///
+/// Extension methods for registering generator service client in DI container
+///
+internal static class WebApplicationBuilderExtensions
+{
+ ///
+ /// Registers Kafka consumer client for interacting with the data generator service
+ ///
+ /// Web application builder
+ /// Web application builder with registered Kafka services
+ public static WebApplicationBuilder AddGeneratorService(this WebApplicationBuilder builder)
+ {
+ builder.Services.AddHostedService();
+
+ builder.AddKafkaConsumer(
+ "car-rental-kafka",
+ configureSettings: settings =>
+ {
+ settings.Config.GroupId = "car-rental-consumer-group";
+ settings.Config.AutoOffsetReset = Confluent.Kafka.AutoOffsetReset.Earliest;
+ settings.Config.EnableAutoCommit = false;
+ }
+ );
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Api/appsettings.Development.json b/CarRental.Api/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/CarRental.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental.Api/appsettings.json b/CarRental.Api/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/CarRental.Api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental.AppHost/CarRental.AppHost.csproj
new file mode 100644
index 000000000..1ab807259
--- /dev/null
+++ b/CarRental.AppHost/CarRental.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 54701a95-76ef-4922-a1a8-8f3a20203073
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs
new file mode 100644
index 000000000..21691b555
--- /dev/null
+++ b/CarRental.AppHost/Program.cs
@@ -0,0 +1,20 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var mongodb = builder.AddMongoDB("mongodb");
+mongodb.AddDatabase("car-rental");
+
+var kafka = builder.AddKafka("car-rental-kafka")
+ .WithKafkaUI()
+ .WithEnvironment("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true");
+
+builder.AddProject("carrental-api")
+ .WithReference(mongodb, "CarRentalDb")
+ .WithReference(kafka)
+ .WaitFor(mongodb)
+ .WaitFor(kafka);
+
+builder.AddProject("carrental-generator")
+ .WithReference(kafka)
+ .WaitFor(kafka);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/CarRental.AppHost/Properties/launchSettings.json b/CarRental.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..3f58aa95b
--- /dev/null
+++ b/CarRental.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17106;http://localhost:15095",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21035",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22055"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15095",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19259",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20117"
+ }
+ }
+ }
+}
diff --git a/CarRental.AppHost/appsettings.Development.json b/CarRental.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/CarRental.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental.AppHost/appsettings.json b/CarRental.AppHost/appsettings.json
new file mode 100644
index 000000000..31c092aa4
--- /dev/null
+++ b/CarRental.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/CarRental.Application.Contracts/Analytics/CarInRentDto.cs b/CarRental.Application.Contracts/Analytics/CarInRentDto.cs
new file mode 100644
index 000000000..d3233a040
--- /dev/null
+++ b/CarRental.Application.Contracts/Analytics/CarInRentDto.cs
@@ -0,0 +1,11 @@
+namespace CarRental.Application.Contracts.Analytics;
+
+///
+/// Data transfer object representing a car that is currently or was previously in an active rental state.
+///
+/// The unique identifier of the car.
+/// The descriptive name of the car model.
+/// The vehicle's license plate number.
+/// The exact start time of the rental period.
+/// The length of the rental in hours.
+public record CarInRentDto(Guid CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Analytics/CarWithRentalCountDto.cs b/CarRental.Application.Contracts/Analytics/CarWithRentalCountDto.cs
new file mode 100644
index 000000000..a0066bda1
--- /dev/null
+++ b/CarRental.Application.Contracts/Analytics/CarWithRentalCountDto.cs
@@ -0,0 +1,10 @@
+namespace CarRental.Application.Contracts.Analytics;
+
+///
+/// Data transfer object for car statistics, including the total number of times it was rented.
+///
+/// The unique identifier of the car.
+/// The descriptive name of the car model.
+/// The vehicle's license plate number.
+/// Total number of rental agreements associated with this car.
+public record CarWithRentalCountDto(Guid Id, string ModelName, string NumberPlate, int RentalCount);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application.Contracts/Analytics/ClientWithTotalAmountDto.cs
new file mode 100644
index 000000000..eac012461
--- /dev/null
+++ b/CarRental.Application.Contracts/Analytics/ClientWithTotalAmountDto.cs
@@ -0,0 +1,12 @@
+namespace CarRental.Application.Contracts.Analytics;
+
+///
+/// Data transfer object for client financial statistics.
+///
+/// The unique identifier of the client.
+/// The first name of the client.
+/// The last name of the client.
+/// The patronymic of the client.
+/// The sum of all rental costs paid by the client.
+/// Total number of times the client has rented vehicles.
+public record ClientWithTotalAmountDto(Guid Id, string FirstName, string LastName, string? Patronymic, decimal TotalSpentAmount, int TotalRentsCount);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Car/CarCreateUpdateDto.cs b/CarRental.Application.Contracts/Car/CarCreateUpdateDto.cs
new file mode 100644
index 000000000..53eb87b56
--- /dev/null
+++ b/CarRental.Application.Contracts/Car/CarCreateUpdateDto.cs
@@ -0,0 +1,9 @@
+namespace CarRental.Application.Contracts.Car;
+
+///
+/// Data transfer object for creating or updating a car record.
+///
+/// The vehicle's license plate number.
+/// The color of the car.
+/// The unique identifier of the associated car model generation.
+public record CarCreateUpdateDto(string NumberPlate, string Colour, Guid ModelGenerationId);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Car/CarDto.cs b/CarRental.Application.Contracts/Car/CarDto.cs
new file mode 100644
index 000000000..768f20b62
--- /dev/null
+++ b/CarRental.Application.Contracts/Car/CarDto.cs
@@ -0,0 +1,10 @@
+namespace CarRental.Application.Contracts.Car;
+
+///
+/// Data transfer object representing a car with its basic details for display.
+///
+/// The unique identifier of the car.
+/// The vehicle's license plate number.
+/// The color of the car.
+/// ID of the model generation.
+public record CarDto(Guid Id, string NumberPlate, string Colour, Guid ModelGenerationId);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRental.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs
new file mode 100644
index 000000000..2c7a11c1d
--- /dev/null
+++ b/CarRental.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs
@@ -0,0 +1,11 @@
+namespace CarRental.Application.Contracts.CarModel;
+
+///
+/// Data transfer object for creating or updating a car model definition.
+///
+/// The brand or specific model name.
+/// The type of drivetrain (e.g., AWD).
+/// The total passenger capacity.
+/// The style of the vehicle body (e.g., Sedan, SUV).
+/// The market segment or luxury class of the vehicle.
+public record CarModelCreateUpdateDto(string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/CarModel/CarModelDto.cs b/CarRental.Application.Contracts/CarModel/CarModelDto.cs
new file mode 100644
index 000000000..affb96206
--- /dev/null
+++ b/CarRental.Application.Contracts/CarModel/CarModelDto.cs
@@ -0,0 +1,12 @@
+namespace CarRental.Application.Contracts.CarModel;
+
+///
+/// Data transfer object representing a car model with its technical specifications.
+///
+/// The unique identifier of the car model.
+/// The brand or specific model name.
+/// The type of drivetrain (e.g., AWD, FWD, RWD).
+/// The total passenger capacity.
+/// The style of the vehicle body (e.g., Sedan, SUV).
+/// The market segment or luxury class of the vehicle.
+public record CarModelDto(Guid Id, string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs
new file mode 100644
index 000000000..b08d19333
--- /dev/null
+++ b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs
@@ -0,0 +1,10 @@
+namespace CarRental.Application.Contracts.CarModelGeneration;
+
+///
+/// Data transfer object for creating or updating a specific car model generation.
+///
+/// The manufacturing year of the generation.
+/// The type of transmission (e.g., Manual, Automatic).
+/// The rental cost per hour for this generation.
+/// The unique identifier of the parent car model.
+public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, Guid ModelId);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs
new file mode 100644
index 000000000..9c04372f9
--- /dev/null
+++ b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs
@@ -0,0 +1,11 @@
+namespace CarRental.Application.Contracts.CarModelGeneration;
+
+///
+/// Data transfer object representing a specific car model generation with pricing and details.
+///
+/// The unique identifier of the car model generation.
+/// The manufacturing year of the generation.
+/// The type of transmission used in this generation.
+/// The rental cost per hour.
+/// The identifier of the parent car model.
+public record CarModelGenerationDto(Guid Id, int Year, string? TransmissionType, decimal HourCost, Guid ModelId);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj
new file mode 100644
index 000000000..fa71b7ae6
--- /dev/null
+++ b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/CarRental.Application.Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application.Contracts/Client/ClientCreateUpdateDto.cs
new file mode 100644
index 000000000..00be2c7d0
--- /dev/null
+++ b/CarRental.Application.Contracts/Client/ClientCreateUpdateDto.cs
@@ -0,0 +1,12 @@
+namespace CarRental.Application.Contracts.Client;
+
+///
+/// Data transfer object for creating or updating client information.
+///
+/// The client's given name.
+/// The client's family name.
+/// The client's patronymic.
+/// The client's contact phone number.
+/// The unique identifier of the client's driving license.
+/// The client's date of birth.
+public record ClientCreateUpdateDto(string FirstName, string LastName, string? Patronymic, string PhoneNumber, string DriverLicenseId, DateOnly? BirthDate);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Client/ClientDto.cs b/CarRental.Application.Contracts/Client/ClientDto.cs
new file mode 100644
index 000000000..1dfa33235
--- /dev/null
+++ b/CarRental.Application.Contracts/Client/ClientDto.cs
@@ -0,0 +1,12 @@
+namespace CarRental.Application.Contracts.Client;
+
+///
+/// Data transfer object representing client details for display and identification.
+///
+/// The unique identifier of the client record.
+/// The identification number of the client's driver license.
+/// The client's family name.
+/// The client's first name.
+/// The client's middle name (optional).
+/// The client's date of birth (optional).
+public record ClientDto(Guid Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs
new file mode 100644
index 000000000..9e6104473
--- /dev/null
+++ b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRental.Application.Contracts.Generator;
+
+///
+/// Request model for generating rental contracts
+///
+public class GenerateRentalsRequest
+{
+ ///
+ /// Total number of rentals to generate
+ ///
+ [Required]
+ [Range(1, 1000, ErrorMessage = "TotalCount must be between 1 and 1000.")]
+ public int TotalCount { get; set; }
+
+ ///
+ /// Number of rentals per batch
+ ///
+ [Required]
+ [Range(1, 100, ErrorMessage = "BatchSize must be between 1 and 100.")]
+ public int BatchSize { get; set; }
+
+ ///
+ /// Delay between batches in milliseconds
+ ///
+ [Required]
+ [Range(100, 30000, ErrorMessage = "DelayMs must be between 100 and 30000.")]
+ public int DelayMs { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs
new file mode 100644
index 000000000..dc51d7042
--- /dev/null
+++ b/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs
@@ -0,0 +1,37 @@
+namespace CarRental.Application.Contracts.Generator;
+
+///
+/// Response model for rental generation operation
+///
+public class GenerateRentalsResponse
+{
+ ///
+ /// Total number of items requested
+ ///
+ public int TotalRequested { get; set; }
+
+ ///
+ /// Total number of items successfully sent
+ ///
+ public int TotalSent { get; set; }
+
+ ///
+ /// Size of each batch
+ ///
+ public int BatchSize { get; set; }
+
+ ///
+ /// Delay between batches in milliseconds
+ ///
+ public int DelayMs { get; set; }
+
+ ///
+ /// Number of batches sent
+ ///
+ public int Batches { get; set; }
+
+ ///
+ /// Whether the operation was canceled
+ ///
+ public bool Canceled { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs
new file mode 100644
index 000000000..0ce7e6dd4
--- /dev/null
+++ b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs
@@ -0,0 +1,35 @@
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Analytics;
+
+namespace CarRental.Application.Contracts.Interfaces;
+
+///
+/// Defines methods for business intelligence and data analysis across cars, clients, and rentals.
+///
+public interface IAnalyticsService
+{
+ ///
+ /// Retrieves all clients who have rented a specific car model.
+ ///
+ public Task> ReadClientsByModelName(string modelName);
+
+ ///
+ /// Lists all cars that are currently occupied at a specific point in time.
+ ///
+ public Task> ReadCarsInRent(DateTime atTime);
+
+ ///
+ /// Returns the top 5 cars with the highest number of rental agreements.
+ ///
+ public Task> ReadTop5MostRentedCars();
+
+ ///
+ /// Returns a list of all cars along with their total rental frequency.
+ ///
+ public Task> ReadAllCarsWithRentalCount();
+
+ ///
+ /// Returns the top 5 clients who have spent the most money on rentals.
+ ///
+ public Task> ReadTop5ClientsByTotalAmount();
+}
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Interfaces/IApplicationService.cs b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs
new file mode 100644
index 000000000..f61d49e1c
--- /dev/null
+++ b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs
@@ -0,0 +1,38 @@
+namespace CarRental.Application.Contracts.Interfaces;
+
+///
+/// Defines a generic contract for application services handling mapping between entities and DTOs.
+///
+/// The data transfer object used for output.
+/// The data transfer object used for input operations.
+/// The type of using key
+public interface IApplicationService
+ where TDto : class
+ where TCreateUpdateDto : class
+ where TKey : struct
+{
+ ///
+ /// Creates a new record from the provided input DTO and returns the resulting output DTO.
+ ///
+ public Task Create(TCreateUpdateDto dto);
+
+ ///
+ /// Retrieves a single record by its unique identifier, mapped to an output DTO.
+ ///
+ public Task Read(TKey id);
+
+ ///
+ /// Retrieves all records mapped to a list of output DTOs.
+ ///
+ public Task> ReadAll();
+
+ ///
+ /// Updates an existing record identified by the given ID using the input DTO data.
+ ///
+ public Task Update(TCreateUpdateDto dto, TKey id);
+
+ ///
+ /// Removes a record from the system by its unique identifier.
+ ///
+ public Task Delete(TKey id);
+}
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application.Contracts/Rent/RentCreateUpdateDto.cs
new file mode 100644
index 000000000..5e8b97378
--- /dev/null
+++ b/CarRental.Application.Contracts/Rent/RentCreateUpdateDto.cs
@@ -0,0 +1,10 @@
+namespace CarRental.Application.Contracts.Rent;
+
+///
+/// Data transfer object for creating or updating a car rental agreement.
+///
+/// The scheduled date and time for the rental to begin.
+/// The length of the rental period in hours.
+/// The unique identifier of the car to be rented.
+/// The unique identifier of the client renting the car.
+public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, Guid CarId, Guid ClientId);
\ No newline at end of file
diff --git a/CarRental.Application.Contracts/Rent/RentDto.cs b/CarRental.Application.Contracts/Rent/RentDto.cs
new file mode 100644
index 000000000..b728f0b25
--- /dev/null
+++ b/CarRental.Application.Contracts/Rent/RentDto.cs
@@ -0,0 +1,11 @@
+namespace CarRental.Application.Contracts.Rent;
+
+///
+/// Data transfer object representing a rental agreement with calculated details and linked entity info.
+///
+/// The unique identifier of the rental record.
+/// The date and time when the rental period starts.
+/// The length of the rental in hours.
+/// The unique identifier of the rented car.
+/// The unique identifier of the client.
+public record RentDto(Guid Id, DateTime StartDateTime, double Duration, Guid CarId, Guid ClientId);
\ No newline at end of file
diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj
new file mode 100644
index 000000000..1b16d887b
--- /dev/null
+++ b/CarRental.Application/CarRental.Application.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
diff --git a/CarRental.Application/CarRentalMapsterConfig.cs b/CarRental.Application/CarRentalMapsterConfig.cs
new file mode 100644
index 000000000..f07f700ad
--- /dev/null
+++ b/CarRental.Application/CarRentalMapsterConfig.cs
@@ -0,0 +1,39 @@
+using Mapster;
+using CarRental.Application.Contracts.Car;
+using CarRental.Application.Contracts.CarModel;
+using CarRental.Application.Contracts.CarModelGeneration;
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Rent;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.InternalData.ComponentClasses;
+
+namespace CarRental.Application;
+
+///
+/// This code defines a Mapster configuration class
+/// used to automate object-to-object mapping within a Car Rental application
+///
+public class CarRentalMapsterConfig : IRegister
+{
+ ///
+ /// Registers mapping rules for converting between domain entities and DTOs
+ /// to enable seamless data projection and transfer using Mapster
+ ///
+ public void Register(TypeAdapterConfig config)
+ {
+ config.NewConfig();
+ config.NewConfig();
+
+ config.NewConfig();
+ config.NewConfig();
+
+ config.NewConfig();
+ config.NewConfig();
+
+ config.NewConfig();
+ config.NewConfig();
+
+ config.NewConfig();
+ config.NewConfig();
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs
new file mode 100644
index 000000000..4bfdf2056
--- /dev/null
+++ b/CarRental.Application/Services/AnalyticsService.cs
@@ -0,0 +1,219 @@
+using Mapster;
+using CarRental.Application.Contracts.Analytics;
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for performing various analytical queries and reporting on car rental data
+///
+/// Database context used for executing optimized LINQ queries against the car rental entities
+public class AnalyticsService(CarRentalDbContext context)
+ : IAnalyticsService
+{
+ ///
+ /// Finds all clients who have rented a specific car model identified by its name
+ ///
+ /// The name (or part of the name) of the car model
+ /// A list of unique clients who rented the specified model, ordered by name
+ public async Task> ReadClientsByModelName(string modelName)
+ {
+ var modelIds = await context.CarModels
+ .AsNoTracking()
+ .Where(m => m.Name.Contains(modelName))
+ .Select(m => m.Id)
+ .ToListAsync();
+ if (!modelIds.Any()) return new List();
+ var generationIds = await context.ModelGenerations
+ .AsNoTracking()
+ .Where(mg => modelIds.Contains(mg.ModelId))
+ .Select(mg => mg.Id)
+ .ToListAsync();
+ var carIds = await context.Cars
+ .AsNoTracking()
+ .Where(c => generationIds.Contains(c.ModelGenerationId))
+ .Select(c => c.Id)
+ .ToListAsync();
+ var clientIds = await context.Rents
+ .AsNoTracking()
+ .Where(r => carIds.Contains(r.CarId))
+ .Select(r => r.ClientId)
+ .Distinct()
+ .ToListAsync();
+ var clients = await context.Clients
+ .AsNoTracking()
+ .Where(cl => clientIds.Contains(cl.Id))
+ .OrderBy(cl => cl.LastName)
+ .ThenBy(cl => cl.FirstName)
+ .ToListAsync();
+
+ return clients.Adapt>();
+ }
+
+ ///
+ /// Identifies all cars that are currently or were rented at a specific point in time
+ ///
+ /// The date and time to check for active rentals
+ /// A list of cars that were in rent at the specified time
+ public async Task> ReadCarsInRent(DateTime atTime)
+ {
+ var activeRents = await context.Rents
+ .AsNoTracking()
+ .Where(r => r.StartDateTime <= atTime)
+ .ToListAsync();
+ var filteredRents = activeRents
+ .Where(r => r.StartDateTime.AddHours(r.Duration) > atTime)
+ .ToList();
+ if (!filteredRents.Any()) return new List();
+ var carIds = filteredRents.Select(r => r.CarId).Distinct().ToList();
+ var cars = await context.Cars
+ .AsNoTracking()
+ .Where(c => carIds.Contains(c.Id))
+ .ToListAsync();
+ var generationIds = cars.Select(c => c.ModelGenerationId).Distinct().ToList();
+ var generations = await context.ModelGenerations
+ .AsNoTracking()
+ .Where(mg => generationIds.Contains(mg.Id))
+ .ToListAsync();
+ var modelIds = generations.Select(mg => mg.ModelId).Distinct().ToList();
+ var models = await context.CarModels
+ .AsNoTracking()
+ .Where(m => modelIds.Contains(m.Id))
+ .ToListAsync();
+ var result = filteredRents.Select(r =>
+ {
+ var car = cars.First(c => c.Id == r.CarId);
+ var gen = generations.First(g => g.Id == car.ModelGenerationId);
+ var model = models.First(m => m.Id == gen.ModelId);
+
+ return new CarInRentDto(
+ car.Id,
+ model.Name,
+ car.NumberPlate,
+ r.StartDateTime,
+ (int)r.Duration
+ );
+ })
+ .OrderBy(x => x.NumberPlate)
+ .ToList();
+
+ return result;
+ }
+
+ ///
+ /// Retrieves the top 5 cars with the highest total number of rental transactions
+ ///
+ /// A list of the 5 most frequently rented cars with their rental counts
+ public async Task> ReadTop5MostRentedCars()
+ {
+ var allRentCarIds = await context.Rents
+ .AsNoTracking()
+ .Select(r => r.CarId)
+ .ToListAsync();
+ if (!allRentCarIds.Any()) return new List();
+ var topStats = allRentCarIds
+ .GroupBy(id => id)
+ .Select(g => new { CarId = g.Key, Count = g.Count() })
+ .OrderByDescending(x => x.Count)
+ .Take(5)
+ .ToList();
+ var topCarIds = topStats.Select(x => x.CarId).ToList();
+ var cars = await context.Cars.AsNoTracking().Where(c => topCarIds.Contains(c.Id)).ToListAsync();
+ var generationIds = cars.Select(c => c.ModelGenerationId).Distinct().ToList();
+ var generations = await context.ModelGenerations.AsNoTracking().Where(mg => generationIds.Contains(mg.Id)).ToListAsync();
+ var modelIds = generations.Select(mg => mg.ModelId).Distinct().ToList();
+ var models = await context.CarModels.AsNoTracking().Where(m => modelIds.Contains(m.Id)).ToListAsync();
+
+ return topStats.Select(stat =>
+ {
+ var car = cars.First(c => c.Id == stat.CarId);
+ var gen = generations.First(g => g.Id == car.ModelGenerationId);
+ var model = models.First(m => m.Id == gen.ModelId);
+ return new CarWithRentalCountDto(car.Id, model.Name, car.NumberPlate, stat.Count);
+ }).ToList();
+ }
+
+ ///
+ /// Calculates the total number of rentals for every car in the system
+ ///
+ /// A complete list of cars and how many times each has been rented
+ public async Task> ReadAllCarsWithRentalCount()
+ {
+ var allRentCarIds = await context.Rents
+ .AsNoTracking()
+ .Select(r => r.CarId)
+ .ToListAsync();
+ var rentDict = allRentCarIds
+ .GroupBy(id => id)
+ .ToDictionary(g => g.Key, g => g.Count());
+ var cars = await context.Cars.AsNoTracking().ToListAsync();
+ var generations = await context.ModelGenerations.AsNoTracking().ToListAsync();
+ var models = await context.CarModels.AsNoTracking().ToListAsync();
+
+ return cars.Select(car =>
+ {
+ var gen = generations.First(g => g.Id == car.ModelGenerationId);
+ var model = models.First(m => m.Id == gen.ModelId);
+ rentDict.TryGetValue(car.Id, out var count);
+
+ return new CarWithRentalCountDto(car.Id, model.Name, car.NumberPlate, count);
+ })
+ .OrderBy(x => x.NumberPlate)
+ .ToList();
+ }
+
+ ///
+ /// Identifies the top 5 clients who have spent the most money on rentals based on duration and hourly cost
+ ///
+ /// A list of the 5 highest-paying clients with their total spent amounts
+ public async Task> ReadTop5ClientsByTotalAmount()
+ {
+ var rents = await context.Rents.AsNoTracking().ToListAsync();
+ var cars = await context.Cars.AsNoTracking().ToListAsync();
+ var generations = await context.ModelGenerations.AsNoTracking().ToListAsync();
+ var clientStats = rents
+ .GroupBy(r => r.ClientId)
+ .Select(g =>
+ {
+ var totalAmount = g.Sum(r =>
+ {
+ var car = cars.FirstOrDefault(c => c.Id == r.CarId);
+ var gen = generations.FirstOrDefault(gn => gn.Id == car?.ModelGenerationId);
+ return (decimal)r.Duration * (gen?.HourCost ?? 0);
+ });
+
+ return new
+ {
+ ClientId = g.Key,
+ Amount = totalAmount,
+ Count = g.Count()
+ };
+ })
+ .OrderByDescending(x => x.Amount)
+ .Take(5)
+ .ToList();
+ if (!clientStats.Any()) return new List();
+ var topClientIds = clientStats.Select(x => x.ClientId).ToList();
+ var clients = await context.Clients
+ .AsNoTracking()
+ .Where(c => topClientIds.Contains(c.Id))
+ .ToListAsync();
+ var result = clientStats.Select(stat =>
+ {
+ var client = clients.First(c => c.Id == stat.ClientId);
+ return new ClientWithTotalAmountDto(
+ client.Id,
+ client.FirstName,
+ client.LastName,
+ client.Patronymic,
+ stat.Amount,
+ stat.Count
+ );
+ }).ToList();
+
+ return result;
+ }
+}
diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs
new file mode 100644
index 000000000..c6786ea1c
--- /dev/null
+++ b/CarRental.Application/Services/CarModelGenerationService.cs
@@ -0,0 +1,93 @@
+using Mapster;
+using CarRental.Application.Contracts.CarModelGeneration;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for managing car model generations, including linking generations to parent car models
+///
+/// The repository for model generation entities
+/// The repository for car model entities
+public class CarModelGenerationService(
+ IBaseRepository repository,
+ IBaseRepository modelRepository)
+ : IApplicationService
+{
+ ///
+ /// Creates a new model generation after validating that the associated car model exists
+ ///
+ /// The model generation data transfer object
+ /// The created model generation as a DTO
+ /// Thrown if the associated CarModel ID is invalid
+ public async Task Create(CarModelGenerationCreateUpdateDto dto)
+ {
+ var entity = dto.Adapt();
+ var model = await modelRepository.Read(dto.ModelId)
+ ?? throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found.");
+ entity.Model = model;
+ entity.ModelId = model.Id;
+ var id = await repository.Create(entity);
+ entity.Id = id;
+ return entity.Adapt();
+ }
+
+ ///
+ /// Retrieves a specific model generation by its unique identifier
+ ///
+ /// The unique identifier of the generation
+ /// The mapped generation DTO
+ /// Thrown if the generation record is not found
+ public async Task Read(Guid id)
+ {
+ var entity = await repository.Read(id)
+ ?? throw new KeyNotFoundException($"CarModelGeneration with Id {id} not found.");
+ return entity.Adapt();
+ }
+
+ ///
+ /// Retrieves all model generations and asynchronously populates their parent models
+ ///
+ /// A list of model generation DTOs with linked model data
+ public async Task> ReadAll()
+ {
+ var entities = await repository.ReadAll();
+ foreach (var generation in entities)
+ {
+ if (generation.ModelId != Guid.Empty)
+ {
+ generation.Model = await modelRepository.Read(generation.ModelId);
+ }
+ }
+ return entities.Adapt>();
+ }
+
+ ///
+ /// Updates an existing model generation and refreshes its link to a car model
+ ///
+ /// The updated data for the generation
+ /// The identifier of the generation to update
+ /// True if the update was successful; otherwise, false
+ /// Thrown if the new parent CarModel is not found
+ public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id)
+ {
+ var existing = await repository.Read(id);
+ if (existing is null) return false;
+ dto.Adapt(existing);
+ var model = await modelRepository.Read(dto.ModelId)
+ ?? throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found.");
+ existing.Model = model;
+ existing.ModelId = model.Id;
+ return await repository.Update(existing, id);
+ }
+
+ ///
+ /// Deletes a car model generation record from the system
+ ///
+ /// The identifier of the generation to delete
+ /// True if the deletion was successful
+ public async Task Delete(Guid id)
+ => await repository.Delete(id);
+}
\ No newline at end of file
diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs
new file mode 100644
index 000000000..30249e455
--- /dev/null
+++ b/CarRental.Application/Services/CarModelService.cs
@@ -0,0 +1,75 @@
+using Mapster;
+using CarRental.Application.Contracts.CarModel;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for managing car model business logic and DTO mapping
+///
+/// The car model data repository.
+public class CarModelService(
+ IBaseRepository repository)
+ : IApplicationService
+{
+ ///
+ /// Creates a new car model and returns the result without re-querying the database
+ ///
+ /// The data transfer object for creating a car model
+ /// The newly created car model DTO.
+ public async Task Create(CarModelCreateUpdateDto dto)
+ {
+ var entity = dto.Adapt();
+ var id = await repository.Create(entity);
+ entity.Id = id;
+ return entity.Adapt();
+ }
+
+ ///
+ /// Retrieves a specific car model by its unique identifier
+ ///
+ /// The unique identifier of the car model
+ /// The mapped car model DTO
+ /// Thrown if the car model is not found
+ public async Task Read(Guid id)
+ {
+ var entity = await repository.Read(id)
+ ?? throw new KeyNotFoundException($"CarModel with Id {id} not found.");
+ return entity.Adapt();
+ }
+
+ ///
+ /// Retrieves all car models from the repository
+ ///
+ /// A list of car model DTOs
+ public async Task> ReadAll()
+ {
+ var entities = await repository.ReadAll();
+ return entities.Adapt>();
+ }
+
+ ///
+ /// Updates an existing car model's information
+ ///
+ /// The updated car model data
+ /// The identifier of the model to update
+ /// True if the update succeeded; otherwise, false
+ public async Task Update(CarModelCreateUpdateDto dto, Guid id)
+ {
+ var existing = await repository.Read(id);
+ if (existing is null) return false;
+
+ dto.Adapt(existing);
+ return await repository.Update(existing, id);
+ }
+
+ ///
+ /// Deletes a car model record by its identifier
+ ///
+ /// The unique identifier of the car model to remove
+ /// True if the deletion was successful
+ public async Task Delete(Guid id)
+ => await repository.Delete(id);
+}
diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs
new file mode 100644
index 000000000..e7a934e7c
--- /dev/null
+++ b/CarRental.Application/Services/CarService.cs
@@ -0,0 +1,88 @@
+using Mapster;
+using CarRental.Application.Contracts.Car;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for managing car business logic and coordinating data between repositories and DTOs
+///
+/// The car data repository
+/// The car model generation data repository
+public class CarService(
+ IBaseRepository repository,
+ IBaseRepository generationRepository)
+ : IApplicationService
+{
+ ///
+ /// Retrieves all cars available in the system as DTOs
+ ///
+ /// A list of car data transfer objects
+ public async Task> ReadAll()
+ {
+ var entities = await repository.ReadAll();
+ return entities.Adapt>();
+ }
+
+ ///
+ /// Retrieves a specific car by its unique identifier
+ ///
+ /// The unique identifier of the car
+ /// The found car DTO.
+ /// Thrown if the car with the specified ID does not exist
+ public async Task Read(Guid id)
+ {
+ var entity = await repository.Read(id)
+ ?? throw new KeyNotFoundException($"Car with Id {id} not found.");
+ return entity.Adapt();
+ }
+
+ ///
+ /// Creates a new car record after validating that the associated model generation exists
+ ///
+ /// The data for creating the new car
+ /// The created car as a DTO.
+ /// Thrown if the provided ModelGenerationId is invalid
+ /// Thrown if the car cannot be retrieved after creation
+ public async Task Create(CarCreateUpdateDto dto)
+ {
+ var generation = await generationRepository.Read(dto.ModelGenerationId)
+ ?? throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found.");
+ var entity = dto.Adapt();
+ var id = await repository.Create(entity);
+ var savedEntity = await repository.Read(id)
+ ?? throw new InvalidOperationException("Created car was not found.");
+ return savedEntity.Adapt();
+ }
+
+ ///
+ /// Updates an existing car's data and validates the model generation if it has changed
+ ///
+ /// The updated car data.
+ /// The unique identifier of the car to update
+ /// True if the update was successful; otherwise, false
+ /// Thrown if the new ModelGenerationId does not exist
+ public async Task Update(CarCreateUpdateDto dto, Guid id)
+ {
+ var existing = await repository.Read(id);
+ if (existing is null) return false;
+ if (dto.ModelGenerationId != existing.ModelGenerationId)
+ {
+ var generation = await generationRepository.Read(dto.ModelGenerationId)
+ ?? throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found.");
+ }
+ dto.Adapt(existing);
+ return await repository.Update(existing, id);
+ }
+
+ ///
+ /// Removes a car record from the system
+ ///
+ /// The unique identifier of the car to delete
+ /// True if the deletion was successful
+ public async Task Delete(Guid id)
+ => await repository.Delete(id);
+}
\ No newline at end of file
diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs
new file mode 100644
index 000000000..51948bd7c
--- /dev/null
+++ b/CarRental.Application/Services/ClientService.cs
@@ -0,0 +1,76 @@
+using Mapster;
+using CarRental.Application.Contracts.Client;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for managing client-related business logic and DTO mapping.
+///
+/// The client data repository.
+public class ClientService(
+ IBaseRepository repository)
+ : IApplicationService
+{
+ ///
+ /// Retrieves all clients as a list of DTOs.
+ ///
+ /// A list of client data transfer objects.
+ public async Task> ReadAll()
+ {
+ var entities = await repository.ReadAll();
+ return entities.Adapt>();
+ }
+
+ ///
+ /// Retrieves a specific client by their unique identifier
+ ///
+ /// The unique identifier of the client
+ /// The mapped client DTO
+ /// Thrown when no client exists with the given ID
+ public async Task Read(Guid id)
+ {
+ var entity = await repository.Read(id)
+ ?? throw new KeyNotFoundException($"Client with Id {id} not found.");
+ return entity.Adapt();
+ }
+
+ ///
+ /// Creates a new client record and returns the created entity as a DTO
+ ///
+ /// The client data for creation
+ /// The created client DTO
+ /// Thrown if the client cannot be retrieved after creation
+ public async Task Create(ClientCreateUpdateDto dto)
+ {
+ var entity = dto.Adapt();
+ var id = await repository.Create(entity);
+ var savedEntity = await repository.Read(id)
+ ?? throw new InvalidOperationException("Created client was not found.");
+ return savedEntity.Adapt();
+ }
+
+ ///
+ /// Updates an existing client record using the provided data
+ ///
+ /// The updated client data
+ /// The identifier of the client to update
+ /// True if the update was successful; otherwise, false
+ public async Task Update(ClientCreateUpdateDto dto, Guid id)
+ {
+ var existing = await repository.Read(id);
+ if (existing is null) return false;
+ dto.Adapt(existing);
+ return await repository.Update(existing, id);
+ }
+
+ ///
+ /// Deletes a client record from the system
+ ///
+ /// The unique identifier of the client to remove
+ /// True if the deletion was successful
+ public async Task Delete(Guid id)
+ => await repository.Delete(id);
+}
\ No newline at end of file
diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs
new file mode 100644
index 000000000..a299f7b1b
--- /dev/null
+++ b/CarRental.Application/Services/RentService.cs
@@ -0,0 +1,97 @@
+using Mapster;
+using CarRental.Application.Contracts.Rent;
+using CarRental.Application.Contracts.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Application.Services;
+
+///
+/// Service for managing rent business logic and mapping between entities and DTOs
+///
+/// The rent data repository
+/// The car data repository
+/// The client data repository
+public class RentService(
+ IBaseRepository repository,
+ IBaseRepository carRepository,
+ IBaseRepository clientRepository)
+ : IApplicationService
+{
+ ///
+ /// Retrieves all rent records as DTOs
+ ///
+ /// A list of rent data transfer objects
+ public async Task> ReadAll()
+ {
+ var rents = await repository.ReadAll();
+ return rents.Adapt>();
+ }
+
+ ///
+ /// Retrieves a specific rent by its identifier
+ ///
+ /// The unique identifier of the rent
+ /// The found rent DTO
+ /// Thrown if rent is not found
+ public async Task Read(Guid id)
+ {
+ var entity = await repository.Read(id)
+ ?? throw new KeyNotFoundException($"Rent with Id {id} not found.");
+ return entity.Adapt();
+ }
+
+ ///
+ /// Creates a new rent record after validating car and client existence
+ ///
+ /// The rent data transfer object for creation
+ /// The created rent as a DTO
+ public async Task Create(RentCreateUpdateDto dto)
+ {
+ var car = await carRepository.Read(dto.CarId)
+ ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found.");
+ var client = await clientRepository.Read(dto.ClientId)
+ ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found.");
+ var entity = dto.Adapt();
+ entity.Car = car;
+ entity.Client = client;
+ var id = await repository.Create(entity);
+ var savedEntity = await repository.Read(id)
+ ?? throw new InvalidOperationException("Created rent was not found.");
+ return savedEntity.Adapt();
+ }
+
+ ///
+ /// Updates an existing rent record and refreshes car/client links if IDs have changed
+ ///
+ /// The updated rent data
+ /// The identifier of the rent to update
+ /// True if the update succeeded; otherwise, false
+ public async Task Update(RentCreateUpdateDto dto, Guid id)
+ {
+ var existing = await repository.Read(id);
+ if (existing is null) return false;
+ dto.Adapt(existing);
+ if (dto.CarId != existing.Car?.Id)
+ {
+ var car = await carRepository.Read(dto.CarId)
+ ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found.");
+ existing.Car = car;
+ }
+ if (dto.ClientId != existing.Client?.Id)
+ {
+ var client = await clientRepository.Read(dto.ClientId)
+ ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found.");
+ existing.Client = client;
+ }
+ return await repository.Update(existing, id);
+ }
+
+ ///
+ /// Deletes a rent record by its identifier
+ ///
+ /// The identifier of the rent to delete
+ /// True if the deletion succeeded
+ public async Task Delete(Guid id)
+ => await repository.Delete(id);
+}
\ No newline at end of file
diff --git a/CarRental.Domain/CarRental.Domain.csproj b/CarRental.Domain/CarRental.Domain.csproj
new file mode 100644
index 000000000..bd71603f5
--- /dev/null
+++ b/CarRental.Domain/CarRental.Domain.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+ True
+
+
+
+
+
+
diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs
new file mode 100644
index 000000000..c31705644
--- /dev/null
+++ b/CarRental.Domain/DataModels/Car.cs
@@ -0,0 +1,34 @@
+using CarRental.Domain.InternalData.ComponentClasses;
+
+namespace CarRental.Domain.DataModels;
+
+///
+/// Represents a specific physical vehicle available for rental
+///
+public class Car
+{
+ ///
+ /// Unique identifier of the car
+ ///
+ public required Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// The model generation id this car belongs to, defining its year, transmission type, and base rental cost
+ ///
+ public required Guid ModelGenerationId { get; set; }
+
+ ///
+ /// The model generation this car belongs to, defining its year, transmission type, and base rental cost
+ ///
+ public required CarModelGeneration ModelGeneration { get; set; }
+
+ ///
+ /// License plate number of the car
+ ///
+ public required string NumberPlate { get; set; }
+
+ ///
+ /// Exterior colour of the car
+ ///
+ public required string Colour { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs
new file mode 100644
index 000000000..c059c1a7d
--- /dev/null
+++ b/CarRental.Domain/DataModels/Client.cs
@@ -0,0 +1,37 @@
+namespace CarRental.Domain.DataModels;
+
+///
+/// Represents a client (rental customer) with personal and identification information
+///
+public class Client
+{
+ ///
+ /// Unique identifier of the client
+ ///
+ public required Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Unique identifier of the client's driver's license
+ ///
+ public required string DriverLicenseId { get; set; }
+
+ ///
+ /// Client's last name (surname)
+ ///
+ public required string LastName { get; set; }
+
+ ///
+ /// Client's first name (given name)
+ ///
+ public required string FirstName { get; set; }
+
+ ///
+ /// Client's patronymic (middle name), if applicable
+ ///
+ public string? Patronymic { get; set; }
+
+ ///
+ /// Client's date of birth
+ ///
+ public DateOnly? BirthDate { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs
new file mode 100644
index 000000000..34a29d8ee
--- /dev/null
+++ b/CarRental.Domain/DataModels/Rent.cs
@@ -0,0 +1,42 @@
+namespace CarRental.Domain.DataModels;
+
+///
+/// Represents a car rental agreement between a client and the rental company
+///
+public class Rent
+{
+ ///
+ /// Unique identifier of the rental record
+ ///
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Date and time when the rental period starts
+ ///
+ public required DateTime StartDateTime { get; set; }
+
+ ///
+ /// Duration of the rental in hours
+ ///
+ public required double Duration { get; set; }
+
+ ///
+ /// The car ID that is being rented
+ ///
+ public required Guid CarId { get; set; }
+
+ ///
+ /// The car that is being rented
+ ///
+ public required Car Car { get; set; }
+
+ ///
+ /// The client ID who is renting the car
+ ///
+ public required Guid ClientId { get; set; }
+
+ ///
+ /// The client who is renting the car
+ ///
+ public required Client Client { get; set; }
+}
diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs
new file mode 100644
index 000000000..4ab5d7485
--- /dev/null
+++ b/CarRental.Domain/DataSeed/DataSeed.cs
@@ -0,0 +1,188 @@
+using CarRental.Domain.DataModels;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.InternalData.ComponentEnums;
+
+namespace CarRental.Domain.DataSeed;
+
+///
+/// Provides a fixed set of pre-initialized domain entities for testing and demonstration purposes
+///
+public class DataSeed
+{
+ ///
+ /// List of physical vehicles available for rental
+ ///
+ public List Cars { get; }
+
+ ///
+ /// List of registered clients
+ ///
+ public List Clients { get; }
+
+ ///
+ /// List of rental agreements linking clients to specific cars
+ ///
+ public List Rents { get; }
+
+ ///
+ /// List of car models representing vehicle
+ ///
+ public List Models { get; }
+
+ ///
+ /// List of car model generations
+ ///
+ public List Generations { get; }
+
+ ///
+ /// Constructor implementation
+ ///
+ public DataSeed()
+ {
+ Models = new List
+ {
+ new() { Id = Guid.NewGuid(), Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A },
+ new() { Id = Guid.NewGuid(), Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D },
+ new() { Id = Guid.NewGuid(), Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E },
+ new() { Id = Guid.NewGuid(), Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D },
+ new() { Id = Guid.NewGuid(), Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D },
+ new() { Id = Guid.NewGuid(), Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F },
+ new() { Id = Guid.NewGuid(), Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E },
+ new() { Id = Guid.NewGuid(), Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E },
+ new() { Id = Guid.NewGuid(), Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D },
+ new() { Id = Guid.NewGuid(), Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D },
+ new() { Id = Guid.NewGuid(), Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F },
+ new() { Id = Guid.NewGuid(), Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E },
+ new() { Id = Guid.NewGuid(), Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C },
+ new() { Id = Guid.NewGuid(), Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E },
+ new() { Id = Guid.NewGuid(), Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B }
+ };
+
+ Generations = new List
+ {
+ new() { Id = Guid.NewGuid(), Year = 2019, TransmissionType = TransmissionType.Manual, ModelId = Models[16].Id, Model = Models[16], HourCost = 160.00m }, // Porsche 911
+ new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[0].Id, Model = Models[0], HourCost = 35.00m }, // Fiat 500
+ new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Manual, ModelId = Models[11].Id, Model = Models[11], HourCost = 55.00m }, // Ford Focus
+ new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Variable, ModelId = Models[4].Id, Model = Models[4], HourCost = 70.00m }, // Nissan Qashqai
+ new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[18].Id, Model = Models[18], HourCost = 120.00m }, // BMW X5
+ new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[15].Id, Model = Models[15], HourCost = 140.00m }, // Lexus LS
+ new() { Id = Guid.NewGuid(), Year = 2018, TransmissionType = TransmissionType.Manual, ModelId = Models[19].Id, Model = Models[19], HourCost = 40.00m }, // Kia Rio
+ new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[7].Id, Model = Models[7], HourCost = 85.00m }, // Honda CR-V
+ new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[12].Id, Model = Models[12], HourCost = 150.00m }, // Jaguar F-Type
+ new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Manual, ModelId = Models[9].Id, Model = Models[9], HourCost = 60.00m }, // VW Transporter
+ new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[1].Id, Model = Models[1], HourCost = 95.00m }, // Subaru Outback
+ new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[8].Id, Model = Models[8], HourCost = 75.00m }, // Hyundai Tucson
+ new() { Id = Guid.NewGuid(), Year = 2019, TransmissionType = TransmissionType.Manual, ModelId = Models[2].Id, Model = Models[2], HourCost = 50.00m }, // VW Golf
+ new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[13].Id, Model = Models[13], HourCost = 100.00m }, // Tesla Model 3
+ new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[14].Id, Model = Models[14], HourCost = 80.00m }, // Toyota Camry
+ new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Automatic, ModelId = Models[6].Id, Model = Models[6], HourCost = 90.00m }, // Audi A4
+ new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[5].Id, Model = Models[5], HourCost = 105.00m }, // Volvo XC90
+ new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Manual, ModelId = Models[17].Id, Model = Models[17], HourCost = 55.00m }, // Renault Megane
+ new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[10].Id, Model = Models[10], HourCost = 110.00m }, // Mercedes E-Class
+ new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[3].Id, Model = Models[3], HourCost = 80.00m } // Mazda CX-5
+ };
+
+ Cars = new List
+ {
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440001"), ModelGenerationId = Generations[5].Id, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440002"), ModelGenerationId = Generations[14].Id, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440003"), ModelGenerationId = Generations[0].Id, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440004"), ModelGenerationId = Generations[19].Id, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440005"), ModelGenerationId = Generations[6].Id, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440006"), ModelGenerationId = Generations[16].Id, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440007"), ModelGenerationId = Generations[7].Id, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440008"), ModelGenerationId = Generations[13].Id, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440009"), ModelGenerationId = Generations[3].Id, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440010"), ModelGenerationId = Generations[18].Id, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440011"), ModelGenerationId = Generations[10].Id, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440012"), ModelGenerationId = Generations[11].Id, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440013"), ModelGenerationId = Generations[8].Id, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440014"), ModelGenerationId = Generations[9].Id, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440015"), ModelGenerationId = Generations[1].Id, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440016"), ModelGenerationId = Generations[15].Id, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440017"), ModelGenerationId = Generations[2].Id, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440018"), ModelGenerationId = Generations[17].Id, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440019"), ModelGenerationId = Generations[4].Id, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" },
+ new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440020"), ModelGenerationId = Generations[12].Id, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" }
+ };
+
+ Clients = new List
+ {
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440001"), DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440002"), DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440003"), DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440004"), DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440005"), DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440006"), DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440007"), DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440008"), DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440009"), DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440010"), DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440011"), DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440012"), DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440013"), DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440014"), DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440015"), DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440016"), DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440017"), DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440018"), DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440019"), DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) },
+ new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440020"), DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) }
+ };
+
+ var baseTime = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc);
+ Rents = new List
+ {
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-2), Duration = 6, CarId = Cars[13].Id, Car = Cars[13], ClientId = Clients[14].Id, Client = Clients[14] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(12), Duration = 6, CarId = Cars[19].Id, Car = Cars[19], ClientId = Clients[19].Id, Client = Clients[19] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-25), Duration = 48, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[2].Id, Client = Clients[2] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(8), Duration = 24, CarId = Cars[17].Id, Car = Cars[17], ClientId = Clients[17].Id, Client = Clients[17] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 72, CarId = Cars[4].Id, Car = Cars[4], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(4), Duration = 72, CarId = Cars[15].Id, Car = Cars[15], ClientId = Clients[15].Id, Client = Clients[15] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-15), Duration = 168, CarId = Cars[6].Id, Car = Cars[6], ClientId = Clients[6].Id, Client = Clients[6] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-4), Duration = 48, CarId = Cars[11].Id, Car = Cars[11], ClientId = Clients[11].Id, Client = Clients[11] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 36, CarId = Cars[8].Id, Car = Cars[8], ClientId = Clients[8].Id, Client = Clients[8] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime, Duration = 24, CarId = Cars[1].Id, Car = Cars[1], ClientId = Clients[0].Id, Client = Clients[0] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(2), Duration = 8, CarId = Cars[14].Id, Car = Cars[14], ClientId = Clients[13].Id, Client = Clients[13] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-8), Duration = 24, CarId = Cars[9].Id, Car = Cars[9], ClientId = Clients[9].Id, Client = Clients[9] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(6), Duration = 12, CarId = Cars[16].Id, Car = Cars[16], ClientId = Clients[16].Id, Client = Clients[16] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-6), Duration = 12, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[10].Id, Client = Clients[10] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(10), Duration = 48, CarId = Cars[18].Id, Car = Cars[18], ClientId = Clients[18].Id, Client = Clients[18] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-28), Duration = 12, CarId = Cars[12].Id, Car = Cars[12], ClientId = Clients[12].Id, Client = Clients[12] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 6, CarId = Cars[3].Id, Car = Cars[3], ClientId = Clients[3].Id, Client = Clients[3] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 8, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-12), Duration = 4, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[7].Id, Client = Clients[7] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-30), Duration = 24, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[1].Id, Client = Clients[1] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-25), Duration = 12, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[0].Id, Client = Clients[0] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 24, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[1].Id, Client = Clients[1] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-5), Duration = 8, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[1].Id, Client = Clients[1] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 48, CarId = Cars[3].Id, Car = Cars[3], ClientId = Clients[2].Id, Client = Clients[2] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-15), Duration = 6, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[2].Id, Client = Clients[2] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-8), Duration = 12, CarId = Cars[15].Id, Car = Cars[15], ClientId = Clients[2].Id, Client = Clients[2] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 24, CarId = Cars[4].Id, Car = Cars[4], ClientId = Clients[3].Id, Client = Clients[3] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 36, CarId = Cars[8].Id, Car = Cars[8], ClientId = Clients[3].Id, Client = Clients[3] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-12), Duration = 12, CarId = Cars[12].Id, Car = Cars[12], ClientId = Clients[3].Id, Client = Clients[3] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-6), Duration = 6, CarId = Cars[17].Id, Car = Cars[17], ClientId = Clients[3].Id, Client = Clients[3] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-28), Duration = 72, CarId = Cars[1].Id, Car = Cars[1], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-24), Duration = 24, CarId = Cars[6].Id, Car = Cars[6], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 48, CarId = Cars[9].Id, Car = Cars[9], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-16), Duration = 12, CarId = Cars[13].Id, Car = Cars[13], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 8, CarId = Cars[18].Id, Car = Cars[18], ClientId = Clients[4].Id, Client = Clients[4] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-30), Duration = 168, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-26), Duration = 24, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 48, CarId = Cars[11].Id, Car = Cars[11], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 6, CarId = Cars[14].Id, Car = Cars[14], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-14), Duration = 12, CarId = Cars[16].Id, Car = Cars[16], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 24, CarId = Cars[19].Id, Car = Cars[19], ClientId = Clients[5].Id, Client = Clients[5] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-3), Duration = 10, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[6].Id, Client = Clients[6] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-1), Duration = 5, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[7].Id, Client = Clients[7] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(1), Duration = 7, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[8].Id, Client = Clients[8] },
+ new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(3), Duration = 9, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[9].Id, Client = Clients[9] }
+ };
+ }
+}
diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs
new file mode 100644
index 000000000..73835cc3a
--- /dev/null
+++ b/CarRental.Domain/Interfaces/BaseRepository.cs
@@ -0,0 +1,84 @@
+namespace CarRental.Domain.Interfaces;
+
+///
+/// Provides a base implementation for in-memory CRUD operations
+///
+/// The type of the entity managed by the repository
+public abstract class BaseRepository : IBaseRepository
+ where TEntity : class
+{
+ private readonly List _entities;
+ ///
+ /// Gets the unique identifier from the entity
+ ///
+ protected abstract Guid GetEntityId(TEntity entity);
+
+ ///
+ /// Sets the unique identifier for the entity
+ ///
+ protected abstract void SetEntityId(TEntity entity, Guid id);
+
+ ///
+ /// Initializes the repository and determines the starting ID based on existing data
+ ///
+ protected BaseRepository(List? entities = null)
+ {
+ _entities = entities ?? new List();
+ }
+
+ ///
+ /// Adds a new entity to the collection and assigns a unique ID
+ ///
+ public virtual Task Create(TEntity entity)
+ {
+ if (entity == null)
+ throw new ArgumentNullException(nameof(entity));
+ var id = Guid.NewGuid();
+ SetEntityId(entity, id);
+ _entities.Add(entity);
+ return Task.FromResult(id);
+ }
+
+ ///
+ /// Retrieves an entity by its unique identifier
+ ///
+ public virtual Task Read(Guid id)
+ {
+ return Task.FromResult(
+ _entities.FirstOrDefault(e => GetEntityId(e) == id)
+ );
+ }
+
+ ///
+ /// Returns all entities in the collection
+ ///
+ public virtual Task> ReadAll()
+ {
+ return Task.FromResult(_entities.ToList());
+ }
+
+ ///
+ /// Replaces an existing entity at the specified ID
+ ///
+ public virtual async Task Update(TEntity entity, Guid id)
+ {
+ var existing = await Read(id);
+ if (existing == null)
+ return false;
+
+ var index = _entities.IndexOf(existing);
+ SetEntityId(entity, id);
+ _entities[index] = entity;
+
+ return true;
+ }
+
+ ///
+ /// Removes an entity from the collection by its ID
+ ///
+ public virtual async Task Delete(Guid id)
+ {
+ var entity = await Read(id);
+ return entity != null && _entities.Remove(entity);
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs
new file mode 100644
index 000000000..6742230ae
--- /dev/null
+++ b/CarRental.Domain/Interfaces/IBaseRepository.cs
@@ -0,0 +1,36 @@
+namespace CarRental.Domain.Interfaces;
+
+///
+/// Defines the standard contract for a generic repository supporting CRUD operations.
+///
+/// The type of the entity object.
+/// The type of the key.
+public interface IBaseRepository
+ where TEntity : class
+ where TKey : struct
+{
+ ///
+ /// Adds a new entity to the collection and returns a unique ID.
+ ///
+ public Task Create(TEntity entity);
+
+ ///
+ /// Retrieves an entity by its unique identifier.
+ ///
+ public Task Read(TKey id);
+
+ ///
+ /// Returns all entities in the collection.
+ ///
+ public Task> ReadAll();
+
+ ///
+ /// Replaces an existing entity at the specified ID.
+ ///
+ public Task Update(TEntity entity, TKey id);
+
+ ///
+ /// Removes an entity from the collection by its ID.
+ ///
+ public Task Delete(TKey id);
+}
\ No newline at end of file
diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs
new file mode 100644
index 000000000..9642fdc47
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs
@@ -0,0 +1,42 @@
+using CarRental.Domain.InternalData.ComponentEnums;
+using DriveType = CarRental.Domain.InternalData.ComponentEnums.DriveType;
+
+namespace CarRental.Domain.InternalData.ComponentClasses;
+
+///
+/// Represents a specific car model with its key characteristics
+/// such as name, body type, drive type, seating capacity, and vehicle class
+///
+public class CarModel
+{
+ ///
+ /// Unique identifier of the car model
+ ///
+ public required Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Name of the car model (e.g., "Camry", "Golf", "Model 3")
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Type of drive system used by the car model (front-wheel, rear-wheel or all-wheel drive)
+ ///
+ public DriveType? DriveType { get; set; }
+
+ ///
+ /// Number of passenger seats in the vehicle
+ ///
+ public required int SeatsNumber { get; set; }
+
+ ///
+ /// Body style of the car model (e.g., sedan, SUV, hatchback)
+ ///
+ public required BodyType BodyType { get; set; }
+
+ ///
+ /// Vehicle classification by size and market segment (A, B, C, D, E or F)
+ ///
+ public ClassType? ClassType { get; set; }
+}
+
diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs
new file mode 100644
index 000000000..e12013c2a
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs
@@ -0,0 +1,46 @@
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.InternalData.ComponentEnums;
+
+namespace CarRental.Domain.InternalData.ComponentClasses;
+
+///
+/// Represents a specific generation of a car model,
+/// including its production year, transmission type,
+/// and rental cost per hour
+///
+public class CarModelGeneration
+{
+ ///
+ /// Unique identifier of the car model generation
+ ///
+ public required Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Calendar year when this generation of the car model was produced
+ ///
+ public required int Year { get; set; }
+
+ ///
+ /// Type of transmission used in this car model generation (manual, automatic, robotic or variable)
+ ///
+ public TransmissionType? TransmissionType { get; set; }
+
+ ///
+ /// The car model ID to which this generation belongs (a class that describes
+ /// the main technical characteristics, such as the model name,
+ /// drive type, transmission type, body type, and vehicle class)
+ ///
+ public Guid ModelId { get; set; }
+
+ ///
+ /// The car model to which this generation belongs (a class that describes
+ /// the main technical characteristics, such as the model name,
+ /// drive type, transmission type, body type, and vehicle class)
+ ///
+ public CarModel? Model { get; set; }
+
+ ///
+ /// Rental cost per hour for vehicles of this model generation
+ ///
+ public required decimal HourCost { get; set; }
+}
diff --git a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs
new file mode 100644
index 000000000..16587ce1f
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs
@@ -0,0 +1,92 @@
+namespace CarRental.Domain.InternalData.ComponentEnums;
+
+///
+/// Type of vehicle body style
+///
+public enum BodyType
+{
+ ///
+ /// Ultra-small city car designed for maximum fuel efficiency and maneuverability
+ ///
+ CityCar,
+
+ ///
+ /// Stylish car with a sloping roofline and typically two doors
+ ///
+ Coupe,
+
+ ///
+ /// SUV-like vehicle built on a car platform, offering elevated ride height and versatility
+ ///
+ Crossover,
+
+ ///
+ /// Car with a retractable soft or hard roof, offering open-air driving
+ ///
+ Cabriolet,
+
+ ///
+ /// Traditional sedan with four side doors and a separate trunk compartment
+ ///
+ FourDoorSedan,
+
+ ///
+ /// Sedan styled with a coupe-like roofline but four functional doors
+ ///
+ FourDoorCoupe,
+
+ ///
+ /// Compact car with a rear door that opens upward, including the rear window
+ ///
+ Hatchback,
+
+ ///
+ /// Extended luxury sedan with extra interior space, often chauffeur-driven
+ ///
+ Limousine,
+
+ ///
+ /// Family-oriented van with car-like handling and flexible interior seating
+ ///
+ Minivan,
+
+ ///
+ /// Rugged vehicle engineered for driving on unpaved and challenging terrain
+ ///
+ OffRoadCar,
+
+ ///
+ /// Light-duty truck with an open cargo bed separate from the passenger cabin
+ ///
+ PickupTruck,
+
+ ///
+ /// Lightweight two-seater sports car with a focus on driving dynamics
+ ///
+ Roadster,
+
+ ///
+ /// Standard passenger car with a three-box configuration: engine, cabin, and trunk
+ ///
+ Sedan,
+
+ ///
+ /// High-riding, versatile vehicle combining passenger and cargo capacity with off-road capability
+ ///
+ SportUtilityVehicle,
+
+ ///
+ /// Low-slung, high-performance vehicle built for speed and agile handling
+ ///
+ SportsCar,
+
+ ///
+ /// Passenger car with an extended roofline and large cargo area at the rear
+ ///
+ StationWagon,
+
+ ///
+ /// Box-shaped vehicle designed primarily for transporting goods or multiple passengers
+ ///
+ Van
+}
\ No newline at end of file
diff --git a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs
new file mode 100644
index 000000000..5c415b308
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs
@@ -0,0 +1,37 @@
+namespace CarRental.Domain.InternalData.ComponentEnums;
+
+///
+/// Vehicle classification based on size and segment
+///
+public enum ClassType
+{
+ ///
+ /// Mini cars, the smallest urban vehicle class
+ ///
+ A,
+
+ ///
+ /// Small cars, compact and economical for city driving
+ ///
+ B,
+
+ ///
+ /// Medium cars, offering balanced space and comfort
+ ///
+ C,
+
+ ///
+ /// Large family cars, with enhanced interior room and features
+ ///
+ D,
+
+ ///
+ /// Executive cars, premium mid-size to large sedans
+ ///
+ E,
+
+ ///
+ /// Luxury cars, high-end flagship vehicles with top-tier features
+ ///
+ F
+}
\ No newline at end of file
diff --git a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs
new file mode 100644
index 000000000..d30a9f9ad
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs
@@ -0,0 +1,22 @@
+namespace CarRental.Domain.InternalData.ComponentEnums;
+
+///
+/// The type of vehicle drive system
+///
+public enum DriveType
+{
+ ///
+ /// Front-wheel drive, where power is delivered to the front wheels
+ ///
+ FrontWheel,
+
+ ///
+ /// Rear-wheel drive, where power is delivered to the rear wheels
+ ///
+ RearWheel,
+
+ ///
+ /// All-wheel drive, where power is distributed to all wheels
+ ///
+ AllWheel
+}
\ No newline at end of file
diff --git a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs
new file mode 100644
index 000000000..50170c726
--- /dev/null
+++ b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs
@@ -0,0 +1,27 @@
+namespace CarRental.Domain.InternalData.ComponentEnums;
+
+///
+/// The type of vehicle transmission
+///
+public enum TransmissionType
+{
+ ///
+ /// Manual gearbox with driver-operated gear shifting
+ ///
+ Manual,
+
+ ///
+ /// Automatic gearbox requiring no driver input for shifting
+ ///
+ Automatic,
+
+ ///
+ /// Robotic gearbox with automated clutch control
+ ///
+ Robotic,
+
+ ///
+ /// Continuously variable transmission (CVT) with stepless gear ratio adjustment
+ ///
+ Variable
+}
\ No newline at end of file
diff --git a/CarRental.Generator/CarRental.Generator.csproj b/CarRental.Generator/CarRental.Generator.csproj
new file mode 100644
index 000000000..4d982f91f
--- /dev/null
+++ b/CarRental.Generator/CarRental.Generator.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental.Generator/Controller/GeneratorController.cs b/CarRental.Generator/Controller/GeneratorController.cs
new file mode 100644
index 000000000..a9e418ecb
--- /dev/null
+++ b/CarRental.Generator/Controller/GeneratorController.cs
@@ -0,0 +1,90 @@
+using Microsoft.AspNetCore.Mvc;
+using CarRental.Generator.Generation;
+using CarRental.Application.Contracts.Rent;
+using CarRental.Application.Contracts.Generator;
+
+namespace CarRental.Generator.Controller;
+
+///
+/// Controller for generating and publishing rental data to Kafka
+///
+/// Kafka producer for sending messages
+/// Service for generating rental contracts
+/// Logger instance
+[ApiController]
+[Route("api/[controller]")]
+public class GeneratorController(
+ KafkaProducer producer,
+ RentGeneratorService rentGenerator,
+ ILogger logger) : ControllerBase
+{
+ ///
+ /// Generates and publishes rental contracts to Kafka in batches
+ ///
+ /// Generation parameters
+ /// Cancellation token
+ /// Result with generation statistics
+ [HttpPost("rentals")]
+ public async Task> GenerateRentals(
+ [FromQuery] GenerateRentalsRequest request,
+ CancellationToken cancellationToken)
+ {
+ logger.LogInformation("Rental generation requested. TotalCount={TotalCount}, BatchSize={BatchSize}, DelayMs={DelayMs}",
+ request.TotalCount, request.BatchSize, request.DelayMs);
+
+ var sent = 0;
+ var batches = 0;
+
+ try
+ {
+ while (sent < request.TotalCount && !cancellationToken.IsCancellationRequested)
+ {
+ var remaining = request.TotalCount - sent;
+ var currentBatchSize = Math.Min(request.BatchSize, remaining);
+
+ IList batch = rentGenerator.GenerateContract(currentBatchSize);
+
+ await producer.ProduceMany(batch, cancellationToken);
+
+ sent += currentBatchSize;
+ batches++;
+
+ if (sent < request.TotalCount && request.DelayMs > 0)
+ {
+ await Task.Delay(request.DelayMs, cancellationToken);
+ }
+ }
+
+ logger.LogInformation("Generation finished. TotalSent={TotalSent}, Batches={Batches}", sent, batches);
+
+ return Ok(new GenerateRentalsResponse
+ {
+ TotalRequested = request.TotalCount,
+ TotalSent = sent,
+ BatchSize = request.BatchSize,
+ DelayMs = request.DelayMs,
+ Batches = batches,
+ Canceled = false
+ });
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("Generation was canceled. TotalSent={TotalSent}/{TotalCount}", sent, request.TotalCount);
+
+ return Ok(new GenerateRentalsResponse
+ {
+ TotalRequested = request.TotalCount,
+ TotalSent = sent,
+ BatchSize = request.BatchSize,
+ DelayMs = request.DelayMs,
+ Batches = batches,
+ Canceled = true
+ });
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unexpected error during generation/publishing. TotalSent={TotalSent}/{TotalCount}", sent, request.TotalCount);
+ return StatusCode(500, "An error occurred while generating and sending rentals");
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Generator/Generation/GeneratorOptions.cs b/CarRental.Generator/Generation/GeneratorOptions.cs
new file mode 100644
index 000000000..977c8d482
--- /dev/null
+++ b/CarRental.Generator/Generation/GeneratorOptions.cs
@@ -0,0 +1,67 @@
+namespace CarRental.Generator.Generation;
+
+///
+/// Configuration options for the rental data generator.
+///
+public class GeneratorOptions
+{
+ private List? _carIdStrings;
+ private List? _clientIdStrings;
+ private List? _carIds;
+ private List? _clientIds;
+
+ ///
+ /// List of available car IDs as strings (for configuration binding).
+ ///
+ public List CarIds
+ {
+ get => _carIdStrings ?? new();
+ set
+ {
+ _carIdStrings = value;
+ _carIds = ParseGuids(value, "CarIds");
+ }
+ }
+
+ ///
+ /// List of available client IDs as strings (for configuration binding).
+ ///
+ public List ClientIds
+ {
+ get => _clientIdStrings ?? new();
+ set
+ {
+ _clientIdStrings = value;
+ _clientIds = ParseGuids(value, "ClientIds");
+ }
+ }
+
+ ///
+ /// List of available car IDs as GUIDs (pre-parsed for performance).
+ ///
+ public List CarIdGuids => _carIds ?? new();
+
+ ///
+ /// List of available client IDs as GUIDs (pre-parsed for performance).
+ ///
+ public List ClientIdGuids => _clientIds ?? new();
+
+ private static List ParseGuids(List? values, string fieldName)
+ {
+ if (values == null)
+ return new List();
+ var result = new List();
+ for (var i = 0; i < values.Count; i++)
+ {
+ var value = values[i];
+ if (string.IsNullOrWhiteSpace(value))
+ throw new FormatException($"{fieldName}[{i}] is null or empty");
+ if (!Guid.TryParse(value, out var guid))
+ throw new FormatException($"{fieldName}[{i}] has invalid GUID format: {value}");
+
+ result.Add(guid);
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Generator/Generation/RentGeneratorService.cs b/CarRental.Generator/Generation/RentGeneratorService.cs
new file mode 100644
index 000000000..dd5280ef7
--- /dev/null
+++ b/CarRental.Generator/Generation/RentGeneratorService.cs
@@ -0,0 +1,37 @@
+using CarRental.Application.Contracts.Rent;
+using Microsoft.Extensions.Options;
+using Bogus;
+
+namespace CarRental.Generator.Generation;
+
+///
+/// Service for generating fake rental contract data
+///
+/// Generator configuration options monitor
+public class RentGeneratorService(IOptionsMonitor optionsMonitor)
+{
+ ///
+ /// Generates a specified number of fake rental contracts
+ ///
+ /// Number of contracts to generate
+ /// List of generated rental DTOs
+ public IList GenerateContract(int count)
+ {
+ var options = optionsMonitor.CurrentValue;
+
+ if (!options.CarIdGuids.Any())
+ throw new InvalidOperationException("CarIds list is empty. Check configuration.");
+
+ if (!options.ClientIdGuids.Any())
+ throw new InvalidOperationException("ClientIds list is empty. Check configuration.");
+
+ var generatedRents = new Faker().CustomInstantiator(f => new RentCreateUpdateDto(
+ StartDateTime: f.Date.Soon(1),
+ Duration: f.Random.Double(1, 100),
+ CarId: f.PickRandom(options.CarIdGuids),
+ ClientId: f.PickRandom(options.ClientIdGuids)
+ ));
+
+ return generatedRents.Generate(count);
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Generator/KafkaProducer.cs b/CarRental.Generator/KafkaProducer.cs
new file mode 100644
index 000000000..224754060
--- /dev/null
+++ b/CarRental.Generator/KafkaProducer.cs
@@ -0,0 +1,105 @@
+using Confluent.Kafka;
+using Microsoft.Extensions.Options;
+using CarRental.Application.Contracts.Rent;
+using System.Text.Json;
+
+namespace CarRental.Generator;
+
+///
+/// Kafka producer that serializes into JSON
+/// and publishes messages to a configured topic
+///
+/// Logger instance
+/// Kafka producer
+/// Producer settings
+public class KafkaProducer(
+ ILogger logger,
+ IProducer producer,
+ IOptions options)
+{
+ private readonly KafkaProducerSettings _settings = options.Value
+ ?? throw new InvalidOperationException("KafkaProducerSettings must be configured.");
+
+ ///
+ /// Sends a rent DTO as a JSON message to Kafka
+ ///
+ /// Rent DTO to send
+ /// Cancellation token
+ public async Task Produce(RentCreateUpdateDto dto, CancellationToken cancellationToken = default)
+ {
+ var payload = JsonSerializer.Serialize(dto);
+
+ for (var attempt = 1; attempt <= _settings.MaxProduceAttempts; attempt++)
+ {
+ try
+ {
+ var result = await producer.ProduceAsync(_settings.TopicName, new Message { Value = payload }, cancellationToken);
+
+ logger.LogInformation(
+ "Kafka message produced successfully. Topic={Topic}, Partition={Partition}, Offset={Offset}, " +
+ "CarId={CarId}, ClientId={ClientId}, StartDateTime={StartDateTime}, Duration={Duration}",
+ result.Topic, result.Partition.Value, result.Offset.Value,
+ dto.CarId, dto.ClientId, dto.StartDateTime, dto.Duration);
+
+ return;
+ }
+ catch (ProduceException ex) when (attempt < _settings.MaxProduceAttempts)
+ {
+ logger.LogWarning(ex,
+ "Kafka produce attempt {Attempt}/{MaxAttempts} failed. Reason={Reason}. Retrying in {Delay}ms...",
+ attempt, _settings.MaxProduceAttempts, ex.Error.Reason, _settings.RetryDelayMs);
+
+ await Task.Delay(_settings.RetryDelayMs, cancellationToken);
+ }
+ catch (ProduceException ex)
+ {
+ logger.LogError(ex,
+ "Kafka produce failed after {MaxAttempts} attempts. Reason={Reason}. CarId={CarId}, ClientId={ClientId}",
+ _settings.MaxProduceAttempts, ex.Error.Reason, dto.CarId, dto.ClientId);
+
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// Sends a batch of rent DTOs as JSON messages to Kafka with controlled parallelism
+ ///
+ /// Rent DTOs to send
+ /// Cancellation token
+ public async Task ProduceMany(IList dtos, CancellationToken cancellationToken = default)
+ {
+ if (!dtos.Any())
+ {
+ logger.LogWarning("No rent DTOs to produce to Kafka.");
+ return;
+ }
+
+ logger.LogInformation("Starting to produce {Count} rent messages to Kafka topic: {Topic} with parallelism {MaxParallelism}",
+ dtos.Count, _settings.TopicName, _settings.MaxParallelism);
+
+ using var semaphore = new SemaphoreSlim(_settings.MaxParallelism);
+ var tasks = new List();
+
+ foreach (var dto in dtos)
+ {
+ await semaphore.WaitAsync(cancellationToken);
+
+ tasks.Add(Task.Run(async () =>
+ {
+ try
+ {
+ await Produce(dto, cancellationToken);
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }, cancellationToken));
+ }
+
+ await Task.WhenAll(tasks);
+
+ logger.LogInformation("Successfully produced all {Count} rent messages to Kafka", dtos.Count);
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Generator/KafkaProducerSettings.cs b/CarRental.Generator/KafkaProducerSettings.cs
new file mode 100644
index 000000000..65160b56c
--- /dev/null
+++ b/CarRental.Generator/KafkaProducerSettings.cs
@@ -0,0 +1,27 @@
+namespace CarRental.Generator;
+
+///
+/// Kafka settings used by the CarRental Kafka producer host
+///
+public class KafkaProducerSettings
+{
+ ///
+ /// Kafka topic name used for producing messages
+ ///
+ public string TopicName { get; init; } = "car-rentals";
+
+ ///
+ /// Maximum number of attempts to send a message
+ ///
+ public int MaxProduceAttempts { get; init; } = 5;
+
+ ///
+ /// Delay between produce retries in milliseconds
+ ///
+ public int RetryDelayMs { get; init; } = 1000;
+
+ ///
+ /// Maximum number of parallel produce operations
+ ///
+ public int MaxParallelism { get; init; } = 10;
+}
\ No newline at end of file
diff --git a/CarRental.Generator/Program.cs b/CarRental.Generator/Program.cs
new file mode 100644
index 000000000..572ce8d9a
--- /dev/null
+++ b/CarRental.Generator/Program.cs
@@ -0,0 +1,48 @@
+using CarRental.Generator;
+using CarRental.Generator.Generation;
+using CarRental.ServiceDefaults;
+using Confluent.Kafka;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.Configure(builder.Configuration.GetSection("Generator"));
+
+builder.AddServiceDefaults();
+
+builder.AddKafkaProducer("car-rental-kafka");
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddControllers();
+builder.Services.AddAuthorization();
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("CarRental"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+app.MapDefaultEndpoints();
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
\ No newline at end of file
diff --git a/CarRental.Generator/Properties/launchSettings.json b/CarRental.Generator/Properties/launchSettings.json
new file mode 100644
index 000000000..fe877566f
--- /dev/null
+++ b/CarRental.Generator/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:52170",
+ "sslPort": 44384
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5112",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7160;http://localhost:5112",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRental.Generator/appsettings.Development.json b/CarRental.Generator/appsettings.Development.json
new file mode 100644
index 000000000..770d3e931
--- /dev/null
+++ b/CarRental.Generator/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "DetailedErrors": true,
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental.Generator/appsettings.json b/CarRental.Generator/appsettings.json
new file mode 100644
index 000000000..4a1cde249
--- /dev/null
+++ b/CarRental.Generator/appsettings.json
@@ -0,0 +1,55 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Generator": {
+ "CarIds": [
+ "550e8400-e29b-41d4-a716-446655440001",
+ "550e8400-e29b-41d4-a716-446655440002",
+ "550e8400-e29b-41d4-a716-446655440003",
+ "550e8400-e29b-41d4-a716-446655440004",
+ "550e8400-e29b-41d4-a716-446655440005",
+ "550e8400-e29b-41d4-a716-446655440006",
+ "550e8400-e29b-41d4-a716-446655440007",
+ "550e8400-e29b-41d4-a716-446655440008",
+ "550e8400-e29b-41d4-a716-446655440009",
+ "550e8400-e29b-41d4-a716-446655440010",
+ "550e8400-e29b-41d4-a716-446655440011",
+ "550e8400-e29b-41d4-a716-446655440012",
+ "550e8400-e29b-41d4-a716-446655440013",
+ "550e8400-e29b-41d4-a716-446655440014",
+ "550e8400-e29b-41d4-a716-446655440015",
+ "550e8400-e29b-41d4-a716-446655440016",
+ "550e8400-e29b-41d4-a716-446655440017",
+ "550e8400-e29b-41d4-a716-446655440018",
+ "550e8400-e29b-41d4-a716-446655440019",
+ "550e8400-e29b-41d4-a716-446655440020"
+ ],
+ "ClientIds": [
+ "c11e4400-e29b-41d4-a716-446655440001",
+ "c11e4400-e29b-41d4-a716-446655440002",
+ "c11e4400-e29b-41d4-a716-446655440003",
+ "c11e4400-e29b-41d4-a716-446655440004",
+ "c11e4400-e29b-41d4-a716-446655440005",
+ "c11e4400-e29b-41d4-a716-446655440006",
+ "c11e4400-e29b-41d4-a716-446655440007",
+ "c11e4400-e29b-41d4-a716-446655440008",
+ "c11e4400-e29b-41d4-a716-446655440009",
+ "c11e4400-e29b-41d4-a716-446655440010",
+ "c11e4400-e29b-41d4-a716-446655440011",
+ "c11e4400-e29b-41d4-a716-446655440012",
+ "c11e4400-e29b-41d4-a716-446655440013",
+ "c11e4400-e29b-41d4-a716-446655440014",
+ "c11e4400-e29b-41d4-a716-446655440015",
+ "c11e4400-e29b-41d4-a716-446655440016",
+ "c11e4400-e29b-41d4-a716-446655440017",
+ "c11e4400-e29b-41d4-a716-446655440018",
+ "c11e4400-e29b-41d4-a716-446655440019",
+ "c11e4400-e29b-41d4-a716-446655440020"
+ ]
+ }
+}
diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj
new file mode 100644
index 000000000..81047227d
--- /dev/null
+++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj
@@ -0,0 +1,19 @@
+
+
+ net8.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs
new file mode 100644
index 000000000..f429c75fe
--- /dev/null
+++ b/CarRental.Infrastructure/CarRentalDbContext.cs
@@ -0,0 +1,84 @@
+using Microsoft.EntityFrameworkCore;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.InternalData.ComponentClasses;
+using MongoDB.EntityFrameworkCore.Extensions;
+
+namespace CarRental.Infrastructure;
+///
+/// Database context for managing car rental entities in MongoDB
+///
+/// The options to be used by the DbContext
+public class CarRentalDbContext(DbContextOptions options) : DbContext(options)
+{
+ ///
+ /// Gets the collection of cars
+ ///
+ public DbSet Cars { get; init; }
+
+ ///
+ /// Gets the collection of clients
+ ///
+ public DbSet Clients { get; init; }
+
+ ///
+ /// Gets the collection of rent records
+ ///
+ public DbSet Rents { get; init; }
+
+ ///
+ /// Gets the collection of car models
+ ///
+ public DbSet CarModels { get; init; }
+
+ ///
+ /// Gets the collection of car model generations
+ ///
+ public DbSet ModelGenerations { get; init; }
+
+ ///
+ /// Configures the database schema and maps entities to MongoDB collections
+ ///
+ /// The builder being used to construct the model for this context
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("cars");
+ builder.HasKey(c => c.Id);
+ builder.Property(c => c.Id).HasElementName("_id");
+ });
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("clients");
+ builder.HasKey(cl => cl.Id);
+ builder.Property(cl => cl.Id).HasElementName("_id");
+ });
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("rents");
+ builder.HasKey(r => r.Id);
+ builder.Property(r => r.Id).HasElementName("_id");
+ builder.Property(r => r.CarId).HasElementName("car_id");
+ builder.Property(r => r.ClientId).HasElementName("client_id");
+ });
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("car_models");
+ builder.HasKey(m => m.Id);
+ builder.Property(m => m.Id).HasElementName("_id");
+ });
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("model_generations");
+ builder.HasKey(g => g.Id);
+ builder.Property(g => g.Id).HasElementName("_id");
+ });
+ }
+}
diff --git a/CarRental.Infrastructure/DbInitializer.cs b/CarRental.Infrastructure/DbInitializer.cs
new file mode 100644
index 000000000..5925fa8d2
--- /dev/null
+++ b/CarRental.Infrastructure/DbInitializer.cs
@@ -0,0 +1,71 @@
+using CarRental.Domain.DataSeed;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace CarRental.Infrastructure;
+
+///
+/// Performs a conditional data seed by checking if the database is empty
+/// and, if so, populating it with a predefined set of entities (from models to rents);
+/// It ensures the system has necessary initial data while maintaining referential integrity through sequential updates
+///
+public static class DbInitializer
+{
+ ///
+ /// Asynchronously seeds the database with initial car rental data if the CarModels table is empty
+ ///
+ /// The database context instance used to persist the seed data
+ /// The logger instance for capturing diagnostic information
+ /// A task representing the asynchronous seeding operation
+ public static async Task SeedData(CarRentalDbContext context, ILogger logger)
+ {
+ try
+ {
+ if (await context.CarModels.AnyAsync())
+ {
+ logger.LogInformation("The database if already filled");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, "Connection with database is not establised");
+ }
+
+ using var transaction = await context.Database.BeginTransactionAsync();
+ try
+ {
+ var data = new DataSeed();
+ logger.LogInformation("The process of database's filling is starting...");
+
+ await context.CarModels.AddRangeAsync(data.Models);
+ await context.SaveChangesAsync();
+ logger.LogInformation("Car models were successfully uploaded");
+
+ await context.ModelGenerations.AddRangeAsync(data.Generations);
+ await context.SaveChangesAsync();
+ logger.LogInformation("Model's generations were successfully uploaded");
+
+ await context.Cars.AddRangeAsync(data.Cars);
+ await context.SaveChangesAsync();
+ logger.LogInformation("Cars were successfully uploaded");
+
+ await context.Clients.AddRangeAsync(data.Clients);
+ await context.SaveChangesAsync();
+ logger.LogInformation("Clients were successfully uploaded");
+
+ await context.Rents.AddRangeAsync(data.Rents);
+ await context.SaveChangesAsync();
+ logger.LogInformation("Rents were successfully uploaded");
+
+ await transaction.CommitAsync();
+ logger.LogInformation("Database was successfully initialized!");
+ }
+ catch (Exception ex)
+ {
+ await transaction.RollbackAsync();
+ logger.LogError(ex, "The problem with filling database. Check logs for more information");
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs
new file mode 100644
index 000000000..4d08feb20
--- /dev/null
+++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs
@@ -0,0 +1,22 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.DataSeed;
+
+namespace CarRental.Infrastructure.InMemoryRepository;
+
+///
+/// Repository for managing CarModelGeneration entities
+/// Provides data access for vehicle model's generation (e.g., year) using the BaseRepository
+///
+public class CarModelGenerationRepository(DataSeed data) : BaseRepository(data.Generations)
+{
+ ///
+ /// Gets the unique identifier from the specified CarModelGeneration entity
+ ///
+ protected override Guid GetEntityId(CarModelGeneration generation) => generation.Id;
+
+ ///
+ /// Sets the unique identifier for the specified CarModelGeneration entity
+ ///
+ protected override void SetEntityId(CarModelGeneration generation, Guid id) => generation.Id = id;
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs
new file mode 100644
index 000000000..3ec4a9b7d
--- /dev/null
+++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs
@@ -0,0 +1,22 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.DataSeed;
+
+namespace CarRental.Infrastructure.InMemoryRepository;
+
+///
+/// Repository for managing CarModel entities
+/// Provides data access for vehicle models (e.g., model name) using the BaseRepository
+///
+public class CarModelRepository(DataSeed data) : BaseRepository(data.Models)
+{
+ ///
+ /// Gets the unique identifier from the specified CarModel entity
+ ///
+ protected override Guid GetEntityId(CarModel model) => model.Id;
+
+ ///
+ /// Sets the unique identifier for the specified CarModel entity
+ ///
+ protected override void SetEntityId(CarModel model, Guid id) => model.Id = id;
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs
new file mode 100644
index 000000000..129b3e8c9
--- /dev/null
+++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs
@@ -0,0 +1,22 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.DataSeed;
+
+namespace CarRental.Infrastructure.InMemoryRepository;
+
+///
+/// Repository for the Car entity
+/// Inherits BaseRepository for in-memory CRUD operations
+///
+public class CarRepository(DataSeed data) : BaseRepository(data.Cars)
+{
+ ///
+ /// Gets the unique identifier from the specified Car entity
+ ///
+ protected override Guid GetEntityId(Car car) => car.Id;
+
+ ///
+ /// Sets the unique identifier for the specified Car entity
+ ///
+ protected override void SetEntityId(Car car, Guid id) => car.Id = id;
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs
new file mode 100644
index 000000000..d9bf76d86
--- /dev/null
+++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs
@@ -0,0 +1,22 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.DataSeed;
+
+namespace CarRental.Infrastructure.InMemoryRepository;
+
+///
+/// Repository for the Client entity
+/// Inherits BaseRepository for in-memory CRUD operations
+///
+public class ClientRepository(DataSeed data) : BaseRepository(data.Clients)
+{
+ ///
+ /// Gets the unique identifier from the specified Client entity
+ ///
+ protected override Guid GetEntityId(Client client) => client.Id;
+
+ ///
+ /// Sets the unique identifier for the specified Client entity
+ ///
+ protected override void SetEntityId(Client client, Guid id) => client.Id = id;
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs
new file mode 100644
index 000000000..843f61603
--- /dev/null
+++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs
@@ -0,0 +1,22 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.DataSeed;
+
+namespace CarRental.Infrastructure.InMemoryRepository;
+
+///
+/// Repository for the Rent entity
+/// Inherits BaseRepository for in-memory CRUD operations
+///
+public class RentRepository(DataSeed data) : BaseRepository(data.Rents)
+{
+ ///
+ /// Gets the unique identifier from the specified Rent entity
+ ///
+ protected override Guid GetEntityId(Rent rent) => rent.Id;
+
+ ///
+ /// Sets the unique identifier for the specified Rent entity
+ ///
+ protected override void SetEntityId(Rent rent, Guid id) => rent.Id = id;
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/Kafka/Consumer.cs b/CarRental.Infrastructure/Kafka/Consumer.cs
new file mode 100644
index 000000000..482ab5379
--- /dev/null
+++ b/CarRental.Infrastructure/Kafka/Consumer.cs
@@ -0,0 +1,163 @@
+using Confluent.Kafka;
+using CarRental.Application.Contracts.Rent;
+using CarRental.Application.Contracts.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System.Text.Json;
+
+namespace CarRental.Infrastructure.Kafka;
+
+///
+/// Background service that consumes rent messages from Kafka and saves them to database
+///
+/// Logger for recording operations
+/// Kafka consumer instance
+/// Factory for creating service scopes
+/// Consumer configuration settings
+public class Consumer(
+ ILogger logger,
+ IConsumer consumer,
+ IServiceScopeFactory scopeFactory,
+ IOptions options) : BackgroundService
+{
+ private readonly ConsumerSettings _settings = options.Value;
+
+ ///
+ /// Main execution loop that continuously consumes and processes messages from Kafka
+ ///
+ /// Cancellation token to stop the consumer
+ /// Task representing the asynchronous operation
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ try
+ {
+ consumer.Subscribe(_settings.TopicName);
+ logger.LogInformation("KafkaConsumer started on topic {TopicName}", _settings.TopicName);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to subscribe to topic {TopicName}", _settings.TopicName);
+ return;
+ }
+
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ ConsumeResult? message = null;
+
+ try
+ {
+ message = consumer.Consume(TimeSpan.FromMilliseconds(_settings.ConsumeTimeoutMs));
+
+ if (message is null)
+ continue;
+
+ if (message.IsPartitionEOF || message.Message == null)
+ {
+ logger.LogDebug("Reached end of partition or empty message");
+ continue;
+ }
+
+ var payload = message.Message?.Value;
+
+ if (string.IsNullOrWhiteSpace(payload))
+ {
+ logger.LogWarning("Empty payload. Topic={Topic}, Offset={Offset}",
+ message.Topic, message.Offset.Value);
+ continue;
+ }
+
+ RentCreateUpdateDto? dto = null;
+
+ for (var attempt = 1; attempt <= _settings.MaxDeserializeAttempts; attempt++)
+ {
+ try
+ {
+ dto = JsonSerializer.Deserialize(payload);
+ break;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Deserialization attempt {Attempt} failed", attempt);
+ }
+ }
+
+ if (dto == null)
+ {
+ logger.LogError("Failed to deserialize after {MaxAttempts} attempts",
+ _settings.MaxDeserializeAttempts);
+ CommitIfNeeded(message);
+ continue;
+ }
+
+ using var scope = scopeFactory.CreateScope();
+ var rentService = scope.ServiceProvider.GetRequiredService>();
+
+ var savedRent = await rentService.Create(dto);
+
+ CommitIfNeeded(message);
+ logger.LogInformation("Saved rent with Id: {RentId} from Kafka", savedRent.Id);
+ }
+ catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart)
+ {
+ logger.LogWarning("Topic not available, retrying...");
+ await Task.Delay(1000, stoppingToken);
+ }
+ catch (ConsumeException ex)
+ {
+ logger.LogError(ex, "Consume error: {Reason}", ex.Error.Reason);
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("KafkaConsumer stopping");
+ break;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unexpected error");
+ }
+ }
+ }
+ finally
+ {
+ try
+ {
+ consumer.Unsubscribe();
+ consumer.Close();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error closing consumer");
+ }
+ logger.LogInformation("KafkaConsumer stopped");
+ }
+ }
+
+ ///
+ /// Commits the message offset if auto-commit is disabled
+ ///
+ /// The consumed message to commit
+ private void CommitIfNeeded(ConsumeResult message)
+ {
+ if (_settings.AutoCommitEnabled)
+ return;
+
+ if (message == null || message.Message == null || message.IsPartitionEOF)
+ {
+ logger.LogDebug("Skipping commit for null/EOF message");
+ return;
+ }
+
+ try
+ {
+ consumer.Commit(message);
+ }
+ catch (KafkaException ex)
+ {
+ logger.LogWarning(ex, "Commit failed. Offset={Offset}", message.Offset.Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/Kafka/ConsumerSettings.cs b/CarRental.Infrastructure/Kafka/ConsumerSettings.cs
new file mode 100644
index 000000000..033cdacaf
--- /dev/null
+++ b/CarRental.Infrastructure/Kafka/ConsumerSettings.cs
@@ -0,0 +1,33 @@
+namespace CarRental.Infrastructure.Kafka;
+
+///
+/// Kafka settings used by CarRental Kafka consumer
+///
+public class ConsumerSettings
+{
+ ///
+ /// Kafka topic name used for consuming messages
+ ///
+ public string TopicName { get; init; } = "car-rentals";
+
+ ///
+ /// Consumer group ID for Kafka
+ ///
+ public string GroupId { get; init; } = "car-rental-consumer-group";
+
+ ///
+ /// Enables Kafka auto-commit for the consumer.
+ /// If false, the consumer commits offsets manually after successful processing
+ ///
+ public bool AutoCommitEnabled { get; init; } = false;
+
+ ///
+ /// Poll timeout for consuming messages in milliseconds
+ ///
+ public int ConsumeTimeoutMs { get; init; } = 5000;
+
+ ///
+ /// Maximum number of attempts to deserialize a message payload
+ ///
+ public int MaxDeserializeAttempts { get; init; } = 3;
+}
diff --git a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs
new file mode 100644
index 000000000..0b6fea4ec
--- /dev/null
+++ b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs
@@ -0,0 +1,82 @@
+using Microsoft.EntityFrameworkCore;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Infrastructure.Repository;
+
+///
+/// Repository for managing car model generations in the database
+///
+/// The database context for car rental data
+public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseRepository
+{
+ ///
+ /// Retrieves all model generations with their associated car models
+ ///
+ /// A list of all model generation entities
+ public async Task> ReadAll()
+ {
+ var generations = await context.ModelGenerations.ToListAsync();
+ var modelIds = generations.Select(g => g.ModelId).Distinct().ToList();
+ var models = await context.CarModels
+ .Where(m => modelIds.Contains(m.Id))
+ .ToListAsync();
+ foreach (var generation in generations)
+ {
+ generation.Model = models.FirstOrDefault(m => m.Id == generation.ModelId);
+ }
+ return generations;
+ }
+
+ ///
+ /// Finds a specific model generation by id and loads its associated car model
+ ///
+ /// The unique identifier of the generation
+ /// The generation entity if found; otherwise, null
+ public async Task Read(Guid id)
+ {
+ var entity = await context.ModelGenerations.FirstOrDefaultAsync(x => x.Id == id);
+ if (entity != null)
+ {
+ entity.Model = await context.CarModels.FirstOrDefaultAsync(m => m.Id == entity.ModelId);
+ }
+ return entity;
+ }
+
+ ///
+ /// Adds a new model generation to the database
+ ///
+ /// The model generation data to persist
+ /// The unique identifier of the created generation
+ public async Task Create(CarModelGeneration entity)
+ {
+ await context.ModelGenerations.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity.Id;
+ }
+
+ ///
+ /// Updates an existing model generation record.
+ ///
+ /// The updated generation entity
+ /// The identifier of the generation to update
+ /// True if the changes were saved successfully; otherwise, false
+ public async Task Update(CarModelGeneration entity, Guid id)
+ {
+ context.ModelGenerations.Update(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+
+ ///
+ /// Removes a model generation from the database by its identifier
+ ///
+ /// The unique identifier of the generation to delete
+ /// True if the deletion was successful; otherwise, false
+ public async Task Delete(Guid id)
+ {
+ var entity = await Read(id);
+ if (entity == null) return false;
+ context.ModelGenerations.Remove(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs
new file mode 100644
index 000000000..43ff6798c
--- /dev/null
+++ b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs
@@ -0,0 +1,63 @@
+using Microsoft.EntityFrameworkCore;
+using CarRental.Domain.InternalData.ComponentClasses;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Infrastructure.Repository;
+
+///
+/// Repository for managing car model entities in the database
+///
+/// The database context for car rental data
+public class DbCarModelRepository(CarRentalDbContext context) : IBaseRepository
+{
+ ///
+ /// Retrieves all car models from the database
+ ///
+ /// A list of all car model entities
+ public async Task> ReadAll() => await context.CarModels.ToListAsync();
+
+ ///
+ /// Finds a specific car model by its unique identifier
+ ///
+ /// The unique identifier of the car model
+ /// The car model entity if found; otherwise, null
+ public async Task Read(Guid id) =>
+ (await context.CarModels.ToListAsync()).FirstOrDefault(x => x.Id == id);
+
+ ///
+ /// Adds a new car model to the database
+ ///
+ /// The car model data to persist
+ /// The unique identifier of the created car model
+ public async Task Create(CarModel entity)
+ {
+ await context.CarModels.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity.Id;
+ }
+
+ ///
+ /// Updates an existing car model record
+ ///
+ /// The updated car model entity
+ /// The identifier of the car model to update
+ /// True if the changes were saved successfully; otherwise, false
+ public async Task Update(CarModel entity, Guid id)
+ {
+ context.CarModels.Update(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+
+ ///
+ /// Removes a car model from the database by its identifier
+ ///
+ /// The unique identifier of the car model to delete
+ /// True if the car model was deleted successfully; otherwise, false
+ public async Task Delete(Guid id)
+ {
+ var entity = await Read(id);
+ if (entity is null) return false;
+ context.CarModels.Remove(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/Repository/DbCarRepository.cs b/CarRental.Infrastructure/Repository/DbCarRepository.cs
new file mode 100644
index 000000000..ee7edb98e
--- /dev/null
+++ b/CarRental.Infrastructure/Repository/DbCarRepository.cs
@@ -0,0 +1,63 @@
+using Microsoft.EntityFrameworkCore;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Infrastructure.Repository;
+
+///
+/// Repository for managing car entities in the database
+///
+/// The database context for car rental data
+public class DbCarRepository(CarRentalDbContext context) : IBaseRepository
+{
+ ///
+ /// Retrieves all cars from the database
+ ///
+ /// A list of all car entities
+ public async Task> ReadAll() => await context.Cars.ToListAsync();
+
+ ///
+ /// Finds a specific car by its unique identifier
+ ///
+ /// The unique identifier of the car
+ /// The car entity if found; otherwise, null
+ public async Task Read(Guid id) =>
+ (await context.Cars.ToListAsync()).FirstOrDefault(x => x.Id == id);
+
+ ///
+ /// Adds a new car to the database
+ ///
+ /// The car data to persist
+ /// The unique identifier of the created car
+ public async Task Create(Car entity)
+ {
+ await context.Cars.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity.Id;
+ }
+
+ ///
+ /// Updates an existing car record
+ ///
+ /// The updated car entity
+ /// The identifier of the car to update
+ /// True if the changes were saved successfully; otherwise, false
+ public async Task Update(Car entity, Guid id)
+ {
+ context.Cars.Update(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+
+ ///
+ /// Removes a car from the database by its identifier
+ ///
+ /// The unique identifier of the car to delete
+ /// True if the car was deleted successfully; otherwise, false
+ public async Task Delete(Guid id)
+ {
+ var entity = await Read(id);
+ if (entity is null) return false;
+ context.Cars.Remove(entity);
+ return await context.SaveChangesAsync() > 0;
+ }
+}
\ No newline at end of file
diff --git a/CarRental.Infrastructure/Repository/DbClientRepository.cs b/CarRental.Infrastructure/Repository/DbClientRepository.cs
new file mode 100644
index 000000000..e974b6e9c
--- /dev/null
+++ b/CarRental.Infrastructure/Repository/DbClientRepository.cs
@@ -0,0 +1,63 @@
+using Microsoft.EntityFrameworkCore;
+using CarRental.Domain.DataModels;
+using CarRental.Domain.Interfaces;
+
+namespace CarRental.Infrastructure.Repository;
+
+///