diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml
new file mode 100644
index 000000000..513be352c
--- /dev/null
+++ b/.github/workflows/dotnet-tests.yml
@@ -0,0 +1,28 @@
+name: .NET Tests
+
+on:
+ push:
+ branches: ["main", "lab2"]
+ pull_request:
+ branches: ["main", "lab2"]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - 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: Test
+ run: dotnet test --no-build --verbosity normal --configuration Release
diff --git a/CarRentalService.Api/CarRentalService.Api.csproj b/CarRentalService.Api/CarRentalService.Api.csproj
new file mode 100644
index 000000000..7d29e4e19
--- /dev/null
+++ b/CarRentalService.Api/CarRentalService.Api.csproj
@@ -0,0 +1,31 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+ CarRentalService.Api.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.Api/Configuration/RentalGeneratorOptions.cs b/CarRentalService.Api/Configuration/RentalGeneratorOptions.cs
new file mode 100644
index 000000000..7a8a3ae96
--- /dev/null
+++ b/CarRentalService.Api/Configuration/RentalGeneratorOptions.cs
@@ -0,0 +1,29 @@
+namespace CarRentalService.Api.Configuration;
+
+///
+/// Configuration options for the rental generator service
+///
+public class RentalGeneratorOptions
+{
+ public const string SectionName = "RentalGenerator";
+
+ ///
+ /// Default number of rental contracts to generate
+ ///
+ public int Count { get; set; } = 100;
+
+ ///
+ /// Batch size for streaming rental contracts
+ ///
+ public int BatchSize { get; set; } = 10;
+
+ ///
+ /// Retry delay in seconds for reconnection attempts
+ ///
+ public int RetryDelay { get; set; } = 5;
+
+ ///
+ /// gRPC service address for the rental generator
+ ///
+ public string? GrpcAddress { get; set; }
+}
diff --git a/CarRentalService.Api/Controllers/AnalyticsController.cs b/CarRentalService.Api/Controllers/AnalyticsController.cs
new file mode 100644
index 000000000..fdc79c785
--- /dev/null
+++ b/CarRentalService.Api/Controllers/AnalyticsController.cs
@@ -0,0 +1,93 @@
+using CarRentalService.Application.Contracts;
+using CarRentalService.Application.Contracts.Analytics;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for analytics and business intelligence endpoints
+///
+/// Analytics service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase
+{
+ ///
+ /// Retrieves customers who rented cars of a specific model name
+ ///
+ /// Car model name to filter by
+ /// List of customer names
+ [HttpGet("clients-by-model-name")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetClientsByModelName([FromQuery] string modelName)
+ {
+ var result = await analyticsService.ReadCustomersByModelName(modelName);
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves customers who rented cars of a specific model ID
+ ///
+ /// Car model ID to filter by
+ /// List of customer names
+ [HttpGet("clients-by-model-id/{modelId:int}")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetClientsByModelId(int modelId)
+ {
+ var result = await analyticsService.ReadCustomersByModelId(modelId);
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves cars currently in rent at a specific time
+ ///
+ /// Time to check for active rentals (default: current time)
+ /// List of currently rented cars
+ [HttpGet("cars-in-rent")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetCarsInRent([FromQuery] DateTime? atTime = null)
+ {
+ var checkTime = atTime ?? DateTime.Now;
+ var result = await analyticsService.ReadCarsInRent(checkTime);
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves top N most frequently rented cars
+ ///
+ /// Number of top cars to return (default: 5)
+ /// List of top rented cars with statistics
+ [HttpGet("top-rented-cars")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetTopRentedCars([FromQuery] int count = 5)
+ {
+ var result = await analyticsService.ReadTopMostRentedCars(count);
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves rental count for all cars
+ ///
+ /// List of all cars with their rental counts
+ [HttpGet("all-cars-with-rental-count")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAllCarsWithRentalCount()
+ {
+ var result = await analyticsService.ReadAllCarsWithRentalCount();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves top N customers by total rental revenue
+ ///
+ /// Number of top customers to return (default: 5)
+ /// List of top customers by revenue
+ [HttpGet("top-customers-by-revenue")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetTopCustomersByRevenue([FromQuery] int count = 5)
+ {
+ var result = await analyticsService.ReadTopCustomersByTotalAmount(count);
+ return Ok(result);
+ }
+}
diff --git a/CarRentalService.Api/Controllers/CarModelGenerationsController.cs b/CarRentalService.Api/Controllers/CarModelGenerationsController.cs
new file mode 100644
index 000000000..c1cdce486
--- /dev/null
+++ b/CarRentalService.Api/Controllers/CarModelGenerationsController.cs
@@ -0,0 +1,106 @@
+using CarRentalService.Application.Contracts.CarModelGeneration;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing car model generations
+///
+/// Car model generation service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class CarModelGenerationsController(ICarModelGenerationService service) : ControllerBase
+{
+ ///
+ /// Retrieves all car model generations
+ ///
+ /// List of all car model generations
+ [HttpGet]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves a specific car model generation by ID
+ ///
+ /// Car model generation identifier
+ /// Car model generation details
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(typeof(CarModelGenerationDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var generation = await service.Get(id);
+ if (generation == null)
+ {
+ return NotFound($"CarModelGeneration with ID {id} not found.");
+ }
+ return Ok(generation);
+ }
+
+ ///
+ /// Creates a new car model generation
+ ///
+ /// Car model generation creation data
+ /// Created car model generation
+ [HttpPost]
+ [ProducesResponseType(typeof(CarModelGenerationDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] CarModelGenerationCreateUpdateDto dto)
+ {
+ try
+ {
+ var createdGeneration = await service.Create(dto);
+ return CreatedAtAction(nameof(GetById), new { id = createdGeneration.Id }, createdGeneration);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid car model generation data: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Updates an existing car model generation
+ ///
+ /// Car model generation identifier
+ /// Car model generation update data
+ /// No content on success
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task Update(int id, [FromBody] CarModelGenerationCreateUpdateDto dto)
+ {
+ try
+ {
+ await service.Update(dto, id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound($"CarModelGeneration with ID {id} not found.");
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Deletes a car model generation
+ ///
+ /// Car model generation identifier
+ /// No content on success
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var result = await service.Delete(id);
+ return result ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRentalService.Api/Controllers/CarModelsController.cs b/CarRentalService.Api/Controllers/CarModelsController.cs
new file mode 100644
index 000000000..4a7266c6c
--- /dev/null
+++ b/CarRentalService.Api/Controllers/CarModelsController.cs
@@ -0,0 +1,120 @@
+using CarRentalService.Application.Contracts.CarModel;
+using CarRentalService.Application.Contracts.Cars;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing car models
+///
+/// Car model service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class CarModelsController(ICarModelService service) : ControllerBase
+{
+ ///
+ /// Retrieves all car models
+ ///
+ /// List of all car models
+ [HttpGet]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves a specific car model by ID
+ ///
+ /// Car model identifier
+ /// Car model details
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(typeof(CarModelDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var carModel = await service.Get(id);
+ if (carModel == null)
+ {
+ return NotFound($"CarModel with ID {id} not found.");
+ }
+ return Ok(carModel);
+ }
+
+ ///
+ /// Creates a new car model
+ ///
+ /// Car model creation data
+ /// Created car model
+ [HttpPost]
+ [ProducesResponseType(typeof(CarModelDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] CarModelCreateUpdateDto dto)
+ {
+ try
+ {
+ var createdCarModel = await service.Create(dto);
+ return CreatedAtAction(nameof(GetById), new { id = createdCarModel.Id }, createdCarModel);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid car model data: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Updates an existing car model
+ ///
+ /// Car model identifier
+ /// Car model update data
+ /// No content on success
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task Update(int id, [FromBody] CarModelCreateUpdateDto dto)
+ {
+ try
+ {
+ await service.Update(dto, id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound($"CarModel with ID {id} not found.");
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Deletes a car model
+ ///
+ /// Car model identifier
+ /// No content on success
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var result = await service.Delete(id);
+ return result ? NoContent() : NotFound();
+ }
+
+ ///
+ /// Retrieves all cars associated with a specific car model
+ ///
+ /// Car model identifier
+ /// List of cars belonging to the specified model
+ [HttpGet("{id:int}/cars")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetCarsByModel(int id)
+ {
+ var result = await service.GetCarsByModelAsync(id);
+ return Ok(result);
+ }
+}
diff --git a/CarRentalService.Api/Controllers/CarsController.cs b/CarRentalService.Api/Controllers/CarsController.cs
new file mode 100644
index 000000000..63b72bfe4
--- /dev/null
+++ b/CarRentalService.Api/Controllers/CarsController.cs
@@ -0,0 +1,106 @@
+using CarRentalService.Application.Contracts.Cars;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing cars
+///
+/// Car service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class CarsController(ICarService service) : ControllerBase
+{
+ ///
+ /// Retrieves all cars
+ ///
+ /// List of all cars
+ [HttpGet]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves a specific car by ID
+ ///
+ /// Car identifier
+ /// Car details
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(typeof(CarDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var car = await service.Get(id);
+ if (car == null)
+ {
+ return NotFound($"Car with ID {id} not found.");
+ }
+ return Ok(car);
+ }
+
+ ///
+ /// Creates a new car
+ ///
+ /// Car creation data
+ /// Created car
+ [HttpPost]
+ [ProducesResponseType(typeof(CarDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] CarCreateUpdateDto dto)
+ {
+ try
+ {
+ var createdCar = await service.Create(dto);
+ return CreatedAtAction(nameof(GetById), new { id = createdCar.Id }, createdCar);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid car data: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Updates an existing car
+ ///
+ /// Car identifier
+ /// Car update data
+ /// No content on success
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task Update(int id, [FromBody] CarCreateUpdateDto dto)
+ {
+ try
+ {
+ var result = await service.Update(dto, id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound($"Car with ID {id} not found.");
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Deletes a car
+ ///
+ /// Car identifier
+ /// No content on success
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var result = await service.Delete(id);
+ return result ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRentalService.Api/Controllers/ClientsController.cs b/CarRentalService.Api/Controllers/ClientsController.cs
new file mode 100644
index 000000000..95c8ef3e1
--- /dev/null
+++ b/CarRentalService.Api/Controllers/ClientsController.cs
@@ -0,0 +1,101 @@
+using CarRentalService.Application.Contracts.Clients;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing clients
+///
+/// Client service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class ClientsController(IClientService service) : ControllerBase
+{
+ ///
+ /// Retrieves all clients
+ ///
+ /// List of all clients
+ [HttpGet]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves a specific client by ID
+ ///
+ /// Client identifier
+ /// Client details
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(typeof(ClientDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var client = await service.Get(id);
+ if (client == null)
+ {
+ return NotFound($"Client with ID {id} not found.");
+ }
+ return Ok(client);
+ }
+
+ ///
+ /// Creates a new client
+ ///
+ /// Client creation data
+ /// Created client
+ [HttpPost]
+ [ProducesResponseType(typeof(ClientDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] ClientCreateUpdateDto dto)
+ {
+ try
+ {
+ var createdClient = await service.Create(dto);
+ return CreatedAtAction(nameof(GetById), new { id = createdClient.Id }, createdClient);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid client data: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Updates an existing client
+ ///
+ /// Client identifier
+ /// Client update data
+ /// No content on success
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task Update(int id, [FromBody] ClientCreateUpdateDto dto)
+ {
+ try
+ {
+ var result = await service.Update(dto, id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound($"Client with ID {id} not found.");
+ }
+ }
+
+ ///
+ /// Deletes a client
+ ///
+ /// Client identifier
+ /// No content on success
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var result = await service.Delete(id);
+ return result ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRentalService.Api/Controllers/GeneratorController.cs b/CarRentalService.Api/Controllers/GeneratorController.cs
new file mode 100644
index 000000000..f11a16630
--- /dev/null
+++ b/CarRentalService.Api/Controllers/GeneratorController.cs
@@ -0,0 +1,162 @@
+using CarRentalService.Application.Contracts.Grpc;
+using CarRentalService.Application.Contracts.Rents;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Application.Contracts.Clients;
+using Microsoft.AspNetCore.Mvc;
+using AutoMapper;
+using Grpc.Core;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing rental contract generation via gRPC
+///
+[ApiController]
+[Route("api/[controller]")]
+public class GeneratorController(
+ RentalIngestor.RentalIngestorClient grpcClient,
+ IRentService rentService,
+ ICarService carService,
+ IClientService clientService,
+ IMapper mapper,
+ ILogger logger) : ControllerBase
+{
+
+ ///
+ /// Gets current system status and statistics
+ ///
+ [HttpGet("status")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetStatus()
+ {
+ try
+ {
+ var cars = await carService.GetAll();
+ var clients = await clientService.GetAll();
+ var rents = await rentService.GetAll();
+
+ return Ok(new
+ {
+ CarsCount = cars.Count,
+ ClientsCount = clients.Count,
+ RentsCount = rents.Count,
+ GeneratorAddress = "https://localhost:7000",
+ Status = "The system is working",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { Error = ex.Message });
+ }
+ }
+
+ ///
+ /// Manually triggers test data generation via gRPC
+ ///
+ /// Number of rental contracts to generate
+ /// Batch size for streaming
+ [HttpPost("generate-test")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task GenerateTestData([FromQuery] int count = 10, [FromQuery] int batchSize = 5)
+ {
+ if (count <= 0 || batchSize <= 0)
+ return BadRequest("Count and batchSize must be greater than 0");
+
+ try
+ {
+ logger.LogInformation("Starting manual generation of {Count} contracts (batchSize={BatchSize})",
+ count, batchSize);
+
+ var requestId = Guid.NewGuid().ToString("N");
+ var generated = 0;
+ var saved = 0;
+
+ using var call = grpcClient.StreamRentals();
+
+ // Sending a request
+ await call.RequestStream.WriteAsync(new RentalGenerationRequest
+ {
+ RequestId = requestId,
+ Count = count,
+ BatchSize = batchSize
+ });
+ await call.RequestStream.CompleteAsync();
+
+ // Get and save data
+ await foreach (var batch in call.ResponseStream.ReadAllAsync())
+ {
+ if (batch.RequestId != requestId)
+ continue;
+
+ logger.LogInformation("Received batch: {BatchCount} contracts", batch.Rentals.Count);
+ generated += batch.Rentals.Count;
+
+ foreach (var rental in batch.Rentals)
+ {
+ try
+ {
+ var carExists = await carService.Get(rental.CarId) != null;
+ var clientExists = await clientService.Get(rental.CustomerId) != null;
+
+ if (!carExists || !clientExists)
+ {
+ logger.LogWarning(
+ "Skipping the contract: CarId={CarId} (exists={CarExists}), ClientId={ClientId} (exists={ClientExists})",
+ rental.CarId, carExists, rental.CustomerId, clientExists);
+ continue;
+ }
+
+ // Save
+ var dto = mapper.Map(rental);
+ await rentService.Create(dto);
+ saved++;
+
+ logger.LogDebug("Saving the contract: CarId={CarId}, ClientId={ClientId}",
+ rental.CarId, rental.CustomerId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error saving the contract");
+ }
+ }
+
+ if (batch.IsFinal)
+ break;
+ }
+
+ return Ok(new
+ {
+ RequestId = requestId,
+ Generated = generated,
+ Saved = saved,
+ Failed = generated - saved,
+ Message = "Generation completed successfully",
+ Timestamp = DateTime.UtcNow
+ });
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error in generating test data");
+ return StatusCode(500, new { Error = ex.Message });
+ }
+ }
+
+ ///
+ /// Tests gRPC client connection status
+ ///
+ [HttpGet("test-grpc-connection")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult TestGrpcConnection()
+ {
+ try
+ {
+ return Ok("gRPC client is registered and ready to work");
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Error gRPC: {ex.Message}");
+ }
+ }
+}
diff --git a/CarRentalService.Api/Controllers/RentsController.cs b/CarRentalService.Api/Controllers/RentsController.cs
new file mode 100644
index 000000000..ea49bb1ac
--- /dev/null
+++ b/CarRentalService.Api/Controllers/RentsController.cs
@@ -0,0 +1,131 @@
+using CarRentalService.Application.Contracts.Rents;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRentalService.Api.Controllers;
+
+///
+/// Controller for managing rental transactions
+///
+/// Rent service dependency
+[ApiController]
+[Route("api/[controller]")]
+[Produces("application/json")]
+public class RentsController(IRentService service) : ControllerBase
+{
+ ///
+ /// Retrieves all rental transactions
+ ///
+ /// List of all rental transactions
+ [HttpGet]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves a specific rental transaction by ID
+ ///
+ /// Rent identifier
+ /// Rental transaction details
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(typeof(RentDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var rent = await service.Get(id);
+ if (rent == null)
+ {
+ return NotFound($"Rent with ID {id} not found.");
+ }
+ return Ok(rent);
+ }
+
+ ///
+ /// Creates a new rental transaction
+ ///
+ /// Rental creation data
+ /// Created rental transaction
+ [HttpPost]
+ [ProducesResponseType(typeof(RentDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] RentCreateUpdateDto dto)
+ {
+ try
+ {
+ var createdRent = await service.Create(dto);
+ return CreatedAtAction(nameof(GetById), new { id = createdRent.Id }, createdRent);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest($"Invalid rent data: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Updates an existing rental transaction
+ ///
+ /// Rent identifier
+ /// Rental update data
+ /// No content on success
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
+ public async Task Update(int id, [FromBody] RentCreateUpdateDto dto)
+ {
+ try
+ {
+ var result = await service.Update(dto, id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound($"Rent with ID {id} not found.");
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Deletes a rental transaction
+ ///
+ /// Rent identifier
+ /// No content on success
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var result = await service.Delete(id);
+ return result ? NoContent() : NotFound();
+ }
+
+ ///
+ /// Retrieves all rental transactions for a specific client
+ ///
+ /// Client identifier
+ /// List of rental transactions for the client
+ [HttpGet("by-client/{clientId:int}")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetByClient(int clientId)
+ {
+ var result = await service.GetRentalsByClientAsync(clientId);
+ return Ok(result);
+ }
+
+ ///
+ /// Retrieves all rental transactions for a specific car
+ ///
+ /// Car identifier
+ /// List of rental transactions for the car
+ [HttpGet("by-car/{carId:int}")]
+ [ProducesResponseType(typeof(List), StatusCodes.Status200OK)]
+ public async Task>> GetByCar(int carId)
+ {
+ var result = await service.GetRentalsByCarAsync(carId);
+ return Ok(result);
+ }
+}
diff --git a/CarRentalService.Api/Program.cs b/CarRentalService.Api/Program.cs
new file mode 100644
index 000000000..b794afe22
--- /dev/null
+++ b/CarRentalService.Api/Program.cs
@@ -0,0 +1,168 @@
+using Microsoft.EntityFrameworkCore;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Application.Contracts.Clients;
+using CarRentalService.Application.Contracts.Rents;
+using CarRentalService.Application.Contracts.CarModel;
+using CarRentalService.Application.Contracts.CarModelGeneration;
+using CarRentalService.Application.Contracts;
+using CarRentalService.Application;
+using CarRentalService.Application.Services;
+using CarRentalService.Infrastructure.EfCore.Repositories;
+using CarRentalService.Infrastructure.EfCore;
+using CarRentalService.Domain.TestData;
+using CarRentalService.ServiceDefaults;
+using MongoDB.Driver;
+using System.Text.Json.Serialization;
+using System.Reflection;
+using CarRentalService.Api.Services;
+using CarRentalService.Application.Contracts.Grpc;
+using CarRentalService.Api.Configuration;
+using Microsoft.Extensions.Options;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.Configure(
+ builder.Configuration.GetSection(RentalGeneratorOptions.SectionName));
+
+// AutoMapper
+builder.Services.AddAutoMapper(config => config.AddProfile());
+
+// Test data
+builder.Services.AddSingleton();
+
+// Repository
+builder.Services.AddScoped, CarEfCoreRepository>();
+builder.Services.AddScoped, CarModelEfCoreRepository>();
+builder.Services.AddScoped, CarModelGenerationEfCoreRepository>();
+builder.Services.AddScoped, CustomerEfCoreRepository>();
+builder.Services.AddScoped, RentEfCoreRepository>();
+
+// Service
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.AddMongoDBClient("carrental");
+
+builder.Services.AddDbContext((services, options) =>
+{
+ var db = services.GetRequiredService();
+
+ options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName);
+});
+
+// gRPC client
+builder.Services.AddGrpcClient((serviceProvider, o) =>
+{
+ var options = serviceProvider.GetRequiredService>().Value;
+ var address = options.GrpcAddress ?? "https://localhost:7000";
+ o.Address = new Uri(address);
+})
+.ConfigureChannel(o =>
+{
+ o.MaxReceiveMessageSize = 32 * 1024 * 1024;
+ o.MaxSendMessageSize = 32 * 1024 * 1024;
+});
+
+builder.Services.AddAutoMapper(typeof(CarRentalGrpcProfile));
+builder.Services.AddMemoryCache();
+builder.Services.AddHostedService();
+
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("AllowAll",
+ builder =>
+ {
+ builder.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+});
+
+// Controller
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ });
+
+// Swagger
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var apiXmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ var apiXmlPath = Path.Combine(AppContext.BaseDirectory, apiXmlFile);
+
+ var contractsXmlFile = "CarRentalService.Application.Contracts.xml";
+ var contractsXmlPath = Path.Combine(AppContext.BaseDirectory, contractsXmlFile);
+
+ if (File.Exists(apiXmlPath))
+ {
+ c.IncludeXmlComments(apiXmlPath);
+ }
+
+ if (File.Exists(contractsXmlPath))
+ {
+ c.IncludeXmlComments(contractsXmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+app.UseCors("AllowAll");
+
+// Database seeding
+try
+{
+ using var scope = app.Services.CreateScope();
+ var context = scope.ServiceProvider.GetRequiredService();
+ var testData = scope.ServiceProvider.GetRequiredService();
+
+ context.Database.EnsureCreated();
+
+ if (!context.CarModels.Any())
+ {
+ Console.WriteLine("Database is empty, seeding with test data...");
+
+ context.CarModels.AddRange(testData.CarModels);
+ context.SaveChanges();
+ context.CarModelGenerations.AddRange(testData.CarModelGenerations);
+ context.SaveChanges();
+ context.Customers.AddRange(testData.Customers);
+ context.SaveChanges();
+ context.Cars.AddRange(testData.Cars);
+ context.SaveChanges();
+ context.Rents.AddRange(testData.Rents);
+ context.SaveChanges();
+ Console.WriteLine("Database seeded successfully with test data!");
+ }
+ else
+ {
+ Console.WriteLine("Database already contains data.");
+ }
+}
+catch (Exception ex)
+{
+ Console.WriteLine($"Error during database seeding: {ex.Message}");
+}
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+
+app.Run();
diff --git a/CarRentalService.Api/Properties/launchSettings.json b/CarRentalService.Api/Properties/launchSettings.json
new file mode 100644
index 000000000..c5927d28b
--- /dev/null
+++ b/CarRentalService.Api/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "CarRentalService.API": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRentalService.Api/Services/CarRentalGrpcProfile.cs b/CarRentalService.Api/Services/CarRentalGrpcProfile.cs
new file mode 100644
index 000000000..028782119
--- /dev/null
+++ b/CarRentalService.Api/Services/CarRentalGrpcProfile.cs
@@ -0,0 +1,23 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Grpc;
+using CarRentalService.Application.Contracts.Rents;
+
+namespace CarRentalService.Api.Services;
+
+///
+/// AutoMapper profile for mapping gRPC messages to DTOs
+///
+public class CarRentalGrpcProfile : Profile
+{
+ ///
+ /// Initializes a new instance of the CarRentalGrpcProfile class
+ ///
+ public CarRentalGrpcProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.CarId, opt => opt.MapFrom(src => src.CarId))
+ .ForMember(dest => dest.ClientId, opt => opt.MapFrom(src => src.CustomerId))
+ .ForMember(dest => dest.StartTime, opt => opt.MapFrom(src => DateTime.Parse(src.StartTime)))
+ .ForMember(dest => dest.Duration, opt => opt.MapFrom(src => src.DurationHours));
+ }
+}
diff --git a/CarRentalService.Api/Services/RentalIngestorClientService.cs b/CarRentalService.Api/Services/RentalIngestorClientService.cs
new file mode 100644
index 000000000..aaade56a1
--- /dev/null
+++ b/CarRentalService.Api/Services/RentalIngestorClientService.cs
@@ -0,0 +1,172 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Grpc;
+using CarRentalService.Application.Contracts.Rents;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Application.Contracts.Clients;
+using CarRentalService.Api.Configuration;
+using Grpc.Core;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+
+namespace CarRentalService.Api.Services;
+
+///
+/// Background gRPC client service for receiving rental contracts
+///
+public class CarRentalGrpcClient(
+ RentalIngestor.RentalIngestorClient client,
+ IServiceScopeFactory scopeFactory,
+ IMapper mapper,
+ ILogger logger,
+ IOptions options,
+ IMemoryCache cache
+) : BackgroundService
+{
+ private static readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(10);
+ private readonly RentalGeneratorOptions _options = options.Value;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ logger.LogInformation("CarRentalGrpcClient service starting...");
+
+ await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await ConnectAndProcessAsync(stoppingToken);
+
+ await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
+ }
+ catch (RpcException ex) when (!stoppingToken.IsCancellationRequested)
+ {
+ logger.LogError(ex, "gRPC stream error: {StatusCode} - {StatusDetail}",
+ ex.StatusCode, ex.Status.Detail);
+ await Task.Delay(TimeSpan.FromSeconds(_options.RetryDelay), stoppingToken);
+ }
+ catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
+ {
+ logger.LogError(ex, "Unexpected error in CarRentalGrpcClient");
+ await Task.Delay(TimeSpan.FromSeconds(_options.RetryDelay), stoppingToken);
+ }
+ }
+ }
+
+ private async Task ConnectAndProcessAsync(CancellationToken stoppingToken)
+ {
+ var count = _options.Count;
+ var batchSize = _options.BatchSize;
+
+ logger.LogInformation("Connecting to RentalGenerator gRPC service...");
+
+ using var call = client.StreamRentals(cancellationToken: stoppingToken);
+ var requestId = Guid.NewGuid().ToString("N");
+
+ var writerTask = Task.Run(async () =>
+ {
+ await call.RequestStream.WriteAsync(new RentalGenerationRequest
+ {
+ RequestId = requestId,
+ Count = count,
+ BatchSize = batchSize
+ }, stoppingToken);
+
+ await call.RequestStream.CompleteAsync();
+ }, stoppingToken);
+
+ await foreach (var batch in call.ResponseStream.ReadAllAsync(stoppingToken))
+ {
+ if (batch.RequestId != requestId)
+ continue;
+
+ await ProcessBatchAsync(batch, stoppingToken);
+
+ if (batch.IsFinal)
+ {
+ logger.LogInformation("Finished receiving rentals for RequestId={RequestId}", requestId);
+ break;
+ }
+ }
+
+ await writerTask;
+ }
+
+ private async Task ProcessBatchAsync(RentalBatchStreamMessage batch, CancellationToken ct)
+ {
+ using var scope = scopeFactory.CreateScope();
+
+ var rentalService = scope.ServiceProvider.GetRequiredService();
+ var carService = scope.ServiceProvider.GetRequiredService();
+ var clientService = scope.ServiceProvider.GetRequiredService();
+
+ var validRentals = new List();
+
+ foreach (var rental in batch.Rentals)
+ {
+ if (!await ValidateEntityExistsAsync(rental.CarId, "Car",
+ async (id, token) => await carService.Get(id) != null, ct))
+ continue;
+
+ if (!await ValidateEntityExistsAsync(rental.CustomerId, "Client",
+ async (id, token) => await clientService.Get(id) != null, ct))
+ continue;
+
+ var dto = mapper.Map(rental);
+ validRentals.Add(dto);
+ }
+
+ var createdCount = 0;
+ foreach (var rentalDto in validRentals)
+ {
+ try
+ {
+ await rentalService.Create(rentalDto);
+ createdCount++;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to create rental for CarId={CarId}, ClientId={ClientId}",
+ rentalDto.CarId, rentalDto.ClientId);
+ }
+ }
+
+ logger.LogInformation(
+ "Processed batch: Total={Total}, Valid={Valid}, Created={Created}, IsFinal={IsFinal}",
+ batch.Rentals.Count, validRentals.Count, createdCount, batch.IsFinal);
+ }
+
+ private async Task ValidateEntityExistsAsync(
+ TId id,
+ string entityName,
+ Func> existenceCheck,
+ CancellationToken ct)
+ {
+ var cacheKey = $"{entityName}:exists:{id}";
+
+ if (cache.TryGetValue(cacheKey, out bool cached))
+ return cached;
+
+ bool exists;
+ try
+ {
+ exists = await existenceCheck(id, ct);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ logger.LogWarning(ex, "Error checking existence of {Entity} with id {Id}", entityName, id);
+ exists = false;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+
+ cache.Set(cacheKey, exists, new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = _cacheTtl
+ });
+
+ return exists;
+ }
+}
diff --git a/CarRentalService.Api/appsettings.Development.json b/CarRentalService.Api/appsettings.Development.json
new file mode 100644
index 000000000..88b8948eb
--- /dev/null
+++ b/CarRentalService.Api/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "CarRentalService.API.Services": "Information",
+ "CarRentalService.API.Controllers": "Information"
+ }
+ }
+}
diff --git a/CarRentalService.Api/appsettings.json b/CarRentalService.Api/appsettings.json
new file mode 100644
index 000000000..9a029fa5d
--- /dev/null
+++ b/CarRentalService.Api/appsettings.json
@@ -0,0 +1,16 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "RentalGenerator": {
+ "GrpcAddress": "https://localhost:7000",
+ "Count": 50,
+ "BatchSize": 10,
+ "RetryDelay": 5,
+ "MaxRetries": 3
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CarRentalService.AppHost/AppHost.cs b/CarRentalService.AppHost/AppHost.cs
new file mode 100644
index 000000000..9bb35994d
--- /dev/null
+++ b/CarRentalService.AppHost/AppHost.cs
@@ -0,0 +1,28 @@
+///
+/// Aspire AppHost for Car Rental Service application orchestration
+///
+var builder = DistributedApplication.CreateBuilder(args);
+
+builder.Configuration["Logging:EventLog:LogLevel:Default"] = "None";
+builder.Configuration["Logging:EventLog:LogLevel:Microsoft.AspNetCore"] = "None";
+
+// Add MongoDB
+var mongo = builder.AddMongoDB("mongo").AddDatabase("carrental");
+
+// Add generator with parameters
+var batchSize = builder.AddParameter("GeneratorBatchSize");
+var waitTime = builder.AddParameter("GeneratorWaitTime");
+
+var generator = builder.AddProject("generator")
+ .WithEnvironment("Generator:BatchSize", batchSize)
+ .WithEnvironment("Generator:WaitTime", waitTime);
+
+// Add main API
+builder.AddProject("carrental-api")
+ .WithReference(mongo)
+ .WithReference(generator)
+ .WithEnvironment("RentalGenerator:GrpcAddress", generator.GetEndpoint("https"))
+ .WaitFor(mongo)
+ .WaitFor(generator);
+
+builder.Build().Run();
diff --git a/CarRentalService.AppHost/CarRentalService.AppHost.csproj b/CarRentalService.AppHost/CarRentalService.AppHost.csproj
new file mode 100644
index 000000000..0953451b9
--- /dev/null
+++ b/CarRentalService.AppHost/CarRentalService.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 53327de9-2e8f-4d99-9fe1-1cf49498fb5e
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.AppHost/Properties/launchSettings.json b/CarRentalService.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..d3390a565
--- /dev/null
+++ b/CarRentalService.AppHost/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17217;http://localhost:15297",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21065",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23071",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22221"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15297",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19159",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18074",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20015"
+ }
+ }
+ }
+}
diff --git a/CarRentalService.AppHost/appsettings.json b/CarRentalService.AppHost/appsettings.json
new file mode 100644
index 000000000..0cbabc4e2
--- /dev/null
+++ b/CarRentalService.AppHost/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ },
+ "Parameters": {
+ "GeneratorBatchSize": 10,
+ "GeneratorWaitTime": 2
+ }
+}
diff --git a/CarRentalService.Application.Contracts/Analytics/CarRentalCountResponse.cs b/CarRentalService.Application.Contracts/Analytics/CarRentalCountResponse.cs
new file mode 100644
index 000000000..3497b3a55
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Analytics/CarRentalCountResponse.cs
@@ -0,0 +1,27 @@
+namespace CarRentalService.Application.Contracts.Analytics;
+
+///
+/// Response for car rental count analytics
+///
+public class CarRentalCountResponse
+{
+ ///
+ /// Car identifier
+ ///
+ public int CarId { get; set; }
+
+ ///
+ /// License plate number
+ ///
+ public string LicensePlate { get; set; } = string.Empty;
+
+ ///
+ /// Car model name
+ ///
+ public string ModelName { get; set; } = string.Empty;
+
+ ///
+ /// Number of rentals
+ ///
+ public int RentalCount { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Analytics/CarRentalResponse.cs b/CarRentalService.Application.Contracts/Analytics/CarRentalResponse.cs
new file mode 100644
index 000000000..f5b04ab5d
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Analytics/CarRentalResponse.cs
@@ -0,0 +1,42 @@
+namespace CarRentalService.Application.Contracts.Analytics;
+
+///
+/// Responses for analytics endpoints
+///
+public class CarRentalResponse
+{
+ ///
+ /// Car identifier
+ ///
+ public int CarId { get; set; }
+
+ ///
+ /// License plate number
+ ///
+ public string LicensePlate { get; set; } = string.Empty;
+
+ ///
+ /// Car color
+ ///
+ public string Color { get; set; } = string.Empty;
+
+ ///
+ /// Car model name
+ ///
+ public string ModelName { get; set; } = string.Empty;
+
+ ///
+ /// Client name
+ ///
+ public string ClientName { get; set; } = string.Empty;
+
+ ///
+ /// Rental start time
+ ///
+ public DateTime RentalStart { get; set; }
+
+ ///
+ /// Rental end time
+ ///
+ public DateTime RentalEnd { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Analytics/TopCarResponse.cs b/CarRentalService.Application.Contracts/Analytics/TopCarResponse.cs
new file mode 100644
index 000000000..49c26dc1a
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Analytics/TopCarResponse.cs
@@ -0,0 +1,37 @@
+namespace CarRentalService.Application.Contracts.Analytics;
+
+///
+/// Response for top rented cars analytics
+///
+public class TopCarResponse
+{
+ ///
+ /// Car identifier
+ ///
+ public int CarId { get; set; }
+
+ ///
+ /// License plate number
+ ///
+ public string LicensePlate { get; set; } = string.Empty;
+
+ ///
+ /// Car model name
+ ///
+ public string ModelName { get; set; } = string.Empty;
+
+ ///
+ /// Number of rentals
+ ///
+ public int RentalCount { get; set; }
+
+ ///
+ /// Total rental hours
+ ///
+ public double TotalHours { get; set; }
+
+ ///
+ /// Total revenue
+ ///
+ public decimal TotalRevenue { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Analytics/TopCustomerResponse.cs b/CarRentalService.Application.Contracts/Analytics/TopCustomerResponse.cs
new file mode 100644
index 000000000..d454d55a6
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Analytics/TopCustomerResponse.cs
@@ -0,0 +1,27 @@
+namespace CarRentalService.Application.Contracts.Analytics;
+
+///
+/// Response for top customers by revenue analytics
+///
+public class TopCustomerResponse
+{
+ ///
+ /// Customer identifier
+ ///
+ public int CustomerId { get; set; }
+
+ ///
+ /// Full name
+ ///
+ public string FullName { get; set; } = string.Empty;
+
+ ///
+ /// Number of rentals
+ ///
+ public int RentalCount { get; set; }
+
+ ///
+ /// Total revenue
+ ///
+ public decimal TotalRevenue { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRentalService.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs
new file mode 100644
index 000000000..3788d0c70
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRentalService.Application.Contracts.CarModel;
+
+///
+/// DTO for creating or updating a car model
+///
+public class CarModelCreateUpdateDto
+{
+ ///
+ /// Model name
+ ///
+ [Required]
+ [StringLength(100)]
+ public required string Name { get; set; }
+
+ ///
+ /// Drive type
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string DriveType { get; set; }
+
+ ///
+ /// Number of seats
+ ///
+ [Required]
+ [Range(1, 20)]
+ public required int SeatCount { get; set; }
+
+ ///
+ /// Body type
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string BodyType { get; set; }
+
+ ///
+ /// Car class
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string CarClass { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/CarModel/CarModelDto.cs b/CarRentalService.Application.Contracts/CarModel/CarModelDto.cs
new file mode 100644
index 000000000..96c89ec50
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModel/CarModelDto.cs
@@ -0,0 +1,37 @@
+namespace CarRentalService.Application.Contracts.CarModel;
+
+///
+/// DTO for car model responses
+///
+public class CarModelDto
+{
+ ///
+ /// Model identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Model name
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Drive type
+ ///
+ public string DriveType { get; set; } = string.Empty;
+
+ ///
+ /// Number of seats
+ ///
+ public int SeatCount { get; set; }
+
+ ///
+ /// Body type
+ ///
+ public string BodyType { get; set; } = string.Empty;
+
+ ///
+ /// Car class
+ ///
+ public string CarClass { get; set; } = string.Empty;
+}
diff --git a/CarRentalService.Application.Contracts/CarModel/ICarModelService.cs b/CarRentalService.Application.Contracts/CarModel/ICarModelService.cs
new file mode 100644
index 000000000..4ee5707fc
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModel/ICarModelService.cs
@@ -0,0 +1,14 @@
+using CarRentalService.Application.Contracts.Cars;
+
+namespace CarRentalService.Application.Contracts.CarModel;
+
+///
+/// Car model service interface
+///
+public interface ICarModelService : IApplicationService
+{
+ ///
+ /// Retrieves all cars associated with a specific car model
+ ///
+ public Task> GetCarsByModelAsync(int modelId);
+}
diff --git a/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs
new file mode 100644
index 000000000..a13b84e4c
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRentalService.Application.Contracts.CarModelGeneration;
+
+///
+/// DTO for creating or updating a car model generation
+///
+public class CarModelGenerationCreateUpdateDto
+{
+ ///
+ /// Car model identifier
+ ///
+ [Required]
+ [Range(1, int.MaxValue)]
+ public required int CarModelId { get; set; }
+
+ ///
+ /// Production year
+ ///
+ [Required]
+ [Range(1900, 2100)]
+ public required int ProductionYear { get; set; }
+
+ ///
+ /// Engine volume in liters
+ ///
+ [Required]
+ [Range(0.5, 10.0)]
+ public required double EngineVolume { get; set; }
+
+ ///
+ /// Transmission type
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string TransmissionType { get; set; }
+
+ ///
+ /// Rental cost per hour
+ ///
+ [Required]
+ [Range(0.01, 10000)]
+ public required decimal RentalCostPerHour { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs
new file mode 100644
index 000000000..1d65b3468
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs
@@ -0,0 +1,42 @@
+namespace CarRentalService.Application.Contracts.CarModelGeneration;
+
+///
+/// DTO for car model generation responses
+///
+public class CarModelGenerationDto
+{
+ ///
+ /// Generation identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Car model identifier
+ ///
+ public int CarModelId { get; set; }
+
+ ///
+ /// Car model name
+ ///
+ public string CarModelName { get; set; } = string.Empty;
+
+ ///
+ /// Production year
+ ///
+ public int ProductionYear { get; set; }
+
+ ///
+ /// Engine volume in liters
+ ///
+ public double EngineVolume { get; set; }
+
+ ///
+ /// Transmission type
+ ///
+ public string TransmissionType { get; set; } = string.Empty;
+
+ ///
+ /// Rental cost per hour
+ ///
+ public decimal RentalCostPerHour { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/CarModelGeneration/ICarModelGenerationService.cs b/CarRentalService.Application.Contracts/CarModelGeneration/ICarModelGenerationService.cs
new file mode 100644
index 000000000..b28cdf19c
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarModelGeneration/ICarModelGenerationService.cs
@@ -0,0 +1,8 @@
+namespace CarRentalService.Application.Contracts.CarModelGeneration;
+
+///
+/// Car model generation service interface
+///
+public interface ICarModelGenerationService : IApplicationService
+{
+}
diff --git a/CarRentalService.Application.Contracts/CarRentalService.Application.Contracts.csproj b/CarRentalService.Application.Contracts/CarRentalService.Application.Contracts.csproj
new file mode 100644
index 000000000..74b9a6a40
--- /dev/null
+++ b/CarRentalService.Application.Contracts/CarRentalService.Application.Contracts.csproj
@@ -0,0 +1,23 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ CarRentalService.Application.Contracts.xml
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.Application.Contracts/Cars/CarCreateUpdateDto.cs b/CarRentalService.Application.Contracts/Cars/CarCreateUpdateDto.cs
new file mode 100644
index 000000000..e38e665c1
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Cars/CarCreateUpdateDto.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRentalService.Application.Contracts.Cars;
+
+///
+/// DTO for creating or updating a car
+///
+public class CarCreateUpdateDto
+{
+ ///
+ /// License plate number
+ ///
+ [Required]
+ [StringLength(20)]
+ public required string LicensePlate { get; set; }
+
+ ///
+ /// Car color
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string Color { get; set; }
+
+ ///
+ /// Car model generation identifier
+ ///
+ [Required]
+ [Range(1, int.MaxValue)]
+ public required int CarModelGenerationId { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Cars/CarDto.cs b/CarRentalService.Application.Contracts/Cars/CarDto.cs
new file mode 100644
index 000000000..7b06d8723
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Cars/CarDto.cs
@@ -0,0 +1,29 @@
+using CarRentalService.Application.Contracts.CarModelGeneration;
+
+namespace CarRentalService.Application.Contracts.Cars;
+
+///
+/// DTO for car responses
+///
+public class CarDto
+{
+ ///
+ /// Car identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// License plate number
+ ///
+ public string LicensePlate { get; set; } = string.Empty;
+
+ ///
+ /// Car color
+ ///
+ public string Color { get; set; } = string.Empty;
+
+ ///
+ /// Car model generation information
+ ///
+ public CarModelGenerationDto CarModelGeneration { get; set; } = null!;
+}
diff --git a/CarRentalService.Application.Contracts/Cars/ICarService.cs b/CarRentalService.Application.Contracts/Cars/ICarService.cs
new file mode 100644
index 000000000..1d8a09b03
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Cars/ICarService.cs
@@ -0,0 +1,8 @@
+namespace CarRentalService.Application.Contracts.Cars;
+
+///
+/// Car service interface
+///
+public interface ICarService : IApplicationService
+{
+}
diff --git a/CarRentalService.Application.Contracts/Clients/ClientCreateUpdateDto.cs b/CarRentalService.Application.Contracts/Clients/ClientCreateUpdateDto.cs
new file mode 100644
index 000000000..a2e6db42c
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Clients/ClientCreateUpdateDto.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRentalService.Application.Contracts.Clients;
+
+///
+/// DTO for creating or updating a client
+///
+public class ClientCreateUpdateDto
+{
+ ///
+ /// Driver license number
+ ///
+ [Required]
+ [StringLength(50)]
+ public required string DriverLicenseNumber { get; set; }
+
+ ///
+ /// Full name
+ ///
+ [Required]
+ [StringLength(100)]
+ public required string FullName { get; set; }
+
+ ///
+ /// Date of birth
+ ///
+ [Required]
+ public required DateTime DateOfBirth { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Clients/ClientDto.cs b/CarRentalService.Application.Contracts/Clients/ClientDto.cs
new file mode 100644
index 000000000..769aba86e
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Clients/ClientDto.cs
@@ -0,0 +1,32 @@
+namespace CarRentalService.Application.Contracts.Clients;
+
+///
+/// DTO for client responses
+///
+public class ClientDto
+{
+ ///
+ /// Client identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Driver license number
+ ///
+ public string DriverLicenseNumber { get; set; } = string.Empty;
+
+ ///
+ /// Full name
+ ///
+ public string FullName { get; set; } = string.Empty;
+
+ ///
+ /// Date of birth
+ ///
+ public DateTime DateOfBirth { get; set; }
+
+ ///
+ /// Age calculated from date of birth
+ ///
+ public int Age => DateTime.Now.Year - DateOfBirth.Year;
+}
diff --git a/CarRentalService.Application.Contracts/Clients/IClientService.cs b/CarRentalService.Application.Contracts/Clients/IClientService.cs
new file mode 100644
index 000000000..6d1aba3f1
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Clients/IClientService.cs
@@ -0,0 +1,8 @@
+namespace CarRentalService.Application.Contracts.Clients;
+
+///
+/// Client service interface
+///
+public interface IClientService : IApplicationService
+{
+}
diff --git a/CarRentalService.Application.Contracts/IAnalyticsService.cs b/CarRentalService.Application.Contracts/IAnalyticsService.cs
new file mode 100644
index 000000000..b05f1eb67
--- /dev/null
+++ b/CarRentalService.Application.Contracts/IAnalyticsService.cs
@@ -0,0 +1,39 @@
+using CarRentalService.Application.Contracts.Analytics;
+
+namespace CarRentalService.Application.Contracts;
+
+///
+/// Analytics service interface
+///
+public interface IAnalyticsService
+{
+ ///
+ /// Get customers who rented cars of specific model name
+ ///
+ public Task> ReadCustomersByModelName(string modelName);
+
+ ///
+ /// Get customers who rented cars of specific model ID
+ ///
+ public Task> ReadCustomersByModelId(int modelId);
+
+ ///
+ /// Get currently rented cars
+ ///
+ public Task> ReadCarsInRent(DateTime atTime);
+
+ ///
+ /// Get top N most rented cars
+ ///
+ public Task> ReadTopMostRentedCars(int count = 5);
+
+ ///
+ /// Get rental count for all cars
+ ///
+ public Task> ReadAllCarsWithRentalCount();
+
+ ///
+ /// Get top N customers by total rental revenue
+ ///
+ public Task> ReadTopCustomersByTotalAmount(int count = 5);
+}
diff --git a/CarRentalService.Application.Contracts/IApplicationService.cs b/CarRentalService.Application.Contracts/IApplicationService.cs
new file mode 100644
index 000000000..3c84f1397
--- /dev/null
+++ b/CarRentalService.Application.Contracts/IApplicationService.cs
@@ -0,0 +1,37 @@
+namespace CarRentalService.Application.Contracts;
+
+///
+/// Generic service interface defining basic CRUD operations for entities
+///
+/// DTO type for entity representation
+/// DTO type for create/update operations
+/// Type of the entity's primary key
+public interface IApplicationService
+ where TDto : class
+ where TCreateUpdateDto : class
+{
+ ///
+ /// Creates a new entity
+ ///
+ public Task Create(TCreateUpdateDto dto);
+
+ ///
+ /// Retrieves a specific entity by its identifier
+ ///
+ public Task Get(TId id);
+
+ ///
+ /// Retrieves all entities
+ ///
+ public Task> GetAll();
+
+ ///
+ /// Updates an existing entity
+ ///
+ public Task Update(TCreateUpdateDto dto, TId id);
+
+ ///
+ /// Deletes an entity by its identifier
+ ///
+ public Task Delete(TId id);
+}
diff --git a/CarRentalService.Application.Contracts/Protos/Rental.proto b/CarRentalService.Application.Contracts/Protos/Rental.proto
new file mode 100644
index 000000000..ebe4dab03
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Protos/Rental.proto
@@ -0,0 +1,27 @@
+syntax = "proto3";
+
+option csharp_namespace = "CarRentalService.Application.Contracts.Grpc";
+
+message RentalGenerationRequest {
+ string request_id = 1;
+ int32 count = 2;
+ int32 batch_size = 3;
+}
+
+message RentalContractMessage {
+ string id = 1;
+ int32 car_id = 2;
+ int32 customer_id = 3;
+ string start_time = 4;
+ double duration_hours = 5;
+}
+
+message RentalBatchStreamMessage {
+ string request_id = 1;
+ repeated RentalContractMessage rentals = 2;
+ bool is_final = 3;
+}
+
+service RentalIngestor {
+ rpc StreamRentals (stream RentalGenerationRequest) returns (stream RentalBatchStreamMessage);
+}
diff --git a/CarRentalService.Application.Contracts/Rents/IRentService.cs b/CarRentalService.Application.Contracts/Rents/IRentService.cs
new file mode 100644
index 000000000..fb6b1ca7d
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Rents/IRentService.cs
@@ -0,0 +1,17 @@
+namespace CarRentalService.Application.Contracts.Rents;
+
+///
+/// Rent service interface
+///
+public interface IRentService : IApplicationService
+{
+ ///
+ /// Retrieves all rental transactions for a specific client
+ ///
+ public Task> GetRentalsByClientAsync(int clientId);
+
+ ///
+ /// Retrieves all rental transactions for a specific car
+ ///
+ public Task> GetRentalsByCarAsync(int carId);
+}
diff --git a/CarRentalService.Application.Contracts/Rents/RentCreateUpdateDto.cs b/CarRentalService.Application.Contracts/Rents/RentCreateUpdateDto.cs
new file mode 100644
index 000000000..651e278c3
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Rents/RentCreateUpdateDto.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CarRentalService.Application.Contracts.Rents;
+
+///
+/// DTO for creating or updating a rent
+///
+public class RentCreateUpdateDto
+{
+ ///
+ /// Car identifier
+ ///
+ [Required]
+ [Range(1, int.MaxValue)]
+ public required int CarId { get; set; }
+
+ ///
+ /// Client identifier
+ ///
+ [Required]
+ [Range(1, int.MaxValue)]
+ public required int ClientId { get; set; }
+
+ ///
+ /// Rental start time
+ ///
+ [Required]
+ public required DateTime StartTime { get; set; }
+
+ ///
+ /// Rental duration in hours (from 30 minutes to 30 days)
+ ///
+ [Required]
+ [Range(0.5, 720)]
+ public required double Duration { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Rents/RentDto.cs b/CarRentalService.Application.Contracts/Rents/RentDto.cs
new file mode 100644
index 000000000..cd56a69a8
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Rents/RentDto.cs
@@ -0,0 +1,44 @@
+using CarRentalService.Application.Contracts.Shared;
+
+namespace CarRentalService.Application.Contracts.Rents;
+
+///
+/// DTO for rent responses
+///
+public class RentDto
+{
+ ///
+ /// Rental identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Car information
+ ///
+ public CarSimpleDto Car { get; set; } = null!;
+
+ ///
+ /// Client information
+ ///
+ public ClientSimpleDto Client { get; set; } = null!;
+
+ ///
+ /// Rental start time
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// Rental duration in hours
+ ///
+ public double Duration { get; set; }
+
+ ///
+ /// Rental end time (calculated)
+ ///
+ public DateTime EndTime => StartTime.AddHours(Duration);
+
+ ///
+ /// Total cost (calculated)
+ ///
+ public decimal TotalCost => (decimal)Duration * Car.RentalCostPerHour;
+}
diff --git a/CarRentalService.Application.Contracts/Shared/CarSimpleDto.cs b/CarRentalService.Application.Contracts/Shared/CarSimpleDto.cs
new file mode 100644
index 000000000..fab3bdeee
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Shared/CarSimpleDto.cs
@@ -0,0 +1,27 @@
+namespace CarRentalService.Application.Contracts.Shared;
+
+///
+/// Simplified DTO for car information
+///
+public class CarSimpleDto
+{
+ ///
+ /// Car identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// License plate number
+ ///
+ public string LicensePlate { get; set; } = string.Empty;
+
+ ///
+ /// Model name
+ ///
+ public string ModelName { get; set; } = string.Empty;
+
+ ///
+ /// Rental cost per hour
+ ///
+ public decimal RentalCostPerHour { get; set; }
+}
diff --git a/CarRentalService.Application.Contracts/Shared/ClientSimpleDto.cs b/CarRentalService.Application.Contracts/Shared/ClientSimpleDto.cs
new file mode 100644
index 000000000..7f003142d
--- /dev/null
+++ b/CarRentalService.Application.Contracts/Shared/ClientSimpleDto.cs
@@ -0,0 +1,17 @@
+namespace CarRentalService.Application.Contracts.Shared;
+
+///
+/// Simplified DTO for client information
+///
+public class ClientSimpleDto
+{
+ ///
+ /// Client identifier
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Full name
+ ///
+ public string FullName { get; set; } = string.Empty;
+}
diff --git a/CarRentalService.Application/CarRentalProfile.cs b/CarRentalService.Application/CarRentalProfile.cs
new file mode 100644
index 000000000..2c47f2912
--- /dev/null
+++ b/CarRentalService.Application/CarRentalProfile.cs
@@ -0,0 +1,70 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Application.Contracts.Clients;
+using CarRentalService.Application.Contracts.Rents;
+using CarRentalService.Application.Contracts.CarModel;
+using CarRentalService.Application.Contracts.CarModelGeneration;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application;
+
+///
+/// AutoMapper profile for Car Rental Service domain entities to DTOs mapping
+///
+public class CarRentalProfile : Profile
+{
+ ///
+ /// Initializes a new instance of the CarRentalProfile class
+ ///
+ public CarRentalProfile()
+ {
+ // CarModel -> CarModelDto
+ CreateMap();
+
+ // CarModelCreateUpdateDto -> CarModel
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.Ignore());
+
+ // CarModelGeneration -> CarModelGenerationDto
+ CreateMap()
+ .ForMember(dest => dest.CarModelName, opt => opt.MapFrom(src => src.CarModel.Name));
+
+ // CarModelGenerationCreateUpdateDto -> CarModelGeneration
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.Ignore());
+
+ // Car -> CarDto
+ CreateMap()
+ .ForMember(dest => dest.CarModelGeneration, opt => opt.MapFrom(src => src.CarModelGeneration));
+
+ // CarCreateUpdateDto -> Car
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.Ignore());
+
+ // Customer -> ClientDto
+ CreateMap();
+
+ // ClientCreateUpdateDto -> Customer
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.Ignore());
+
+ // Rent -> RentDto
+ CreateMap()
+ .ForMember(dest => dest.Car, opt => opt.MapFrom(src => new Contracts.Shared.CarSimpleDto
+ {
+ Id = src.Car.Id,
+ LicensePlate = src.Car.LicensePlate,
+ ModelName = src.Car.CarModelGeneration.CarModel.Name,
+ RentalCostPerHour = src.Car.CarModelGeneration.RentalCostPerHour
+ }))
+ .ForMember(dest => dest.Client, opt => opt.MapFrom(src => new Contracts.Shared.ClientSimpleDto
+ {
+ Id = src.Customer.Id,
+ FullName = src.Customer.FullName
+ }));
+
+ // RentCreateUpdateDto -> Rent
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.Ignore());
+ }
+}
diff --git a/CarRentalService.Application/CarRentalService.Application.csproj b/CarRentalService.Application/CarRentalService.Application.csproj
new file mode 100644
index 000000000..96430d686
--- /dev/null
+++ b/CarRentalService.Application/CarRentalService.Application.csproj
@@ -0,0 +1,17 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.Application/Services/AnalyticsService.cs b/CarRentalService.Application/Services/AnalyticsService.cs
new file mode 100644
index 000000000..06272895f
--- /dev/null
+++ b/CarRentalService.Application/Services/AnalyticsService.cs
@@ -0,0 +1,285 @@
+using CarRentalService.Application.Contracts;
+using CarRentalService.Application.Contracts.Analytics;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Analytics service implementation for car rental business intelligence
+///
+/// Rent repository
+/// Car repository
+/// Customer repository
+/// Car model repository
+/// Car model generation repository
+public class AnalyticsService(
+ IRepository rentRepository,
+ IRepository carRepository,
+ IRepository customerRepository,
+ IRepository carModelRepository,
+ IRepository generationRepository
+) : IAnalyticsService
+{
+ ///
+ /// Get customers who rented cars of specific model name
+ ///
+ /// Car model name to filter by
+ /// List of customer full names ordered alphabetically
+ public async Task> ReadCustomersByModelName(string modelName)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCustomers = await customerRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+ var allCarModels = await carModelRepository.ReadAll();
+
+ var targetModel = allCarModels.FirstOrDefault(m =>
+ m.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase));
+
+ if (targetModel == null)
+ return new List();
+
+ var modelGenerationIds = allGenerations
+ .Where(g => g.CarModelId == targetModel.Id)
+ .Select(g => g.Id)
+ .ToList();
+
+ var carIds = allCars
+ .Where(c => modelGenerationIds.Contains(c.CarModelGenerationId))
+ .Select(c => c.Id)
+ .ToList();
+
+ var customerIds = allRents
+ .Where(r => carIds.Contains(r.CarId))
+ .Select(r => r.CustomerId)
+ .Distinct()
+ .ToList();
+
+ return allCustomers
+ .Where(c => customerIds.Contains(c.Id))
+ .Select(c => c.FullName)
+ .OrderBy(name => name)
+ .ToList();
+ }
+
+ ///
+ /// Get customers who rented cars of specific model ID
+ ///
+ /// /// Car model ID to filter by
+ /// List of customer full names ordered alphabetically
+ public async Task> ReadCustomersByModelId(int modelId)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCustomers = await customerRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+
+ var modelGenerationIds = allGenerations
+ .Where(g => g.CarModelId == modelId)
+ .Select(g => g.Id)
+ .ToList();
+
+ var carIds = allCars
+ .Where(c => modelGenerationIds.Contains(c.CarModelGenerationId))
+ .Select(c => c.Id)
+ .ToList();
+
+ var customerIds = allRents
+ .Where(r => carIds.Contains(r.CarId))
+ .Select(r => r.CustomerId)
+ .Distinct()
+ .ToList();
+
+ return allCustomers
+ .Where(c => customerIds.Contains(c.Id))
+ .Select(c => c.FullName)
+ .OrderBy(name => name)
+ .ToList();
+ }
+
+ ///
+ /// Get currently rented cars at specific time
+ ///
+ /// Time to check for active rentals
+ /// List of currently rented cars with rental details
+ public async Task> ReadCarsInRent(DateTime atTime)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allCustomers = await customerRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+ var allCarModels = await carModelRepository.ReadAll();
+
+ var activeRentals = allRents
+ .Where(r =>
+ {
+ var rentalEnd = r.StartTime.AddHours(r.Duration);
+ return r.StartTime <= atTime && atTime <= rentalEnd;
+ })
+ .ToList();
+
+ var result = new List();
+
+ foreach (var rental in activeRentals)
+ {
+ var car = allCars.FirstOrDefault(c => c.Id == rental.CarId);
+ var customer = allCustomers.FirstOrDefault(c => c.Id == rental.CustomerId);
+ var generation = allGenerations.FirstOrDefault(g => g.Id == car?.CarModelGenerationId);
+ var carModel = allCarModels.FirstOrDefault(m => m.Id == generation?.CarModelId);
+
+ if (car != null && customer != null && generation != null)
+ {
+ result.Add(new CarRentalResponse
+ {
+ CarId = car.Id,
+ LicensePlate = car.LicensePlate,
+ Color = car.Color,
+ ModelName = carModel?.Name ?? "Unknown",
+ ClientName = customer.FullName,
+ RentalStart = rental.StartTime,
+ RentalEnd = rental.StartTime.AddHours(rental.Duration)
+ });
+ }
+ }
+
+ return result
+ .DistinctBy(r => r.CarId)
+ .OrderBy(r => r.RentalStart)
+ .ToList();
+ }
+
+ ///
+ /// Get top N most rented cars
+ ///
+ /// Number of top cars to return (default: 5)
+ /// List of top rented cars with rental statistics
+ public async Task> ReadTopMostRentedCars(int count = 5)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+ var allCarModels = await carModelRepository.ReadAll();
+
+ var carGroups = allRents
+ .GroupBy(r => r.CarId)
+ .Select(g => new
+ {
+ CarId = g.Key,
+ RentalCount = g.Count(),
+ TotalHours = g.Sum(r => r.Duration)
+ });
+
+ var carRentalStats = new List();
+
+ foreach (var group in carGroups)
+ {
+ var car = allCars.FirstOrDefault(c => c.Id == group.CarId);
+ if (car == null) continue;
+
+ var generation = allGenerations.FirstOrDefault(g => g.Id == car.CarModelGenerationId);
+ if (generation == null) continue;
+
+ var carModel = allCarModels.FirstOrDefault(m => m.Id == generation.CarModelId);
+
+ carRentalStats.Add(new TopCarResponse
+ {
+ CarId = car.Id,
+ LicensePlate = car.LicensePlate,
+ ModelName = carModel?.Name ?? "Unknown",
+ RentalCount = group.RentalCount,
+ TotalHours = group.TotalHours,
+ TotalRevenue = (decimal)(group.TotalHours * (double)generation.RentalCostPerHour)
+ });
+ }
+
+ return carRentalStats
+ .OrderByDescending(x => x.RentalCount)
+ .ThenBy(x => x.LicensePlate)
+ .Take(count)
+ .ToList();
+ }
+
+ ///
+ /// Get rental count for all cars
+ ///
+ /// List of all cars with their rental counts
+ public async Task> ReadAllCarsWithRentalCount()
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+ var allCarModels = await carModelRepository.ReadAll();
+
+ var rentsByCar = allRents
+ .GroupBy(r => r.CarId)
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ return allCars
+ .Select(car =>
+ {
+ var generation = allGenerations.FirstOrDefault(g => g.Id == car.CarModelGenerationId);
+ var carModel = generation != null
+ ? allCarModels.FirstOrDefault(m => m.Id == generation.CarModelId)
+ : null;
+
+ return new CarRentalCountResponse
+ {
+ CarId = car.Id,
+ LicensePlate = car.LicensePlate,
+ ModelName = carModel?.Name ?? "Unknown",
+ RentalCount = rentsByCar.GetValueOrDefault(car.Id, 0)
+ };
+ })
+ .OrderByDescending(x => x.RentalCount)
+ .ThenBy(x => x.CarId)
+ .ToList();
+ }
+
+ ///
+ /// Get top N customers by total rental revenue
+ ///
+ /// Number of top customers to return (default: 5)
+ /// List of top customers by total rental revenue
+ public async Task> ReadTopCustomersByTotalAmount(int count = 5)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var allCustomers = await customerRepository.ReadAll();
+ var allCars = await carRepository.ReadAll();
+ var allGenerations = await generationRepository.ReadAll();
+
+ return allRents
+ .Select(r => new
+ {
+ Rental = r,
+ Car = allCars.FirstOrDefault(c => c.Id == r.CarId),
+ Customer = allCustomers.FirstOrDefault(c => c.Id == r.CustomerId)
+ })
+ .Where(x => x.Car != null && x.Customer != null)
+ .GroupBy(x => x.Customer!.Id)
+ .Select(g =>
+ {
+ var customer = g.First().Customer!;
+
+ var totalRevenue = g.Sum(x =>
+ {
+ var generation = allGenerations.FirstOrDefault(gen => gen.Id == x.Car!.CarModelGenerationId);
+ return generation != null
+ ? (decimal)(x.Rental.Duration * (double)generation.RentalCostPerHour)
+ : 0;
+ });
+
+ return new TopCustomerResponse
+ {
+ CustomerId = customer.Id,
+ FullName = customer.FullName,
+ RentalCount = g.Count(),
+ TotalRevenue = totalRevenue
+ };
+ })
+ .OrderByDescending(x => x.TotalRevenue)
+ .ThenBy(x => x.FullName)
+ .Take(count)
+ .ToList();
+ }
+}
diff --git a/CarRentalService.Application/Services/CarModelGenerationService.cs b/CarRentalService.Application/Services/CarModelGenerationService.cs
new file mode 100644
index 000000000..f9a3c080e
--- /dev/null
+++ b/CarRentalService.Application/Services/CarModelGenerationService.cs
@@ -0,0 +1,132 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.CarModelGeneration;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Service implementation for managing car model generations
+///
+/// Car model generation repository
+/// Car model repository
+/// AutoMapper instance
+public class CarModelGenerationService(
+ IRepository generationRepository,
+ IRepository carModelRepository,
+ IMapper mapper
+) : ICarModelGenerationService
+{
+ ///
+ /// Creates a new car model generation
+ ///
+ /// Car model generation creation data
+ /// Created car model generation DTO
+ public async Task Create(CarModelGenerationCreateUpdateDto dto)
+ {
+ var allGenerations = await generationRepository.ReadAll();
+ var maxId = allGenerations.Any() ? allGenerations.Max(g => g.Id) : 0;
+
+ var carModel = await carModelRepository.Read(dto.CarModelId);
+ if (carModel == null)
+ throw new ArgumentException($"CarModel with id {dto.CarModelId} not found");
+
+ var generation = mapper.Map(dto);
+ generation.Id = maxId + 1;
+ generation.CarModelId = carModel.Id;
+
+ var created = await generationRepository.Create(generation);
+ return await MapToDtoWithDetails(created);
+ }
+
+ ///
+ /// Deletes a car model generation by its identifier
+ ///
+ /// Car model generation identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ => await generationRepository.Delete(id);
+
+ ///
+ /// Retrieves a car model generation by its identifier
+ ///
+ /// Car model generation identifier
+ /// Car model generation DTO if found, null otherwise
+ public async Task Get(int id)
+ {
+ var generation = await generationRepository.Read(id);
+ return generation != null ? await MapToDtoWithDetails(generation) : null;
+ }
+
+ ///
+ /// Retrieves all car model generations
+ ///
+ /// List of all car model generation DTOs
+ public async Task> GetAll()
+ {
+ var generations = await generationRepository.ReadAll();
+ var carModels = await carModelRepository.ReadAll();
+
+ var result = new List();
+ foreach (var generation in generations)
+ {
+ var carModel = carModels.FirstOrDefault(m => m.Id == generation.CarModelId);
+ result.Add(new CarModelGenerationDto
+ {
+ Id = generation.Id,
+ CarModelId = generation.CarModelId,
+ CarModelName = carModel?.Name ?? "Unknown",
+ ProductionYear = generation.ProductionYear,
+ EngineVolume = generation.EngineVolume,
+ TransmissionType = generation.TransmissionType,
+ RentalCostPerHour = generation.RentalCostPerHour
+ });
+ }
+
+ return result;
+ }
+
+ ///
+ /// Updates an existing car model generation
+ ///
+ /// Car model generation update data
+ /// Car model generation identifier
+ /// Updated car model generation DTO
+ public async Task Update(CarModelGenerationCreateUpdateDto dto, int id)
+ {
+ var generation = await generationRepository.Read(id);
+ if (generation == null)
+ throw new KeyNotFoundException($"CarModelGeneration with id {id} not found");
+
+ var carModel = await carModelRepository.Read(dto.CarModelId);
+ if (carModel == null)
+ throw new ArgumentException($"CarModel with id {dto.CarModelId} not found");
+
+ mapper.Map(dto, generation);
+ generation.CarModelId = carModel.Id;
+
+ var updated = await generationRepository.Update(generation);
+ return await MapToDtoWithDetails(updated);
+ }
+
+ ///
+ /// Maps CarModelGeneration entity to DTO with additional details
+ ///
+ /// Car model generation entity
+ /// Car model generation DTO with details
+ private async Task MapToDtoWithDetails(CarModelGeneration generation)
+ {
+ var carModel = await carModelRepository.Read(generation.CarModelId);
+
+ return new CarModelGenerationDto
+ {
+ Id = generation.Id,
+ CarModelId = generation.CarModelId,
+ CarModelName = carModel?.Name ?? "Unknown",
+ ProductionYear = generation.ProductionYear,
+ EngineVolume = generation.EngineVolume,
+ TransmissionType = generation.TransmissionType,
+ RentalCostPerHour = generation.RentalCostPerHour
+ };
+ }
+}
diff --git a/CarRentalService.Application/Services/CarModelService.cs b/CarRentalService.Application/Services/CarModelService.cs
new file mode 100644
index 000000000..ea5c553db
--- /dev/null
+++ b/CarRentalService.Application/Services/CarModelService.cs
@@ -0,0 +1,100 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.CarModel;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Service implementation for managing car models
+///
+/// Car model repository
+/// Car repository
+/// Car model generation repository
+/// AutoMapper instance
+public class CarModelService(
+ IRepository carModelRepository,
+ IRepository carRepository,
+ IMapper mapper
+) : ICarModelService
+{
+ ///
+ /// Creates a new car model
+ ///
+ /// Car model creation data
+ /// Created car model DTO
+ public async Task Create(CarModelCreateUpdateDto dto)
+ {
+ var allModels = await carModelRepository.ReadAll();
+ var maxId = allModels.Any() ? allModels.Max(m => m.Id) : 0;
+
+ var carModel = mapper.Map(dto);
+ carModel.Id = maxId + 1;
+
+ var created = await carModelRepository.Create(carModel);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Deletes a car model by its identifier
+ ///
+ /// Car model identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ => await carModelRepository.Delete(id);
+
+ ///
+ /// Retrieves a car model by its identifier
+ ///
+ /// Car model identifier
+ /// Car model DTO if found, null otherwise
+ public async Task Get(int id)
+ {
+ var carModel = await carModelRepository.Read(id);
+ return carModel != null ? mapper.Map(carModel) : null;
+ }
+
+ ///
+ /// Retrieves all car models
+ ///
+ /// List of all car model DTOs
+ public async Task> GetAll()
+ {
+ var carModels = await carModelRepository.ReadAll();
+ return mapper.Map>(carModels);
+ }
+
+ ///
+ /// Updates an existing car model
+ ///
+ /// Car model update data
+ /// Car model identifier
+ /// Updated car model DTO
+ public async Task Update(CarModelCreateUpdateDto dto, int id)
+ {
+ var carModel = await carModelRepository.Read(id);
+ if (carModel == null)
+ throw new KeyNotFoundException($"CarModel with id {id} not found");
+
+ mapper.Map(dto, carModel);
+ var updated = await carModelRepository.Update(carModel);
+ return mapper.Map(updated);
+ }
+
+ ///
+ /// Retrieves all cars associated with a specific car model
+ ///
+ /// Car model identifier
+ /// List of cars belonging to the specified model
+ public async Task> GetCarsByModelAsync(int modelId)
+ {
+ var cars = await carRepository.ReadAll();
+
+ var carsForModel = cars
+ .Where(c => c.CarModelGeneration.CarModel.Id == modelId)
+ .ToList();
+
+ return mapper.Map>(carsForModel);
+ }
+}
diff --git a/CarRentalService.Application/Services/CarService.cs b/CarRentalService.Application/Services/CarService.cs
new file mode 100644
index 000000000..a3bc4faa8
--- /dev/null
+++ b/CarRentalService.Application/Services/CarService.cs
@@ -0,0 +1,158 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Cars;
+using CarRentalService.Application.Contracts.CarModelGeneration;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Service implementation for managing cars
+///
+/// Car repository
+/// Car model repository
+/// Car model generation repository
+/// AutoMapper instance
+public class CarService(
+ IRepository carRepository,
+ IRepository carModelRepository,
+ IRepository generationRepository,
+ IMapper mapper
+) : ICarService
+{
+ ///
+ /// Creates a new car
+ ///
+ /// Car creation data
+ /// Created car DTO
+ public async Task Create(CarCreateUpdateDto dto)
+ {
+ var allCars = await carRepository.ReadAll();
+ var maxId = allCars.Any() ? allCars.Max(c => c.Id) : 0;
+
+ var generation = await generationRepository.Read(dto.CarModelGenerationId);
+ if (generation == null)
+ throw new ArgumentException($"CarModelGeneration with id {dto.CarModelGenerationId} not found");
+
+ var car = mapper.Map(dto);
+ car.Id = maxId + 1;
+ car.CarModelGenerationId = generation.Id;
+
+ var created = await carRepository.Create(car);
+ return await MapToDtoWithDetails(created);
+ }
+
+ ///
+ /// Deletes a car by its identifier
+ ///
+ /// Car identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ => await carRepository.Delete(id);
+
+ ///
+ /// Retrieves a car by its identifier
+ ///
+ /// Car identifier
+ /// Car DTO if found, null otherwise
+ public async Task Get(int id)
+ {
+ var car = await carRepository.Read(id);
+ return car != null ? await MapToDtoWithDetails(car) : null;
+ }
+
+ ///
+ /// Retrieves all cars
+ ///
+ /// List of all car DTOs
+ public async Task> GetAll()
+ {
+ var cars = await carRepository.ReadAll();
+ var result = new List();
+
+ var allGenerations = await generationRepository.ReadAll();
+ var allCarModels = await carModelRepository.ReadAll();
+
+ foreach (var car in cars)
+ {
+ var generation = allGenerations.FirstOrDefault(g => g.Id == car.CarModelGenerationId);
+ if (generation == null) continue;
+
+ var carModel = allCarModels.FirstOrDefault(m => m.Id == generation.CarModelId);
+
+ result.Add(new CarDto
+ {
+ Id = car.Id,
+ LicensePlate = car.LicensePlate,
+ Color = car.Color,
+ CarModelGeneration = new CarModelGenerationDto
+ {
+ Id = generation.Id,
+ CarModelId = generation.CarModelId,
+ CarModelName = carModel?.Name ?? "Unknown",
+ ProductionYear = generation.ProductionYear,
+ EngineVolume = generation.EngineVolume,
+ TransmissionType = generation.TransmissionType,
+ RentalCostPerHour = generation.RentalCostPerHour
+ }
+ });
+ }
+
+ return result;
+ }
+
+ ///
+ /// Updates an existing car
+ ///
+ /// Car update data
+ /// Car identifier
+ /// Updated car DTO
+ public async Task Update(CarCreateUpdateDto dto, int id)
+ {
+ var car = await carRepository.Read(id);
+ if (car == null)
+ throw new KeyNotFoundException($"Car with id {id} not found");
+
+ var generation = await generationRepository.Read(dto.CarModelGenerationId);
+ if (generation == null)
+ throw new ArgumentException($"CarModelGeneration with id {dto.CarModelGenerationId} not found");
+
+ car.LicensePlate = dto.LicensePlate;
+ car.Color = dto.Color;
+ car.CarModelGenerationId = generation.Id;
+
+ var updated = await carRepository.Update(car);
+ return await MapToDtoWithDetails(updated);
+ }
+
+ ///
+ /// Maps Car entity to DTO with additional details
+ ///
+ /// Car entity
+ /// Car DTO with details
+ private async Task MapToDtoWithDetails(Car car)
+ {
+ var generation = await generationRepository.Read(car.CarModelGenerationId);
+ if (generation == null)
+ throw new InvalidOperationException($"CarModelGeneration {car.CarModelGenerationId} not found");
+
+ var carModel = await carModelRepository.Read(generation.CarModelId);
+
+ return new CarDto
+ {
+ Id = car.Id,
+ LicensePlate = car.LicensePlate,
+ Color = car.Color,
+ CarModelGeneration = new CarModelGenerationDto
+ {
+ Id = generation.Id,
+ CarModelId = generation.CarModelId,
+ CarModelName = carModel?.Name ?? "Unknown",
+ ProductionYear = generation.ProductionYear,
+ EngineVolume = generation.EngineVolume,
+ TransmissionType = generation.TransmissionType,
+ RentalCostPerHour = generation.RentalCostPerHour
+ }
+ };
+ }
+}
diff --git a/CarRentalService.Application/Services/ClientService.cs b/CarRentalService.Application/Services/ClientService.cs
new file mode 100644
index 000000000..0bd0a72c9
--- /dev/null
+++ b/CarRentalService.Application/Services/ClientService.cs
@@ -0,0 +1,80 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Clients;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Service implementation for managing clients
+///
+/// Customer repository
+/// AutoMapper instance
+public class ClientService(
+ IRepository customerRepository,
+ IMapper mapper
+) : IClientService
+{
+ ///
+ /// Creates a new client
+ ///
+ /// Client creation data
+ /// Created client DTO
+ public async Task Create(ClientCreateUpdateDto dto)
+ {
+ var allCustomers = await customerRepository.ReadAll();
+ var maxId = allCustomers.Any() ? allCustomers.Max(c => c.Id) : 0;
+
+ var customer = mapper.Map(dto);
+ customer.Id = maxId + 1;
+
+ var created = await customerRepository.Create(customer);
+ return mapper.Map(created);
+ }
+
+ ///
+ /// Deletes a client by its identifier
+ ///
+ /// Client identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ => await customerRepository.Delete(id);
+
+ ///
+ /// Retrieves a client by its identifier
+ ///
+ /// Client identifier
+ /// Client DTO if found, null otherwise
+ public async Task Get(int id)
+ {
+ var customer = await customerRepository.Read(id);
+ return customer != null ? mapper.Map(customer) : null;
+ }
+
+ ///
+ /// Retrieves all clients
+ ///
+ /// List of all client DTOs
+ public async Task> GetAll()
+ {
+ var customers = await customerRepository.ReadAll();
+ return mapper.Map>(customers);
+ }
+
+ ///
+ /// Updates an existing client
+ ///
+ /// Client update data
+ /// Client identifier
+ /// Updated client DTO
+ public async Task Update(ClientCreateUpdateDto dto, int id)
+ {
+ var customer = await customerRepository.Read(id);
+ if (customer == null)
+ throw new KeyNotFoundException($"Client with id {id} not found");
+
+ mapper.Map(dto, customer);
+ var updated = await customerRepository.Update(customer);
+ return mapper.Map(updated);
+ }
+}
diff --git a/CarRentalService.Application/Services/RentService.cs b/CarRentalService.Application/Services/RentService.cs
new file mode 100644
index 000000000..969a1f863
--- /dev/null
+++ b/CarRentalService.Application/Services/RentService.cs
@@ -0,0 +1,192 @@
+using AutoMapper;
+using CarRentalService.Application.Contracts.Rents;
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Application.Services;
+
+///
+/// Service implementation for managing rent transactions
+///
+/// Rent repository
+/// Car repository
+/// Customer repository
+/// Car model generation repository
+/// AutoMapper instance
+public class RentService(
+ IRepository rentRepository,
+ IRepository carRepository,
+ IRepository customerRepository,
+ IRepository generationRepository,
+ IRepository carModelRepository,
+ IMapper mapper
+) : IRentService
+{
+ ///
+ /// Creates a new rent transaction
+ ///
+ /// Rent creation data
+ /// Created rent DTO
+ public async Task Create(RentCreateUpdateDto dto)
+ {
+ var allRents = await rentRepository.ReadAll();
+ var maxId = allRents.Any() ? allRents.Max(r => r.Id) : 0;
+
+ var car = await carRepository.Read(dto.CarId);
+ if (car == null)
+ throw new ArgumentException($"Car with id {dto.CarId} not found");
+
+ var customer = await customerRepository.Read(dto.ClientId);
+ if (customer == null)
+ throw new ArgumentException($"Client with id {dto.ClientId} not found");
+
+ var rent = mapper.Map(dto);
+ rent.Id = maxId + 1;
+ rent.CarId = car.Id;
+ rent.CustomerId = customer.Id;
+
+ var created = await rentRepository.Create(rent);
+ return await MapToDtoWithDetails(created);
+ }
+
+ ///
+ /// Deletes a rent transaction by its identifier
+ ///
+ /// Rent identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ => await rentRepository.Delete(id);
+
+ ///
+ /// Retrieves a rent transaction by its identifier
+ ///
+ /// Rent identifier
+ /// Rent DTO if found, null otherwise
+ public async Task Get(int id)
+ {
+ var rent = await rentRepository.Read(id);
+ return rent != null ? await MapToDtoWithDetails(rent) : null;
+ }
+
+ ///
+ /// Retrieves all rent transactions
+ ///
+ /// List of all rent DTOs
+ public async Task> GetAll()
+ {
+ var rents = await rentRepository.ReadAll();
+ var result = new List();
+
+ foreach (var rent in rents)
+ {
+ result.Add(await MapToDtoWithDetails(rent));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Updates an existing rent transaction
+ ///
+ /// Rent update data
+ /// Rent identifier
+ /// Updated rent DTO
+ public async Task Update(RentCreateUpdateDto dto, int id)
+ {
+ var rent = await rentRepository.Read(id);
+ if (rent == null)
+ throw new KeyNotFoundException($"Rent with id {id} not found");
+
+ var car = await carRepository.Read(dto.CarId);
+ if (car == null)
+ throw new ArgumentException($"Car with id {dto.CarId} not found");
+
+ var customer = await customerRepository.Read(dto.ClientId);
+ if (customer == null)
+ throw new ArgumentException($"Client with id {dto.ClientId} not found");
+
+ mapper.Map(dto, rent);
+ rent.CarId = car.Id;
+ rent.CustomerId = customer.Id;
+
+ var updated = await rentRepository.Update(rent);
+ return await MapToDtoWithDetails(updated);
+ }
+
+ ///
+ /// Retrieves all rental transactions for a specific client
+ ///
+ /// Client identifier
+ /// List of rent DTOs for the specified client
+ public async Task> GetRentalsByClientAsync(int clientId)
+ {
+ var rents = await rentRepository.ReadAll();
+ var clientRents = rents.Where(r => r.CustomerId == clientId).ToList();
+
+ var result = new List();
+ foreach (var rent in clientRents)
+ {
+ result.Add(await MapToDtoWithDetails(rent));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Retrieves all rental transactions for a specific car
+ ///
+ /// Car identifier
+ /// List of rent DTOs for the specified car
+ public async Task> GetRentalsByCarAsync(int carId)
+ {
+ var rents = await rentRepository.ReadAll();
+ var carRents = rents.Where(r => r.CarId == carId).ToList();
+
+ var result = new List();
+ foreach (var rent in carRents)
+ {
+ result.Add(await MapToDtoWithDetails(rent));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Maps Rent entity to DTO with additional details
+ ///
+ /// Rent entity
+ /// Rent DTO with details
+ private async Task MapToDtoWithDetails(Rent rent)
+ {
+ var car = await carRepository.Read(rent.CarId);
+ var customer = await customerRepository.Read(rent.CustomerId);
+
+ if (car == null || customer == null)
+ throw new InvalidOperationException("Car or Customer not found for rent");
+
+ var generation = await generationRepository.Read(car.CarModelGenerationId);
+ if (generation == null)
+ throw new InvalidOperationException($"CarModelGeneration {car.CarModelGenerationId} not found");
+
+ var carModel = await carModelRepository.Read(generation.CarModelId);
+
+ return new RentDto
+ {
+ Id = rent.Id,
+ Car = new Contracts.Shared.CarSimpleDto
+ {
+ Id = car.Id,
+ LicensePlate = car.LicensePlate,
+ ModelName = carModel?.Name ?? "Unknown",
+ RentalCostPerHour = generation.RentalCostPerHour
+ },
+ Client = new Contracts.Shared.ClientSimpleDto
+ {
+ Id = customer.Id,
+ FullName = customer.FullName
+ },
+ StartTime = rent.StartTime,
+ Duration = rent.Duration
+ };
+ }
+}
diff --git a/CarRentalService.Domain/CarRentalService.Domain.csproj b/CarRentalService.Domain/CarRentalService.Domain.csproj
new file mode 100644
index 000000000..80e861aed
--- /dev/null
+++ b/CarRentalService.Domain/CarRentalService.Domain.csproj
@@ -0,0 +1,8 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
diff --git a/CarRentalService.Domain/IRepository.cs b/CarRentalService.Domain/IRepository.cs
new file mode 100644
index 000000000..3e3ddae98
--- /dev/null
+++ b/CarRentalService.Domain/IRepository.cs
@@ -0,0 +1,43 @@
+namespace CarRentalService.Domain;
+
+///
+/// Generic repository interface for basic CRUD operations
+///
+/// Entity type
+/// Entity identifier type
+public interface IRepository where T : class
+{
+ ///
+ /// Creates a new entity in the repository
+ ///
+ /// Entity to create
+ /// Created entity
+ public Task Create(T entity);
+
+ ///
+ /// Deletes an entity by its identifier
+ ///
+ /// Entity identifier
+ /// True if deletion was successful, false otherwise
+ public Task Delete(TId id);
+
+ ///
+ /// Retrieves an entity by its identifier
+ ///
+ /// Entity identifier
+ /// Entity if found, null otherwise
+ public Task Read(TId id);
+
+ ///
+ /// Retrieves all entities from the repository
+ ///
+ /// List of all entities
+ public Task> ReadAll();
+
+ ///
+ /// Updates an existing entity
+ ///
+ /// Entity with updated data
+ /// Updated entity
+ public Task Update(T entity);
+}
diff --git a/CarRentalService.Domain/Models/Car.cs b/CarRentalService.Domain/Models/Car.cs
new file mode 100644
index 000000000..717090085
--- /dev/null
+++ b/CarRentalService.Domain/Models/Car.cs
@@ -0,0 +1,27 @@
+namespace CarRentalService.Domain.Models;
+///
+/// Car in the rental fleet
+///
+public class Car
+{
+ ///
+ /// Car identifier
+ ///
+ public int Id { get; set; }
+ ///
+ /// License plate number
+ ///
+ public required string LicensePlate { get; set; }
+ ///
+ /// Car color
+ ///
+ public required string Color { get; set; }
+ ///
+ /// Car model generation identifier
+ ///
+ public int CarModelGenerationId { get; set; }
+ ///
+ /// Car model generation
+ ///
+ public required CarModelGeneration CarModelGeneration { get; set; }
+}
diff --git a/CarRentalService.Domain/Models/CarModel.cs b/CarRentalService.Domain/Models/CarModel.cs
new file mode 100644
index 000000000..aab78f9d7
--- /dev/null
+++ b/CarRentalService.Domain/Models/CarModel.cs
@@ -0,0 +1,31 @@
+namespace CarRentalService.Domain.Models;
+///
+/// Car model reference
+///
+public class CarModel
+{
+ ///
+ /// Model identifier
+ ///
+ public int Id { get; set; }
+ ///
+ /// Model name
+ ///
+ public required string Name { get; set; }
+ ///
+ /// Drive type
+ ///
+ public required string DriveType { get; set; }
+ ///
+ /// Number of seats
+ ///
+ public required int SeatCount { get; set; }
+ ///
+ /// Body type
+ ///
+ public required string BodyType { get; set; }
+ ///
+ /// Car class
+ ///
+ public required string CarClass { get; set; }
+}
diff --git a/CarRentalService.Domain/Models/CarModelGeneration.cs b/CarRentalService.Domain/Models/CarModelGeneration.cs
new file mode 100644
index 000000000..31f9d13a2
--- /dev/null
+++ b/CarRentalService.Domain/Models/CarModelGeneration.cs
@@ -0,0 +1,35 @@
+namespace CarRentalService.Domain.Models;
+///
+/// Car model generation reference
+///
+public class CarModelGeneration
+{
+ ///
+ /// Generation identifier
+ ///
+ public int Id { get; set; }
+ ///
+ /// Car model identifier
+ ///
+ public int CarModelId { get; set; }
+ ///
+ /// Car model
+ ///
+ public required CarModel CarModel { get; set; }
+ ///
+ /// Production year
+ ///
+ public required int ProductionYear { get; set; }
+ ///
+ /// Engine volume
+ ///
+ public required double EngineVolume { get; set; }
+ ///
+ /// Transmission type
+ ///
+ public required string TransmissionType { get; set; }
+ ///
+ /// Rental cost per hour
+ ///
+ public required decimal RentalCostPerHour { get; set; }
+}
diff --git a/CarRentalService.Domain/Models/Customer.cs b/CarRentalService.Domain/Models/Customer.cs
new file mode 100644
index 000000000..2eba74d5c
--- /dev/null
+++ b/CarRentalService.Domain/Models/Customer.cs
@@ -0,0 +1,23 @@
+namespace CarRentalService.Domain.Models;
+///
+/// Customer of the rental service
+///
+public class Customer
+{
+ ///
+ /// Customer identifier
+ ///
+ public int Id { get; set; }
+ ///
+ /// Driver license number
+ ///
+ public required string DriverLicenseNumber { get; set; }
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; set; }
+ ///
+ /// Date of birth
+ ///
+ public required DateTime DateOfBirth { get; set; }
+}
diff --git a/CarRentalService.Domain/Models/Rent.cs b/CarRentalService.Domain/Models/Rent.cs
new file mode 100644
index 000000000..80a09eded
--- /dev/null
+++ b/CarRentalService.Domain/Models/Rent.cs
@@ -0,0 +1,35 @@
+namespace CarRentalService.Domain.Models;
+///
+/// Car rental transaction
+///
+public class Rent
+{
+ ///
+ /// Rental identifier
+ ///
+ public int Id { get; set; }
+ ///
+ /// Rented car identifier
+ ///
+ public int CarId { get; set; }
+ ///
+ /// Customer identifier
+ ///
+ public int CustomerId { get; set; }
+ ///
+ /// Rented car
+ ///
+ public required Car Car { get; set; }
+ ///
+ /// Customer
+ ///
+ public required Customer Customer { get; set; }
+ ///
+ /// Rental start time
+ ///
+ public required DateTime StartTime { get; set; }
+ ///
+ /// Rental duration in hours
+ ///
+ public required double Duration { get; set; }
+}
diff --git a/CarRentalService.Domain/TestData/DataSeed.cs b/CarRentalService.Domain/TestData/DataSeed.cs
new file mode 100644
index 000000000..99c238085
--- /dev/null
+++ b/CarRentalService.Domain/TestData/DataSeed.cs
@@ -0,0 +1,764 @@
+using CarRentalService.Domain.Models;
+
+namespace CarRentalService.Domain.TestData;
+///
+/// Test data generator for car rental service
+///
+public class TestData
+{
+ ///
+ /// List of car models
+ ///
+ public List CarModels { get; }
+ ///
+ /// List of customers
+ ///
+ public List Customers { get; }
+ ///
+ /// List of car model generations
+ ///
+ public List CarModelGenerations { get; }
+ ///
+ /// List of cars
+ ///
+ public List Cars { get; }
+ ///
+ /// List of rental transactions
+ ///
+ public List Rents { get; }
+ ///
+ /// Constructor for data initialization
+ ///
+ public TestData()
+ {
+ CarModels =
+ [
+ new()
+ {
+ Id = 1,
+ Name = "Toyota Camry",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Business"
+ },
+ new()
+ {
+ Id = 2,
+ Name = "BMW X5",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "SUV",
+ CarClass = "Premium"
+ },
+ new()
+ {
+ Id = 3,
+ Name = "Volkswagen Golf",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Hatchback",
+ CarClass = "Economy"
+ },
+ new()
+ {
+ Id = 4,
+ Name = "Mercedes-Benz E-Class",
+ DriveType = "Rear",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Business"
+ },
+ new()
+ {
+ Id = 5,
+ Name = "Audi A6",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Premium"
+ },
+ new()
+ {
+ Id = 6,
+ Name = "Hyundai Solaris",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Economy"
+ },
+ new()
+ {
+ Id = 7,
+ Name = "Kia Sportage",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "SUV",
+ CarClass = "Standard"
+ },
+ new()
+ {
+ Id = 8,
+ Name = "Ford Focus",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Hatchback",
+ CarClass = "Standard"
+ },
+ new()
+ {
+ Id = 9,
+ Name = "Skoda Octavia",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Liftback",
+ CarClass = "Standard"
+ },
+ new()
+ {
+ Id = 10,
+ Name = "Lexus RX",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "SUV",
+ CarClass = "Premium"
+ },
+ new()
+ {
+ Id = 11,
+ Name = "Volvo XC90",
+ DriveType = "All",
+ SeatCount = 7,
+ BodyType = "SUV",
+ CarClass = "Premium"
+ },
+ new()
+ {
+ Id = 12,
+ Name = "Renault Logan",
+ DriveType = "Front",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Economy"
+ },
+ new()
+ {
+ Id = 13,
+ Name = "Tesla Model 3",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "Sedan",
+ CarClass = "Premium"
+ },
+ new()
+ {
+ Id = 14,
+ Name = "Honda CR-V",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "SUV",
+ CarClass = "Standard"
+ },
+ new()
+ {
+ Id = 15,
+ Name = "Mazda CX-5",
+ DriveType = "All",
+ SeatCount = 5,
+ BodyType = "SUV",
+ CarClass = "Standard"
+ }
+ ];
+
+ CarModelGenerations =
+ [
+ new()
+ {
+ Id = 1,
+ CarModelId = 1,
+ CarModel = CarModels[0],
+ ProductionYear = 2020,
+ EngineVolume = 2.5,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 1500
+ },
+ new()
+ {
+ Id = 2,
+ CarModelId = 1,
+ CarModel = CarModels[0],
+ ProductionYear = 2022,
+ EngineVolume = 2.5,
+ TransmissionType = "CVT",
+ RentalCostPerHour = 1700
+ },
+ new()
+ {
+ Id = 3,
+ CarModelId = 2,
+ CarModel = CarModels[1],
+ ProductionYear = 2021,
+ EngineVolume = 3.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 2500
+ },
+ new()
+ {
+ Id = 4,
+ CarModelId = 2,
+ CarModel = CarModels[1],
+ ProductionYear = 2023,
+ EngineVolume = 3.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 2800
+ },
+ new()
+ {
+ Id = 5,
+ CarModelId = 3,
+ CarModel = CarModels[2],
+ ProductionYear = 2020,
+ EngineVolume = 1.4,
+ TransmissionType = "Manual",
+ RentalCostPerHour = 800
+ },
+ new()
+ {
+ Id = 6,
+ CarModelId = 3,
+ CarModel = CarModels[2],
+ ProductionYear = 2022,
+ EngineVolume = 1.4,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 900
+ },
+ new()
+ {
+ Id = 7,
+ CarModelId = 4,
+ CarModel = CarModels[3],
+ ProductionYear = 2021,
+ EngineVolume = 2.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 2200
+ },
+ new()
+ {
+ Id = 8,
+ CarModelId = 5,
+ CarModel = CarModels[4],
+ ProductionYear = 2022,
+ EngineVolume = 2.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 2400
+ },
+ new()
+ {
+ Id = 9,
+ CarModelId = 6,
+ CarModel = CarModels[5],
+ ProductionYear = 2021,
+ EngineVolume = 1.6,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 750
+ },
+ new()
+ {
+ Id = 10,
+ CarModelId = 7,
+ CarModel = CarModels[6],
+ ProductionYear = 2022,
+ EngineVolume = 2.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 1200
+ },
+ new()
+ {
+ Id = 11,
+ CarModelId = 8,
+ CarModel = CarModels[7],
+ ProductionYear = 2020,
+ EngineVolume = 1.6,
+ TransmissionType = "Manual",
+ RentalCostPerHour = 850
+ },
+ new()
+ {
+ Id = 12,
+ CarModelId = 9,
+ CarModel = CarModels[8],
+ ProductionYear = 2021,
+ EngineVolume = 1.8,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 950
+ },
+ new()
+ {
+ Id = 13,
+ CarModelId = 10,
+ CarModel = CarModels[9],
+ ProductionYear = 2023,
+ EngineVolume = 3.5,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 3000
+ },
+ new()
+ {
+ Id = 14,
+ CarModelId = 11,
+ CarModel = CarModels[10],
+ ProductionYear = 2022,
+ EngineVolume = 2.0,
+ TransmissionType = "Automatic",
+ RentalCostPerHour = 2700
+ },
+ new()
+ {
+ Id = 15,
+ CarModelId = 12,
+ CarModel = CarModels[11],
+ ProductionYear = 2020,
+ EngineVolume = 1.6,
+ TransmissionType = "Manual",
+ RentalCostPerHour = 700
+ }
+ ];
+
+ Cars =
+ [
+ new()
+ {
+ Id = 1,
+ LicensePlate = "A123BC777",
+ Color = "Black",
+ CarModelGenerationId = 1,
+ CarModelGeneration = CarModelGenerations[0]
+ },
+ new()
+ {
+ Id = 2,
+ LicensePlate = "B234CD777",
+ Color = "White",
+ CarModelGenerationId = 1,
+ CarModelGeneration = CarModelGenerations[0]
+ },
+ new()
+ {
+ Id = 3,
+ LicensePlate = "C345DE777",
+ Color = "Silver",
+ CarModelGenerationId = 3,
+ CarModelGeneration = CarModelGenerations[2]
+ },
+ new()
+ {
+ Id = 4,
+ LicensePlate = "D456EF777",
+ Color = "Blue",
+ CarModelGenerationId = 3,
+ CarModelGeneration = CarModelGenerations[2]
+ },
+ new()
+ {
+ Id = 5,
+ LicensePlate = "E567FG777",
+ Color = "Red",
+ CarModelGenerationId = 5,
+ CarModelGeneration = CarModelGenerations[4]
+ },
+ new()
+ {
+ Id = 6,
+ LicensePlate = "F678GH777",
+ Color = "Gray",
+ CarModelGenerationId = 5,
+ CarModelGeneration = CarModelGenerations[4]
+ },
+ new()
+ {
+ Id = 7,
+ LicensePlate = "G789HI777",
+ Color = "Black",
+ CarModelGenerationId = 7,
+ CarModelGeneration = CarModelGenerations[6]
+ },
+ new()
+ {
+ Id = 8,
+ LicensePlate = "H890IJ777",
+ Color = "White",
+ CarModelGenerationId = 7,
+ CarModelGeneration = CarModelGenerations[6]
+ },
+ new()
+ {
+ Id = 9,
+ LicensePlate = "I901JK777",
+ Color = "Blue",
+ CarModelGenerationId = 9,
+ CarModelGeneration = CarModelGenerations[8]
+ },
+ new()
+ {
+ Id = 10,
+ LicensePlate = "J012KL777",
+ Color = "Silver",
+ CarModelGenerationId = 9,
+ CarModelGeneration = CarModelGenerations[8]
+ },
+ new()
+ {
+ Id = 11,
+ LicensePlate = "K123LM777",
+ Color = "Red",
+ CarModelGenerationId = 11,
+ CarModelGeneration = CarModelGenerations[10]
+ },
+ new()
+ {
+ Id = 12,
+ LicensePlate = "L234MN777",
+ Color = "Black",
+ CarModelGenerationId = 11,
+ CarModelGeneration = CarModelGenerations[10]
+ },
+ new()
+ {
+ Id = 13,
+ LicensePlate = "M345NO777",
+ Color = "White",
+ CarModelGenerationId = 13,
+ CarModelGeneration = CarModelGenerations[12]
+ },
+ new()
+ {
+ Id = 14,
+ LicensePlate = "N456OP777",
+ Color = "Gray",
+ CarModelGenerationId = 13,
+ CarModelGeneration = CarModelGenerations[12]
+ },
+ new()
+ {
+ Id = 15,
+ LicensePlate = "O567PQ777",
+ Color = "Blue",
+ CarModelGenerationId = 15,
+ CarModelGeneration = CarModelGenerations[14]
+ }
+ ];
+
+ Customers =
+ [
+ new()
+ {
+ Id = 1,
+ DriverLicenseNumber = "77AA123456",
+ FullName = "Ivanov Ivan Ivanovich",
+ DateOfBirth = new DateTime(1990, 5, 15)
+ },
+ new()
+ {
+ Id = 2,
+ DriverLicenseNumber = "77BB234567",
+ FullName = "Petrov Petr Petrovich",
+ DateOfBirth = new DateTime(1985, 8, 22)
+ },
+ new()
+ {
+ Id = 3,
+ DriverLicenseNumber = "77CC345678",
+ FullName = "Sidorova Anna Sergeevna",
+ DateOfBirth = new DateTime(1992, 3, 10)
+ },
+ new()
+ {
+ Id = 4,
+ DriverLicenseNumber = "77DD456789",
+ FullName = "Kuznetsov Alexey Vladimirovich",
+ DateOfBirth = new DateTime(1988, 11, 5)
+ },
+ new()
+ {
+ Id = 5,
+ DriverLicenseNumber = "77EE567890",
+ FullName = "Smirnova Ekaterina Dmitrievna",
+ DateOfBirth = new DateTime(1995, 7, 18)
+ },
+ new()
+ {
+ Id = 6,
+ DriverLicenseNumber = "77FF678901",
+ FullName = "Vasiliev Dmitry Andreevich",
+ DateOfBirth = new DateTime(1991, 2, 28)
+ },
+ new()
+ {
+ Id = 7,
+ DriverLicenseNumber = "77GG789012",
+ FullName = "Nikolaeva Olga Igorevna",
+ DateOfBirth = new DateTime(1987, 9, 12)
+ },
+ new()
+ {
+ Id = 8,
+ DriverLicenseNumber = "77HH890123",
+ FullName = "Morozov Sergey Viktorovich",
+ DateOfBirth = new DateTime(1993, 4, 25)
+ },
+ new()
+ {
+ Id = 9,
+ DriverLicenseNumber = "77II901234",
+ FullName = "Pavlova Maria Alexandrovna",
+ DateOfBirth = new DateTime(1994, 6, 30)
+ },
+ new()
+ {
+ Id = 10,
+ DriverLicenseNumber = "77JJ012345",
+ FullName = "Fedorov Andrey Nikolaevich",
+ DateOfBirth = new DateTime(1989, 12, 8)
+ },
+ new()
+ {
+ Id = 11,
+ DriverLicenseNumber = "77KK123456",
+ FullName = "Lebedeva Tatyana Petrovna",
+ DateOfBirth = new DateTime(1996, 1, 14)
+ },
+ new()
+ {
+ Id = 12,
+ DriverLicenseNumber = "77LL234567",
+ FullName = "Sokolov Igor Borisovich",
+ DateOfBirth = new DateTime(1990, 10, 20)
+ },
+ new()
+ {
+ Id = 13,
+ DriverLicenseNumber = "77MM345678",
+ FullName = "Romanova Elena Andreevna",
+ DateOfBirth = new DateTime(1986, 7, 3)
+ },
+ new()
+ {
+ Id = 14,
+ DriverLicenseNumber = "77NN456789",
+ FullName = "Kazakov Mikhail Sergeevich",
+ DateOfBirth = new DateTime(1997, 11, 15)
+ },
+ new()
+ {
+ Id = 15,
+ DriverLicenseNumber = "77OO567890",
+ FullName = "Tikhonova Anastasia Pavlovna",
+ DateOfBirth = new DateTime(1998, 3, 22)
+ }
+ ];
+
+ Rents =
+ [
+ new()
+ {
+ Id = 1,
+ CarId = 1,
+ Car = Cars[0],
+ CustomerId = 1,
+ Customer = Customers[0],
+ StartTime = new DateTime(2024, 3, 1, 10, 0, 0),
+ Duration = 48
+ },
+ new()
+ {
+ Id = 2,
+ CarId = 1,
+ Car = Cars[0],
+ CustomerId = 3,
+ Customer = Customers[2],
+ StartTime = new DateTime(2024, 2, 25, 14, 30, 0),
+ Duration = 72
+ },
+ new()
+ {
+ Id = 3,
+ CarId = 1,
+ Car = Cars[0],
+ CustomerId = 5,
+ Customer = Customers[4],
+ StartTime = new DateTime(2024, 2, 20, 9, 15, 0),
+ Duration = 24
+ },
+ new()
+ {
+ Id = 4,
+ CarId = 2,
+ Car = Cars[1],
+ CustomerId = 2,
+ Customer = Customers[1],
+ StartTime = new DateTime(2024, 2, 27, 11, 45, 0),
+ Duration = 96
+ },
+ new()
+ {
+ Id = 5,
+ CarId = 2,
+ Car = Cars[1],
+ CustomerId = 4,
+ Customer = Customers[3],
+ StartTime = new DateTime(2024, 2, 25, 16, 0, 0),
+ Duration = 120
+ },
+ new()
+ {
+ Id = 6,
+ CarId = 3,
+ Car = Cars[2],
+ CustomerId = 6,
+ Customer = Customers[5],
+ StartTime = new DateTime(2024, 2, 23, 13, 20, 0),
+ Duration = 72
+ },
+ new()
+ {
+ Id = 7,
+ CarId = 3,
+ Car = Cars[2],
+ CustomerId = 8,
+ Customer = Customers[7],
+ StartTime = new DateTime(2024, 2, 18, 10, 10, 0),
+ Duration = 48
+ },
+ new()
+ {
+ Id = 8,
+ CarId = 4,
+ Car = Cars[3],
+ CustomerId = 7,
+ Customer = Customers[6],
+ StartTime = new DateTime(2024, 2, 28, 8, 30, 0),
+ Duration = 36
+ },
+ new()
+ {
+ Id = 9,
+ CarId = 5,
+ Car = Cars[4],
+ CustomerId = 9,
+ Customer = Customers[8],
+ StartTime = new DateTime(2024, 2, 15, 12, 0, 0),
+ Duration = 96
+ },
+ new()
+ {
+ Id = 10,
+ CarId = 6,
+ Car = Cars[5],
+ CustomerId = 10,
+ Customer = Customers[9],
+ StartTime = new DateTime(2024, 2, 28, 7, 0, 0),
+ Duration = 168
+ },
+ new()
+ {
+ Id = 11,
+ CarId = 7,
+ Car = Cars[6],
+ CustomerId = 11,
+ Customer = Customers[10],
+ StartTime = new DateTime(2024, 2, 22, 15, 45, 0),
+ Duration = 72
+ },
+ new()
+ {
+ Id = 12,
+ CarId = 8,
+ Car = Cars[7],
+ CustomerId = 12,
+ Customer = Customers[11],
+ StartTime = new DateTime(2024, 2, 26, 9, 20, 0),
+ Duration = 48
+ },
+ new()
+ {
+ Id = 13,
+ CarId = 9,
+ Car = Cars[8],
+ CustomerId = 13,
+ Customer = Customers[12],
+ StartTime = new DateTime(2024, 2, 29, 22, 0, 0),
+ Duration = 60
+ },
+ new()
+ {
+ Id = 14,
+ CarId = 10,
+ Car = Cars[9],
+ CustomerId = 14,
+ Customer = Customers[13],
+ StartTime = new DateTime(2024, 2, 24, 11, 30, 0),
+ Duration = 96
+ },
+ new()
+ {
+ Id = 15,
+ CarId = 11,
+ Car = Cars[10],
+ CustomerId = 15,
+ Customer = Customers[14],
+ StartTime = new DateTime(2024, 2, 10, 14, 15, 0),
+ Duration = 120
+ },
+ new()
+ {
+ Id = 16,
+ CarId = 12,
+ Car = Cars[11],
+ CustomerId = 1,
+ Customer = Customers[0],
+ StartTime = new DateTime(2024, 2, 29, 14, 0, 0),
+ Duration = 48
+ },
+ new()
+ {
+ Id = 17,
+ CarId = 13,
+ Car = Cars[12],
+ CustomerId = 2,
+ Customer = Customers[1],
+ StartTime = new DateTime(2024, 2, 5, 16, 45, 0),
+ Duration = 72
+ },
+ new()
+ {
+ Id = 18,
+ CarId = 14,
+ Car = Cars[13],
+ CustomerId = 3,
+ Customer = Customers[2],
+ StartTime = new DateTime(2024, 2, 12, 10, 10, 0),
+ Duration = 36
+ },
+ new()
+ {
+ Id = 19,
+ CarId = 15,
+ Car = Cars[14],
+ CustomerId = 4,
+ Customer = Customers[3],
+ StartTime = new DateTime(2024, 2, 16, 13, 30, 0),
+ Duration = 84
+ },
+ new()
+ {
+ Id = 20,
+ CarId = 2,
+ Car = Cars[1],
+ CustomerId = 6,
+ Customer = Customers[5],
+ StartTime = new DateTime(2024, 3, 2, 9, 0, 0),
+ Duration = 24
+ }
+ ];
+ }
+}
diff --git a/CarRentalService.Generator.Grpc.Host/CarRentalService.Generator.Grpc.Host.csproj b/CarRentalService.Generator.Grpc.Host/CarRentalService.Generator.Grpc.Host.csproj
new file mode 100644
index 000000000..f281b49cf
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/CarRentalService.Generator.Grpc.Host.csproj
@@ -0,0 +1,21 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.Generator.Grpc.Host/Generator/RentalGenerator.cs b/CarRentalService.Generator.Grpc.Host/Generator/RentalGenerator.cs
new file mode 100644
index 000000000..3bd1ee111
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/Generator/RentalGenerator.cs
@@ -0,0 +1,42 @@
+using Bogus;
+using CarRentalService.Application.Contracts.Grpc;
+
+namespace CarRentalService.Generator.Grpc.Host.Generator;
+
+///
+/// Static generator for creating rental data using Bogus library
+///
+public static class RentalGenerator
+{
+ private static readonly Faker _faker = new("ru");
+
+ private static readonly int[] _carIds =
+ Enumerable.Range(1, 15).ToArray();
+
+ private static readonly int[] _customerIds =
+ Enumerable.Range(1, 15).ToArray();
+
+ ///
+ /// Generates a list of rental contracts
+ ///
+ /// Number of contracts to generate
+ /// List of generated rental contracts
+ public static IList Generate(int count)
+ {
+ var list = new List(count);
+
+ for (var i = 0; i < count; i++)
+ {
+ list.Add(new RentalContractMessage
+ {
+ Id = Guid.NewGuid().ToString(),
+ CarId = _faker.PickRandom(_carIds),
+ CustomerId = _faker.PickRandom(_customerIds),
+ DurationHours = _faker.Random.Double(0.5, 720),
+ StartTime = _faker.Date.Recent(30).ToString("o")
+ });
+ }
+
+ return list;
+ }
+}
diff --git a/CarRentalService.Generator.Grpc.Host/Grpc/CarRentalGrpcGeneratorService.cs b/CarRentalService.Generator.Grpc.Host/Grpc/CarRentalGrpcGeneratorService.cs
new file mode 100644
index 000000000..3e529edad
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/Grpc/CarRentalGrpcGeneratorService.cs
@@ -0,0 +1,83 @@
+using CarRentalService.Application.Contracts.Grpc;
+using CarRentalService.Generator.Grpc.Host.Generator;
+using Grpc.Core;
+
+namespace CarRentalService.Generator.Grpc.Host.Grpc;
+
+///
+/// gRPC service for generating rental contracts
+///
+/// Application configuration
+/// Logger instance
+public sealed class CarRentalGrpcGeneratorService(
+ IConfiguration configuration,
+ ILogger logger
+) : RentalIngestor.RentalIngestorBase
+{
+ private readonly int _defaultBatchSize = configuration.GetValue("Generator:BatchSize", 10);
+ private readonly int _waitTimeSeconds = configuration.GetValue("Generator:WaitTime", 2);
+ private readonly bool _enableLogging = configuration.GetValue("Generator:EnableLogging", true);
+
+ ///
+ /// Bidirectional gRPC stream method for generating rental contracts
+ ///
+ /// Incoming request stream
+ /// Outgoing response stream
+ /// Server call context
+ public override async Task StreamRentals(
+ IAsyncStreamReader requestStream,
+ IServerStreamWriter responseStream,
+ ServerCallContext context)
+ {
+ logger.LogInformation("Starting gRPC stream for rental generation");
+
+ await foreach (var req in requestStream.ReadAllAsync(context.CancellationToken))
+ {
+ if (_enableLogging)
+ {
+ logger.LogInformation(
+ "Received generation request: RequestId={RequestId}, Count={Count}, BatchSize={BatchSize}",
+ req.RequestId, req.Count, req.BatchSize);
+ }
+
+ var batchSize = req.BatchSize > 0 ? req.BatchSize : _defaultBatchSize;
+ var sent = 0;
+
+ while (sent < req.Count && !context.CancellationToken.IsCancellationRequested)
+ {
+ var take = Math.Min(batchSize, req.Count - sent);
+ var rentals = RentalGenerator.Generate(take);
+
+ var payload = new RentalBatchStreamMessage
+ {
+ RequestId = req.RequestId,
+ IsFinal = sent + take >= req.Count
+ };
+
+ payload.Rentals.AddRange(rentals);
+
+ if (_enableLogging)
+ {
+ logger.LogDebug(
+ "Sending batch: RequestId={RequestId}, BatchSize={BatchSize}, TotalSent={Sent}, IsFinal={IsFinal}",
+ req.RequestId, take, sent + take, payload.IsFinal);
+ }
+
+ await responseStream.WriteAsync(payload, context.CancellationToken);
+ sent += take;
+
+ if (!payload.IsFinal && _waitTimeSeconds > 0)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(_waitTimeSeconds), context.CancellationToken);
+ }
+ }
+
+ if (_enableLogging)
+ {
+ logger.LogInformation(
+ "Completed generation for RequestId={RequestId}: TotalGenerated={Total}",
+ req.RequestId, sent);
+ }
+ }
+ }
+}
diff --git a/CarRentalService.Generator.Grpc.Host/Program.cs b/CarRentalService.Generator.Grpc.Host/Program.cs
new file mode 100644
index 000000000..bafd8042a
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/Program.cs
@@ -0,0 +1,20 @@
+using CarRentalService.Generator.Grpc.Host.Grpc;
+using CarRentalService.ServiceDefaults;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddGrpc(options =>
+{
+ options.EnableDetailedErrors = builder.Environment.IsDevelopment();
+ options.MaxReceiveMessageSize = 32 * 1024 * 1024;
+ options.MaxSendMessageSize = 32 * 1024 * 1024;
+});
+
+var app = builder.Build();
+
+app.MapGrpcService();
+app.MapDefaultEndpoints();
+
+app.Run();
diff --git a/CarRentalService.Generator.Grpc.Host/Properties/launchSettings.json b/CarRentalService.Generator.Grpc.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..946800195
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "CarRentalService.Generator.Grpc.Host": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7000;http://localhost:7001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRentalService.Generator.Grpc.Host/appsettings.json b/CarRentalService.Generator.Grpc.Host/appsettings.json
new file mode 100644
index 000000000..cda8a7f90
--- /dev/null
+++ b/CarRentalService.Generator.Grpc.Host/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Grpc": "Warning"
+ }
+ },
+ "Generator": {
+ "BatchSize": 10,
+ "WaitTime": 2,
+ "EnableLogging": true
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CarRentalService.Infrastructure.EfCore/CarRentalService.Infrastructure.EfCore.csproj b/CarRentalService.Infrastructure.EfCore/CarRentalService.Infrastructure.EfCore.csproj
new file mode 100644
index 000000000..55afc6609
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/CarRentalService.Infrastructure.EfCore.csproj
@@ -0,0 +1,16 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRentalService.Infrastructure.EfCore/CarRentalServiceDbContext.cs b/CarRentalService.Infrastructure.EfCore/CarRentalServiceDbContext.cs
new file mode 100644
index 000000000..39c3cd566
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/CarRentalServiceDbContext.cs
@@ -0,0 +1,128 @@
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+using MongoDB.EntityFrameworkCore.Extensions;
+
+namespace CarRentalService.Infrastructure.EfCore;
+
+///
+/// Database context for Car Rental Service using MongoDB
+///
+public class CarRentalDbContext : DbContext
+{
+ ///
+ /// Initializes a new instance of the CarRentalDbContext
+ ///
+ /// DbContext options
+ public CarRentalDbContext(DbContextOptions options) : base(options)
+ {
+ Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
+ }
+ ///
+ /// Gets or sets the CarModels collection
+ ///
+ public DbSet CarModels { get; set; } = null!;
+ ///
+ /// Gets or sets the CarModelGenerations collection
+ ///
+ public DbSet CarModelGenerations { get; set; } = null!;
+ ///
+ /// Gets or sets the Cars collection
+ ///
+ public DbSet Cars { get; set; } = null!;
+ ///
+ /// Gets or sets the Customers collection
+ ///
+ public DbSet Customers { get; set; } = null!;
+ ///
+ /// Gets or sets the Rents collection
+ ///
+ public DbSet Rents { get; set; } = null!;
+
+ ///
+ /// Configures the database context options
+ ///
+ /// Options builder
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.EnableThreadSafetyChecks(false);
+ }
+
+ ///
+ /// Configures the entity models and relationships for MongoDB
+ ///
+ /// Model builder instance
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // CarModel configuration
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("car_models");
+ builder.HasKey(cm => cm.Id);
+ builder.Property(cm => cm.Id).HasElementName("_id");
+ builder.Property(cm => cm.Name).IsRequired().HasElementName("name");
+ builder.Property(cm => cm.DriveType).IsRequired().HasElementName("drive_type");
+ builder.Property(cm => cm.SeatCount).IsRequired().HasElementName("seat_count");
+ builder.Property(cm => cm.BodyType).IsRequired().HasElementName("body_type");
+ builder.Property(cm => cm.CarClass).IsRequired().HasElementName("car_class");
+ });
+
+ // CarModelGeneration configuration
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("car_model_generations");
+ builder.HasKey(cmg => cmg.Id);
+ builder.Property(cmg => cmg.Id).HasElementName("_id");
+ builder.Property(cmg => cmg.CarModelId).IsRequired().HasElementName("car_model_id");
+ builder.Property(cmg => cmg.ProductionYear).IsRequired().HasElementName("production_year");
+ builder.Property(cmg => cmg.EngineVolume).IsRequired().HasElementName("engine_volume");
+ builder.Property(cmg => cmg.TransmissionType).IsRequired().HasElementName("transmission_type");
+ builder.Property(cmg => cmg.RentalCostPerHour).IsRequired().HasElementName("rental_cost_per_hour");
+ builder.HasOne(cmg => cmg.CarModel)
+ .WithMany()
+ .HasForeignKey(cmg => cmg.CarModelId);
+ });
+
+ // Car configuration
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("cars");
+ builder.HasKey(c => c.Id);
+ builder.Property(c => c.Id).HasElementName("_id");
+ builder.Property(c => c.LicensePlate).IsRequired().HasElementName("license_plate");
+ builder.Property(c => c.Color).IsRequired().HasElementName("color");
+ builder.Property(c => c.CarModelGenerationId).IsRequired().HasElementName("car_model_generation_id");
+ builder.HasOne(c => c.CarModelGeneration)
+ .WithMany()
+ .HasForeignKey(c => c.CarModelGenerationId);
+ });
+
+ // Customer configuration
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("customers");
+ builder.HasKey(c => c.Id);
+ builder.Property(c => c.Id).HasElementName("_id");
+ builder.Property(c => c.DriverLicenseNumber).IsRequired().HasElementName("driver_license_number");
+ builder.Property(c => c.FullName).IsRequired().HasElementName("full_name");
+ builder.Property(c => c.DateOfBirth).IsRequired().HasElementName("date_of_birth");
+ });
+
+ // Rent configuration
+ modelBuilder.Entity(builder =>
+ {
+ builder.ToCollection("rents");
+ builder.HasKey(r => r.Id);
+ builder.Property(r => r.Id).HasElementName("_id");
+ builder.Property(r => r.CarId).IsRequired().HasElementName("car_id");
+ builder.Property(r => r.CustomerId).IsRequired().HasElementName("customer_id");
+ builder.Property(r => r.StartTime).IsRequired().HasElementName("start_time");
+ builder.Property(r => r.Duration).IsRequired().HasElementName("duration");
+ builder.HasOne(r => r.Car)
+ .WithMany()
+ .HasForeignKey(r => r.CarId);
+ builder.HasOne(r => r.Customer)
+ .WithMany()
+ .HasForeignKey(r => r.CustomerId);
+ });
+ }
+}
diff --git a/CarRentalService.Infrastructure.EfCore/Repositories/CarEfCoreRepository.cs b/CarRentalService.Infrastructure.EfCore/Repositories/CarEfCoreRepository.cs
new file mode 100644
index 000000000..7f8436b56
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/Repositories/CarEfCoreRepository.cs
@@ -0,0 +1,67 @@
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRentalService.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for Car entities using Entity Framework Core with MongoDB
+///
+public class CarEfCoreRepository(CarRentalDbContext context) : IRepository
+{
+ private readonly DbSet _cars = context.Cars!;
+
+ ///
+ /// Retrieves a car by its identifier
+ ///
+ /// Car identifier
+ /// Car if found, null otherwise
+ public async Task Read(int id)
+ => await _cars.FirstOrDefaultAsync(e => e.Id == id);
+
+ ///
+ /// Retrieves all cars from the database
+ ///
+ /// List of all cars
+ public async Task> ReadAll()
+ => await _cars.ToListAsync();
+
+ ///
+ /// Creates a new car in the database
+ ///
+ /// Car entity to create
+ /// Created car entity
+ public async Task Create(Car entity)
+ {
+ var result = await _cars.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return result.Entity;
+ }
+
+ ///
+ /// Updates an existing car in the database
+ ///
+ /// Car entity with updated data
+ /// Updated car entity
+ public async Task Update(Car entity)
+ {
+ _cars.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Deletes a car by its identifier
+ ///
+ /// Car identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ {
+ var entity = await Read(id);
+ if (entity == null) return false;
+
+ _cars.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
diff --git a/CarRentalService.Infrastructure.EfCore/Repositories/CarModelEfCoreRepository.cs b/CarRentalService.Infrastructure.EfCore/Repositories/CarModelEfCoreRepository.cs
new file mode 100644
index 000000000..42eeba6aa
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/Repositories/CarModelEfCoreRepository.cs
@@ -0,0 +1,67 @@
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRentalService.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for CarModel entities using Entity Framework Core with MongoDB
+///
+public class CarModelEfCoreRepository(CarRentalDbContext context) : IRepository
+{
+ private readonly DbSet _carModels = context.CarModels!;
+
+ ///
+ /// Retrieves a car model by its identifier
+ ///
+ /// Car model identifier
+ /// Car model if found, null otherwise
+ public async Task Read(int id)
+ => await _carModels.FindAsync(id);
+
+ ///
+ /// Retrieves all car models from the database
+ ///
+ /// List of all car models
+ public async Task> ReadAll()
+ => await _carModels.ToListAsync();
+
+ ///
+ /// Creates a new car model in the database
+ ///
+ /// Car model entity to create
+ /// Created car model entity
+ public async Task Create(CarModel entity)
+ {
+ var result = await _carModels.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return result.Entity;
+ }
+
+ ///
+ /// Updates an existing car model in the database
+ ///
+ /// Car model entity with updated data
+ /// Updated car model entity
+ public async Task Update(CarModel entity)
+ {
+ _carModels.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Deletes a car model by its identifier
+ ///
+ /// Car model identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ {
+ var entity = await Read(id);
+ if (entity == null) return false;
+
+ _carModels.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
diff --git a/CarRentalService.Infrastructure.EfCore/Repositories/CarModelGenerationEfCoreRepository.cs b/CarRentalService.Infrastructure.EfCore/Repositories/CarModelGenerationEfCoreRepository.cs
new file mode 100644
index 000000000..7c2acc9aa
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/Repositories/CarModelGenerationEfCoreRepository.cs
@@ -0,0 +1,68 @@
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRentalService.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for CarModelGeneration entities using Entity Framework Core with MongoDB
+///
+public class CarModelGenerationEfCoreRepository(CarRentalDbContext context)
+ : IRepository
+{
+ private readonly DbSet _generations = context.CarModelGenerations!;
+
+ ///
+ /// Retrieves a car model generation by its identifier
+ ///
+ /// Car model generation identifier
+ /// Car model generation if found, null otherwise
+ public async Task Read(int id)
+ => await _generations.FindAsync(id);
+
+ ///
+ /// Retrieves all car model generations from the database
+ ///
+ /// List of all car model generations
+ public async Task> ReadAll()
+ => await _generations.ToListAsync();
+
+ ///
+ /// Creates a new car model generation in the database
+ ///
+ /// Car model generation entity to create
+ /// Created car model generation entity
+ public async Task Create(CarModelGeneration entity)
+ {
+ var result = await _generations.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return result.Entity;
+ }
+
+ ///
+ /// Updates an existing car model generation in the database
+ ///
+ /// Car model generation entity with updated data
+ /// Updated car model generation entity
+ public async Task Update(CarModelGeneration entity)
+ {
+ _generations.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Deletes a car model generation by its identifier
+ ///
+ /// Car model generation identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ {
+ var entity = await Read(id);
+ if (entity == null) return false;
+
+ _generations.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
diff --git a/CarRentalService.Infrastructure.EfCore/Repositories/CustomerEfCoreRepository.cs b/CarRentalService.Infrastructure.EfCore/Repositories/CustomerEfCoreRepository.cs
new file mode 100644
index 000000000..fe247e102
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/Repositories/CustomerEfCoreRepository.cs
@@ -0,0 +1,67 @@
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRentalService.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for Customer entities using Entity Framework Core with MongoDB
+///
+public class CustomerEfCoreRepository(CarRentalDbContext context) : IRepository
+{
+ private readonly DbSet _customers = context.Customers!;
+
+ ///
+ /// Retrieves a customer by its identifier
+ ///
+ /// Customer identifier
+ /// Customer if found, null otherwise
+ public async Task Read(int id)
+ => await _customers.FindAsync(id);
+
+ ///
+ /// Retrieves all customers from the database
+ ///
+ /// List of all customers
+ public async Task> ReadAll()
+ => await _customers.ToListAsync();
+
+ ///
+ /// Creates a new customer in the database
+ ///
+ /// Customer entity to create
+ /// Created customer entity
+ public async Task Create(Customer entity)
+ {
+ var result = await _customers.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return result.Entity;
+ }
+
+ ///
+ /// Updates an existing customer in the database
+ ///
+ /// Customer entity with updated data
+ /// Updated customer entity
+ public async Task Update(Customer entity)
+ {
+ _customers.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Deletes a customer by its identifier
+ ///
+ /// Customer identifier
+ /// True if deletion was successful, false otherwise
+ public async Task Delete(int id)
+ {
+ var entity = await Read(id);
+ if (entity == null) return false;
+
+ _customers.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
diff --git a/CarRentalService.Infrastructure.EfCore/Repositories/RentEfCoreRepository.cs b/CarRentalService.Infrastructure.EfCore/Repositories/RentEfCoreRepository.cs
new file mode 100644
index 000000000..ffca10ab6
--- /dev/null
+++ b/CarRentalService.Infrastructure.EfCore/Repositories/RentEfCoreRepository.cs
@@ -0,0 +1,67 @@
+using CarRentalService.Domain;
+using CarRentalService.Domain.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRentalService.Infrastructure.EfCore.Repositories;
+
+///
+/// Repository implementation for Rent entities using Entity Framework Core with MongoDB
+///
+public class RentEfCoreRepository(CarRentalDbContext context) : IRepository
+{
+ private readonly DbSet _rents = context.Rents!;
+
+ ///
+ /// Retrieves a rent transaction by its identifier
+ ///
+ /// Rent identifier
+ /// Rent transaction if found, null otherwise
+ public async Task Read(int id)
+ => await _rents.FirstOrDefaultAsync(e => e.Id == id);
+
+ ///
+ /// Retrieves all rent transactions from the database
+ ///
+ /// List of all rent transactions
+ public async Task> ReadAll()
+ => await _rents.ToListAsync();
+
+ ///