diff --git a/CarRental/CarRental/.github/workflows/dotnet-tests.yml b/CarRental/CarRental/.github/workflows/dotnet-tests.yml
new file mode 100644
index 000000000..36c06b335
--- /dev/null
+++ b/CarRental/CarRental/.github/workflows/dotnet-tests.yml
@@ -0,0 +1,29 @@
+name: .NET Tests
+
+on:
+ push:
+ branches: [ main, lab_1 ]
+ pull_request:
+ branches: [ main, lab_1 ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore ./CarRental/CarRental.slnx
+
+ - name: Build
+ run: dotnet build ./CarRental/CarRental.slnx --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test ./CarRental/CarRental.Tests/CarRental.Tests.csproj --configuration Release --verbosity normal
diff --git a/CarRental/CarRental/CarRental.API/CarRental.API.csproj b/CarRental/CarRental/CarRental.API/CarRental.API.csproj
new file mode 100644
index 000000000..15ea0b55c
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/CarRental.API.csproj
@@ -0,0 +1,27 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
new file mode 100644
index 000000000..bb1b514dd
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
@@ -0,0 +1,199 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// Контроллер для аналитических запросов и отчетов
+///
+[ApiController]
+[Route("api/analytics")]
+public class AnalyticsController(
+ IRepository rentalsRepo,
+ IRepository carsRepo,
+ IRepository clientsRepo,
+ IRepository generationsRepo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ /// Получает список клиентов, арендовавших автомобили указанной модели, отсортированный по названию
+ ///
+ /// Название модели автомобиля
+ /// Список клиентов
+ [HttpGet("clients-by-model")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetClientsByModelSortedByName(
+ [FromQuery] string modelName)
+ {
+ var rentalsQuery = rentalsRepo.GetQueryable(
+ include: query => query
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+
+ var clients = await rentalsQuery
+ .Where(r => r.Car!.ModelGeneration!.Model!.Name == modelName)
+ .Select(r => r.Client)
+ .Distinct()
+ .OrderBy(c => c!.FullName)
+ .ToListAsync();
+
+ var result = clients
+ .Select(mapper.Map)
+ .ToList();
+
+ return Ok(result);
+ }
+
+ ///
+ /// Получает арендованные в данный момент автомобили
+ ///
+ /// Текущая дата проверки
+ /// Список арендованных автомобилей
+ [HttpGet("currently-rented-cars")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetCurrentlyRentedCars(
+ [FromQuery] DateTime currentDate)
+ {
+ var rentedCarIds = await rentalsRepo.GetQueryable()
+ .Where(r => r.RentalDate.AddHours(r.RentalHours) > currentDate)
+ .Select(r => r.CarId)
+ .Distinct()
+ .ToListAsync();
+
+ var rentedCars = await carsRepo.GetQueryable()
+ .Where(c => rentedCarIds.Contains(c.Id))
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(m => m!.Model)
+ .ToListAsync();
+
+ var result = rentedCars
+ .Select(mapper.Map)
+ .ToList();
+
+ return Ok(result);
+ }
+
+ ///
+ /// Получает топ 5 самых популярных арендованных автомобилей
+ ///
+ /// Список автомобилей, которые можно взять напрокат
+ [HttpGet("top-5-most-rented-cars")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetTop5MostRentedCars()
+ {
+ var topCarStats = await rentalsRepo.GetQueryable()
+ .GroupBy(r => r.CarId)
+ .Select(g => new { CarId = g.Key, RentalCount = g.Count() })
+ .OrderByDescending(x => x.RentalCount)
+ .Take(5)
+ .ToListAsync();
+
+ var topCarIds = topCarStats.Select(x => x.CarId).ToList();
+
+ var cars = await carsRepo.GetQueryable()
+ .Where(c => topCarIds.Contains(c.Id))
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(m => m!.Model)
+ .ToListAsync();
+
+ var carsDict = cars.ToDictionary(c => c.Id);
+
+ var topCarsResult = topCarStats
+ .Where(x => carsDict.ContainsKey(x.CarId))
+ .Select(x => new CarRentalCountDto(
+ mapper.Map(carsDict[x.CarId]),
+ x.RentalCount))
+ .ToList();
+
+ return Ok(topCarsResult);
+ }
+
+ ///
+ /// Получает количество арендованных автомобилей для каждого автомобиля
+ ///
+ /// Список всех автомобилей, которые были взяты в аренду
+ [HttpGet("rental-count-per-car")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetRentalCountPerCar()
+ {
+ var rentalCounts = await rentalsRepo.GetQueryable()
+ .GroupBy(r => r.CarId)
+ .Select(g => new { CarId = g.Key, Count = g.Count() })
+ .ToDictionaryAsync(x => x.CarId, x => x.Count);
+
+ var cars = await carsRepo.GetQueryable()
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(m => m!.Model)
+ .ToListAsync();
+
+ var carsWithRentalCount = cars
+ .Select(car => new CarRentalCountDto(
+ mapper.Map(car),
+ rentalCounts.GetValueOrDefault(car.Id, 0)))
+ .OrderByDescending(x => x.RentalCount)
+ .ToList();
+
+ return Ok(carsWithRentalCount);
+ }
+
+ ///
+ /// Получает топ 5 клиентов по общей сумме аренды
+ ///
+ /// Список клиентов с общей суммой арендной платы
+ [HttpGet("top-5-clients-by-rental-amount")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetTop5ClientsByRentalAmount()
+ {
+ var rentals = await rentalsRepo.GetQueryable()
+ .Select(r => new { r.ClientId, r.CarId, r.RentalHours })
+ .ToListAsync();
+
+ var cars = await carsRepo.GetQueryable()
+ .Select(c => new { c.Id, c.ModelGenerationId })
+ .ToListAsync();
+
+ var generations = await generationsRepo.GetQueryable()
+ .Select(g => new { g.Id, g.RentalPricePerHour })
+ .ToListAsync();
+
+ var carPrices = cars.Join(generations,
+ c => c.ModelGenerationId,
+ g => g.Id,
+ (c, g) => new { CarId = c.Id, Price = g.RentalPricePerHour })
+ .ToDictionary(x => x.CarId, x => x.Price);
+
+ var topClientStats = rentals
+ .GroupBy(r => r.ClientId)
+ .Select(g => new
+ {
+ ClientId = g.Key,
+ TotalAmount = g.Sum(r => r.RentalHours * carPrices.GetValueOrDefault(r.CarId, 0))
+ })
+ .OrderByDescending(x => x.TotalAmount)
+ .Take(5)
+ .ToList();
+
+ var topClientIds = topClientStats.Select(x => x.ClientId).ToList();
+
+ var clients = await clientsRepo.GetQueryable()
+ .Where(c => topClientIds.Contains(c.Id))
+ .ToListAsync();
+
+ var clientsDict = clients.ToDictionary(c => c.Id);
+
+ var result = topClientStats
+ .Where(x => clientsDict.ContainsKey(x.ClientId))
+ .Select(x => new ClientRentalAmountDto(
+ mapper.Map(clientsDict[x.ClientId]),
+ x.TotalAmount))
+ .ToList();
+
+ return Ok(result);
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs
new file mode 100644
index 000000000..faeb8d19d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs
@@ -0,0 +1,92 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// Контроллер для управления моделями автомобилей
+///
+[ApiController]
+[Route("api/car-models")]
+public class CarModelsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ /// Получает доступ ко всем моделям автомобилей
+ ///
+ /// Список всех моделей автомобилей
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var entities = await repo.GetAllAsync();
+ var dtos = mapper.Map>(entities);
+ return Ok(dtos);
+ }
+
+ ///
+ /// Получает модель автомобиля по идентификатору
+ ///
+ /// Идентификатор модели
+ /// Модель автомобиля
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Get(int id)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+ var dto = mapper.Map(entity);
+ return Ok(dto);
+ }
+
+ ///
+ /// Создает новую модель автомобиля
+ ///
+ /// Данные для создания модели
+ /// Созданная модель автомобиля
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] CarModelEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ var resultDto = mapper.Map(created);
+ return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
+ }
+
+ ///
+ /// Обновляет существующую модель автомобиля
+ ///
+ /// Идентификатор модели
+ /// Обновленные данные модели
+ /// Обновленная модель автомобиля
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] CarModelEditDto dto)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+ mapper.Map(dto, entity);
+ await repo.UpdateAsync(entity);
+ var resultDto = mapper.Map(entity);
+ return Ok(resultDto);
+ }
+
+ ///
+ /// Удалить модель автомобиля
+ ///
+ /// Идентификатор модели
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task Delete(int id)
+ {
+ await repo.DeleteAsync(id);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs
new file mode 100644
index 000000000..1d1ae82dd
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs
@@ -0,0 +1,124 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Api.Controllers;
+
+///
+///
+///
+[ApiController]
+[Route("api/cars")]
+public class CarsController(
+ IRepository repo,
+ IRepository modelGenerationRepo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var entities = await repo.GetAllAsync(
+ include: query => query
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ var dtos = mapper.Map>(entities);
+ return Ok(dtos);
+ }
+
+ ///
+ /// ID
+ ///
+ ///
+ ///
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Get(int id)
+ {
+ var entity = await repo.GetByIdAsync(id,
+ include: query => query
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ if (entity == null) return NotFound();
+ var dto = mapper.Map(entity);
+ return Ok(dto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] CarEditDto dto)
+ {
+ var modelGeneration = await modelGenerationRepo.GetByIdAsync(dto.ModelGenerationId);
+ if (modelGeneration == null)
+ return BadRequest($"Model generation with Id {dto.ModelGenerationId} does not exist.");
+
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+
+ // DTO
+ var carWithIncludes = await repo.GetByIdAsync(created.Id,
+ include: query => query
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ var resultDto = mapper.Map(carWithIncludes);
+
+ return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Update(int id, [FromBody] CarEditDto dto)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+
+ var modelGeneration = await modelGenerationRepo.GetByIdAsync(dto.ModelGenerationId);
+ if (modelGeneration == null)
+ return BadRequest($"Model generation with Id {dto.ModelGenerationId} does not exist.");
+
+ mapper.Map(dto, entity);
+ await repo.UpdateAsync(entity);
+
+ var updatedWithIncludes = await repo.GetByIdAsync(entity.Id,
+ include: query => query
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ var resultDto = mapper.Map(updatedWithIncludes);
+
+ return Ok(resultDto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task Delete(int id)
+ {
+ await repo.DeleteAsync(id);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs
new file mode 100644
index 000000000..40d2cf804
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs
@@ -0,0 +1,92 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// Контроллер для управления клиентами
+///
+[ApiController]
+[Route("api/clients")]
+public class ClientsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ /// Получает всех клиентов
+ ///
+ /// Список всех клиентов
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var entities = await repo.GetAllAsync();
+ var dtos = mapper.Map>(entities);
+ return Ok(dtos);
+ }
+
+ ///
+ /// Получает клиента по ID
+ ///
+ /// Идентификатор клиента
+ /// Клиент
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Get(int id)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+ var dto = mapper.Map(entity);
+ return Ok(dto);
+ }
+
+ ///
+ /// Создает нового клиента
+ ///
+ /// Данные для создания клиента
+ /// Созданный клиент
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] ClientEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ var resultDto = mapper.Map(created);
+ return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
+ }
+
+ ///
+ /// Обновляет существующий клиент
+ ///
+ /// Идентификатор клиента
+ /// Обновленные данные о клиентах
+ /// Обновленный клиент
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] ClientEditDto dto)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+ mapper.Map(dto, entity);
+ await repo.UpdateAsync(entity);
+ var resultDto = mapper.Map(entity);
+ return Ok(resultDto);
+ }
+
+ ///
+ /// Удаляет клиента
+ ///
+ /// Идентификатор клиента
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task Delete(int id)
+ {
+ await repo.DeleteAsync(id);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs
new file mode 100644
index 000000000..1a4d77932
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs
@@ -0,0 +1,117 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Api.Controllers;
+
+///
+///
+///
+[ApiController]
+[Route("api/model-generations")]
+public class ModelGenerationsController(
+ IRepository repo,
+ IRepository carModelRepo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var entities = await repo.GetAllAsync(
+ include: query => query.Include(mg => mg.Model));
+ var dtos = mapper.Map>(entities);
+ return Ok(dtos);
+ }
+
+ ///
+ /// ID
+ ///
+ ///
+ ///
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Get(int id)
+ {
+ var entity = await repo.GetByIdAsync(id,
+ include: query => query.Include(mg => mg.Model));
+ if (entity == null) return NotFound();
+ var dto = mapper.Map(entity);
+ return Ok(dto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] ModelGenerationEditDto dto)
+ {
+ var carModel = await carModelRepo.GetByIdAsync(dto.ModelId);
+ if (carModel == null)
+ return BadRequest($"Car model with Id {dto.ModelId} does not exist.");
+
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+
+ // DTO
+ var generationWithIncludes = await repo.GetByIdAsync(created.Id,
+ include: query => query.Include(mg => mg.Model));
+ var resultDto = mapper.Map(generationWithIncludes);
+
+ return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Update(int id, [FromBody] ModelGenerationEditDto dto)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+
+ var carModel = await carModelRepo.GetByIdAsync(dto.ModelId);
+ if (carModel == null)
+ return BadRequest($"Car model with Id {dto.ModelId} does not exist.");
+
+ mapper.Map(dto, entity);
+ await repo.UpdateAsync(entity);
+
+ // DTO
+ var updatedWithIncludes = await repo.GetByIdAsync(entity.Id,
+ include: query => query.Include(mg => mg.Model));
+ var resultDto = mapper.Map(updatedWithIncludes);
+
+ return Ok(resultDto);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task Delete(int id)
+ {
+ await repo.DeleteAsync(id);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs
new file mode 100644
index 000000000..1ebfe0f82
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs
@@ -0,0 +1,140 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Api.Controllers;
+
+///
+/// Контроллер для управления арендой
+///
+[ApiController]
+[Route("api/rentals")]
+public class RentalsController(
+ IRepository repo,
+ IRepository carRepo,
+ IRepository clientRepo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ /// Получает все аренды
+ ///
+ /// Список всех объектов аренды
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var entities = await repo.GetAllAsync(
+ include: query => query
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ var dtos = mapper.Map>(entities);
+ return Ok(dtos);
+ }
+
+ ///
+ /// Получает аренду по ID
+ ///
+ /// Идентификатор аренды
+ /// Аренда
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Get(int id)
+ {
+ var entity = await repo.GetByIdAsync(id,
+ include: query => query
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ if (entity == null) return NotFound();
+ var dto = mapper.Map(entity);
+ return Ok(dto);
+ }
+
+ ///
+ /// Создает новую аренду
+ ///
+ /// Данные о создании аренды
+ /// Созданная аренда
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] RentalEditDto dto)
+ {
+ var car = await carRepo.GetByIdAsync(dto.CarId);
+ if (car == null)
+ return BadRequest($"Car with Id {dto.CarId} does not exist.");
+
+ var client = await clientRepo.GetByIdAsync(dto.ClientId);
+ if (client == null)
+ return BadRequest($"Client with Id {dto.ClientId} does not exist.");
+
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+
+ var rentalWithIncludes = await repo.GetByIdAsync(created.Id,
+ include: query => query
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ var resultDto = mapper.Map(rentalWithIncludes);
+
+ return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
+ }
+
+ ///
+ /// Обновляет существующую аренду
+ ///
+ /// Идентификатор аренды
+ /// Обновленные данные об аренде
+ /// Обновленная аренда
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Update(int id, [FromBody] RentalEditDto dto)
+ {
+ var entity = await repo.GetByIdAsync(id);
+ if (entity == null) return NotFound();
+
+ var car = await carRepo.GetByIdAsync(dto.CarId);
+ if (car == null)
+ return BadRequest($"Car with Id {dto.CarId} does not exist.");
+
+ var client = await clientRepo.GetByIdAsync(dto.ClientId);
+ if (client == null)
+ return BadRequest($"Client with Id {dto.ClientId} does not exist.");
+
+ mapper.Map(dto, entity);
+ await repo.UpdateAsync(entity);
+
+ var updatedWithIncludes = await repo.GetByIdAsync(entity.Id,
+ include: query => query
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ var resultDto = mapper.Map(updatedWithIncludes);
+
+ return Ok(resultDto);
+ }
+
+ ///
+ /// Удаляет аренду
+ ///
+ /// Идентификатор аренды
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task Delete(int id)
+ {
+ await repo.DeleteAsync(id);
+ return NoContent();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Program.cs b/CarRental/CarRental/CarRental.API/Program.cs
new file mode 100644
index 000000000..88a4e40f5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Program.cs
@@ -0,0 +1,90 @@
+using CarRental.Application.Contracts;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Data;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using CarRental.Infrastructure.Kafka;
+using CarRental.Infrastructure.Kafka.Deserializers;
+using CarRental.Infrastructure.Persistence;
+using CarRental.Infrastructure.Repositories;
+using Confluent.Kafka;
+using Microsoft.EntityFrameworkCore;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.Services.AddSingleton();
+
+builder.Services.AddControllers().AddJsonOptions(options =>
+{
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+});
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var basePath = AppContext.BaseDirectory;
+
+ var xmlApiPath = Path.Combine(basePath, "CarRental.Api.xml");
+ if (File.Exists(xmlApiPath))
+ {
+ c.IncludeXmlComments(xmlApiPath, includeControllerXmlComments: true);
+ }
+
+ var xmlContractsPath = Path.Combine(basePath, "CarRental.Application.Contracts.xml");
+ if (File.Exists(xmlContractsPath))
+ {
+ c.IncludeXmlComments(xmlContractsPath);
+ }
+
+ var xmlDomainPath = Path.Combine(basePath, "CarRental.Domain.xml");
+ if (File.Exists(xmlDomainPath))
+ {
+ c.IncludeXmlComments(xmlDomainPath);
+ }
+});
+
+builder.Services.AddAutoMapper(cfg => cfg.AddProfile());
+
+builder.Services.AddDbContext(options =>
+ options.UseSqlServer(
+ builder.Configuration.GetConnectionString("DefaultConnection")));
+
+builder.Services.AddScoped, DbRepository>();
+builder.Services.AddScoped, DbRepository>();
+builder.Services.AddScoped, DbRepository>();
+builder.Services.AddScoped, DbRepository>();
+builder.Services.AddScoped, DbRepository>();
+
+builder.Services.AddHostedService();
+
+builder.AddKafkaConsumer>("carrental-kafka",
+ configureBuilder: builder =>
+ {
+ builder.SetKeyDeserializer(new GuidKeyDeserializer());
+ builder.SetValueDeserializer(new RentalValueDeserializer());
+ },
+ configureSettings: settings =>
+ {
+ settings.Config.GroupId = "rental-consumer";
+ settings.Config.AutoOffsetReset = AutoOffsetReset.Earliest;
+ }
+);
+
+var app = builder.Build();
+
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.Migrate();
+}
+
+app.UseSwagger();
+app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Car Rental API"));
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapDefaultEndpoints();
+app.MapControllers();
+app.Run();
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.API/Properties/launchSettings.json b/CarRental/CarRental/CarRental.API/Properties/launchSettings.json
new file mode 100644
index 000000000..8abf301bf
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:22448",
+ "sslPort": 44345
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5208",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7197;http://localhost:5208",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/appsettings.Development.json b/CarRental/CarRental/CarRental.API/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/appsettings.json b/CarRental/CarRental/CarRental.API/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj
new file mode 100644
index 000000000..b7f1b55f3
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 191c3bb9-6290-42d5-81ef-3f797aee0fdb
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs
new file mode 100644
index 000000000..275a53bcf
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Program.cs
@@ -0,0 +1,20 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var sqlServer = builder.AddSqlServer("carrental-sql-server")
+ .AddDatabase("CarRentalDb");
+
+var kafka = builder.AddKafka("carrental-kafka").WithKafkaUI();
+
+builder.AddProject("carrental-api")
+ .WithReference(sqlServer, "DefaultConnection")
+ .WithReference(kafka)
+ .WithEnvironment("Kafka__RentalTopicName", "rentals")
+ .WaitFor(sqlServer)
+ .WaitFor(kafka);
+
+builder.AddProject("carrental-generator-kafka-host")
+ .WithReference(kafka)
+ .WithEnvironment("Kafka__RentalTopicName", "rentals")
+ .WaitFor(kafka);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..a4ef35c1d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "CarRental.AppHost": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15058",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19141",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20144",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..b09d7ac12
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "None"
+ }
+ },
+ "Aspire": {
+ "Dashboard": {
+ "Frontend": {
+ "BrowserAuthMode": "Unsecured"
+ },
+ "ResourceServiceClient": {
+ "AuthMode": "Unsecured"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.json b/CarRental/CarRental/CarRental.AppHost/appsettings.json
new file mode 100644
index 000000000..d87c105e5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": ""
+ },
+ "Kafka": {
+ "RentalTopicName": "rentals"
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj
new file mode 100644
index 000000000..12163a6ec
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj
@@ -0,0 +1,15 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs
new file mode 100644
index 000000000..6be43e18d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs
@@ -0,0 +1,8 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для отображения количества арендованных автомобилей
+///
+/// Информация об автомобиле
+/// Количество прокатов этого автомобиля
+public record CarRentalCountDto(CarGetDto Car, int RentalCount);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs
new file mode 100644
index 000000000..693139711
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs
@@ -0,0 +1,13 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления автомобилей
+///
+/// Номерной знак автомобиля
+/// Цвет автомобиля
+/// Идентификатор поколения модели
+public record CarEditDto(
+ string LicensePlate,
+ string Color,
+ int ModelGenerationId
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs
new file mode 100644
index 000000000..61745bfbe
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs
@@ -0,0 +1,15 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для получения информации об автомобиле
+///
+/// Уникальный идентификатор автомобиля
+/// Номерной знак автомобиля
+/// Цвет автомобиля
+/// Информация о создании модели, включая подробные сведения о модели
+public record CarGetDto(
+ int Id,
+ string LicensePlate,
+ string Color,
+ ModelGenerationGetDto ModelGeneration
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs
new file mode 100644
index 000000000..182a33ff5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления моделей автомобилей
+///
+/// Название модели автомобиля (например, "BMW 3 Series")
+/// Тип привода (FWD, RWD, AWD, 4WD)
+/// Количество посадочных мест в автомобиле
+/// Тип кузова (Sedan, SUV, Coupe, и т.д.)
+/// Класс автомобиля (Economy, Premium, Luxury, и т.д.)
+public record CarModelEditDto(
+ string Name,
+ string DriveType,
+ int SeatsCount,
+ string BodyType,
+ string Class
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs
new file mode 100644
index 000000000..bba44edb7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs
@@ -0,0 +1,19 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для получения информации о модели автомобиля
+///
+/// Уникальный идентификатор модели автомобиля
+/// Название модели автомобиля (например, "BMW 3 Series")
+/// Тип привода (FWD, RWD, AWD, 4WD)
+/// Количество посадочных мест в автомобиле
+/// Тип кузова (Sedan, SUV, Coupe, и т.д.)
+/// Класс автомобиля (Economy, Premium, Luxury, и т.д.)
+public record CarModelGetDto(
+ int Id,
+ string Name,
+ string DriveType,
+ int SeatsCount,
+ string BodyType,
+ string Class
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs
new file mode 100644
index 000000000..4ae25b474
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs
@@ -0,0 +1,8 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для отображения сумм арендной платы клиентов
+///
+/// Информация о клиенте
+/// Общая сумма арендной платы для данного клиента
+public record ClientRentalAmountDto(ClientGetDto Client, decimal TotalAmount);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs
new file mode 100644
index 000000000..769d9deb4
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs
@@ -0,0 +1,13 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления клиентов
+///
+/// Номер водительского удостоверения
+/// Полное имя клиента
+/// Дата рождения клиента
+public record ClientEditDto(
+ string LicenseNumber,
+ string FullName,
+ DateOnly BirthDate
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs
new file mode 100644
index 000000000..3f8e1c9a2
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs
@@ -0,0 +1,15 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для получения информации о клиенте
+///
+/// Уникальный идентификатор клиента
+/// Номер водительского удостоверения
+/// Полное имя клиента
+/// Дата рождения клиента
+public record ClientGetDto(
+ int Id,
+ string LicenseNumber,
+ string FullName,
+ DateOnly BirthDate
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs
new file mode 100644
index 000000000..67260c51f
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления поколений моделей
+///
+/// Год выпуска поколения модели
+/// Объем двигателя в литрах
+/// Тип трансмиссии (MT, AT, CVT)
+/// Стоимость аренды в час
+/// Идентификатор модели автомобиля
+public record ModelGenerationEditDto(
+ int Year,
+ double EngineVolume,
+ string Transmission,
+ decimal RentalPricePerHour,
+ int ModelId
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs
new file mode 100644
index 000000000..db938281b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs
@@ -0,0 +1,19 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для получения информации о генерации модели
+///
+/// Уникальный идентификатор генерации модели
+/// Год выпуска поколения модели
+/// Объем двигателя в литрах
+/// Тип трансмиссии (MT, AT, вариатор)
+/// Стоимость аренды в час
+/// Информация о модели автомобиля
+public record ModelGenerationGetDto(
+ int Id,
+ int Year,
+ double EngineVolume,
+ string Transmission,
+ decimal RentalPricePerHour,
+ CarModelGetDto Model
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs
new file mode 100644
index 000000000..f8a168404
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs
@@ -0,0 +1,15 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления проката
+///
+/// Дата и время начала аренды
+/// Продолжительность аренды в часах
+/// Идентификатор арендованного автомобиля
+/// Идентификатор клиента
+public record RentalEditDto(
+ DateTime RentalDate,
+ int RentalHours,
+ int CarId,
+ int ClientId
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs
new file mode 100644
index 000000000..eb379969b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для получения информации об аренде
+///
+/// Уникальный идентификатор объекта аренды
+/// Дата и время начала аренды
+/// Продолжительность аренды в часах
+/// Информация об арендованном автомобиле
+/// Информация о клиенте
+public record RentalGetDto(
+ int Id,
+ DateTime RentalDate,
+ int RentalHours,
+ CarGetDto Car,
+ ClientGetDto Client
+);
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs
new file mode 100644
index 000000000..e396443e3
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs
@@ -0,0 +1,37 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using System.Runtime.ConstrainedExecution;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace CarRental.Application.Contracts;
+
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.ModelGeneration,
+ opt => opt.MapFrom(src => src.ModelGeneration));
+
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap()
+ .ForMember(dest => dest.Model,
+ opt => opt.MapFrom(src => src.Model));
+ CreateMap();
+
+ CreateMap()
+ .ForMember(dest => dest.Car,
+ opt => opt.MapFrom(src => src.Car))
+ .ForMember(dest => dest.Client,
+ opt => opt.MapFrom(src => src.Client));
+ CreateMap();
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj
new file mode 100644
index 000000000..db17823c4
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net8.0
+ CarRental.Domain
+ enable
+ enable
+
+
+
diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs
new file mode 100644
index 000000000..49117dea4
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs
@@ -0,0 +1,119 @@
+using CarRental.Domain.Entities;
+
+namespace CarRental.Domain.Data;
+
+///
+/// Fixture с тестовыми данными
+///
+public class CarRentalFixture
+{
+ public List CarModels { get; }
+ public List ModelGenerations { get; }
+ public List Cars { get; }
+ public List Clients { get; }
+ public List Rentals { get; }
+
+ public CarRentalFixture()
+ {
+ CarModels =
+ [
+ new() { Id = 1, Name = "BMW 3 Series", DriveType = "RWD", SeatsCount = 5, BodyType = "Sedan", Class = "Premium" },
+ new() { Id = 2, Name = "Ford Mustang", DriveType = "RWD", SeatsCount = 4, BodyType = "Coupe", Class = "Sports" },
+ new() { Id = 3, Name = "Honda Civic", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Compact" },
+ new() { Id = 4, Name = "Jeep Wrangler", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" },
+ new() { Id = 5, Name = "Porsche 911", DriveType = "RWD", SeatsCount = 4, BodyType = "Coupe", Class = "Luxury" },
+ new() { Id = 6, Name = "Chevrolet Tahoe", DriveType = "AWD", SeatsCount = 8, BodyType = "SUV", Class = "Full-size" },
+ new() { Id = 7, Name = "Lada Vesta", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Economy" },
+ new() { Id = 8, Name = "Subaru Outback", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Mid-size" },
+ new() { Id = 9, Name = "GAZ Gazelle Next", DriveType = "RWD", SeatsCount = 3, BodyType = "Van", Class = "Commercial" },
+ new() { Id = 10, Name = "Toyota Prius", DriveType = "FWD", SeatsCount = 5, BodyType = "Hatchback", Class = "Hybrid" },
+ new() { Id = 11, Name = "UAZ Patriot", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" },
+ new() { Id = 12, Name = "Lexus RX", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Premium" },
+ new() { Id = 13, Name = "Range Rover Sport", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Luxury" },
+ new() { Id = 14, Name = "Audi A4", DriveType = "AWD", SeatsCount = 5, BodyType = "Sedan", Class = "Premium" },
+ new() { Id = 15, Name = "Lada Niva Travel", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" }
+ ];
+
+
+ ModelGenerations =
+ [
+ new() { Id = 1, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2200, ModelId = 1 },
+ new() { Id = 2, Year = 2022, EngineVolume = 5.0, Transmission = "AT", RentalPricePerHour = 5000, ModelId = 2 },
+ new() { Id = 3, Year = 2024, EngineVolume = 1.5, Transmission = "CVT", RentalPricePerHour = 1200, ModelId = 3 },
+ new() { Id = 4, Year = 2023, EngineVolume = 3.6, Transmission = "AT", RentalPricePerHour = 2800, ModelId = 4 },
+ new() { Id = 5, Year = 2024, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 8000, ModelId = 5 },
+ new() { Id = 6, Year = 2022, EngineVolume = 5.3, Transmission = "AT", RentalPricePerHour = 3500, ModelId = 6 },
+ new() { Id = 7, Year = 2023, EngineVolume = 1.6, Transmission = "MT", RentalPricePerHour = 700, ModelId = 7 },
+ new() { Id = 8, Year = 2024, EngineVolume = 2.5, Transmission = "AT", RentalPricePerHour = 1800, ModelId = 8 },
+ new() { Id = 9, Year = 2022, EngineVolume = 2.7, Transmission = "MT", RentalPricePerHour = 1500, ModelId = 9 },
+ new() { Id = 10, Year = 2023, EngineVolume = 1.8, Transmission = "CVT", RentalPricePerHour = 1600, ModelId = 10 },
+ new() { Id = 11, Year = 2022, EngineVolume = 2.7, Transmission = "MT", RentalPricePerHour = 1400, ModelId = 11 },
+ new() { Id = 12, Year = 2024, EngineVolume = 3.5, Transmission = "AT", RentalPricePerHour = 3200, ModelId = 12 },
+ new() { Id = 13, Year = 2023, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 6000, ModelId = 13 },
+ new() { Id = 14, Year = 2024, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2800, ModelId = 14 },
+ new() { Id = 15, Year = 2023, EngineVolume = 1.7, Transmission = "MT", RentalPricePerHour = 900, ModelId = 15 }
+ ];
+
+ Cars =
+ [
+ new() { Id = 1, LicensePlate = "A001AA163", Color = "Black", ModelGenerationId = 1 },
+ new() { Id = 2, LicensePlate = "B777BC163", Color = "Red", ModelGenerationId = 2 },
+ new() { Id = 3, LicensePlate = "C123ET163", Color = "White", ModelGenerationId = 3 },
+ new() { Id = 4, LicensePlate = "E555KH163", Color = "Green", ModelGenerationId = 4 },
+ new() { Id = 5, LicensePlate = "K234MR163", Color = "Silver", ModelGenerationId = 5 },
+ new() { Id = 6, LicensePlate = "M888OA163", Color = "Gray", ModelGenerationId = 6 },
+ new() { Id = 7, LicensePlate = "N456RS163", Color = "Blue", ModelGenerationId = 7 },
+ new() { Id = 8, LicensePlate = "O789TU163", Color = "Brown", ModelGenerationId = 8 },
+ new() { Id = 9, LicensePlate = "P321XO163", Color = "White", ModelGenerationId = 9 },
+ new() { Id = 10, LicensePlate = "S654AM163", Color = "Black", ModelGenerationId = 10 },
+ new() { Id = 11, LicensePlate = "T987RE163", Color = "Orange", ModelGenerationId = 11 },
+ new() { Id = 12, LicensePlate = "U246KN163", Color = "White", ModelGenerationId = 12 },
+ new() { Id = 13, LicensePlate = "H135VT163", Color = "Black", ModelGenerationId = 13 },
+ new() { Id = 14, LicensePlate = "SH579SA163", Color = "Gray", ModelGenerationId = 14 },
+ new() { Id = 15, LicensePlate = "SCH864RO163", Color = "Blue", ModelGenerationId = 15 }
+ ];
+
+
+ Clients =
+ [
+ new() { Id = 1, LicenseNumber = "2023-001", FullName = "Alexander Smirnov", BirthDate = new DateOnly(1988, 3, 15) },
+ new() { Id = 2, LicenseNumber = "2022-045", FullName = "Marina Kovalenko", BirthDate = new DateOnly(1992, 7, 22) },
+ new() { Id = 3, LicenseNumber = "2024-012", FullName = "Denis Popov", BirthDate = new DateOnly(1995, 11, 10) },
+ new() { Id = 4, LicenseNumber = "2021-078", FullName = "Elena Vasnetsova", BirthDate = new DateOnly(1985, 5, 3) },
+ new() { Id = 5, LicenseNumber = "2023-056", FullName = "Igor Kozlovsky",BirthDate = new DateOnly(1990, 9, 30) },
+ new() { Id = 6, LicenseNumber = "2022-123", FullName = "Anna Orlova", BirthDate = new DateOnly(1993, 2, 14) },
+ new() { Id = 7, LicenseNumber = "2024-034", FullName = "Artem Belov", BirthDate = new DateOnly(1987, 8, 18) },
+ new() { Id = 8, LicenseNumber = "2021-099", FullName = "Sofia Grigorieva", BirthDate = new DateOnly(1994, 12, 25) },
+ new() { Id = 9, LicenseNumber = "2023-087", FullName = "Pavel Melnikov", BirthDate = new DateOnly(1991, 6, 7) },
+ new() { Id = 10, LicenseNumber = "2022-067", FullName = "Olga Zakharova", BirthDate = new DateOnly(1989, 4, 12) },
+ new() { Id = 11, LicenseNumber = "2024-005", FullName = "Mikhail Tikhonov", BirthDate = new DateOnly(1996, 10, 28) },
+ new() { Id = 12, LicenseNumber = "2021-112", FullName = "Ksenia Fedorova", BirthDate = new DateOnly(1986, 1, 19) },
+ new() { Id = 13, LicenseNumber = "2023-092", FullName = "Roman Sokolov", BirthDate = new DateOnly(1997, 7, 3) },
+ new() { Id = 14, LicenseNumber = "2022-031", FullName = "Tatiana Krylova", BirthDate = new DateOnly(1984, 3, 22) },
+ new() { Id = 15, LicenseNumber = "2024-021", FullName = "Andrey Davydov", BirthDate = new DateOnly(1998, 11, 15) }
+ ];
+
+ Rentals =
+ [
+ new() { Id = 1, CarId = 7, ClientId = 1, RentalDate = new DateTime(2024, 3, 1, 10, 0, 0), RentalHours = 48 },
+ new() { Id = 2, CarId = 7, ClientId = 3, RentalDate = new DateTime(2024, 2, 25, 14, 30, 0), RentalHours = 72 },
+ new() { Id = 3, CarId = 7, ClientId = 5, RentalDate = new DateTime(2024, 2, 20, 9, 15, 0), RentalHours = 24},
+ new() { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2024, 2, 27, 11, 45, 0), RentalHours = 96 },
+ new() { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2024, 2, 25, 16, 0, 0), RentalHours = 120},
+ new() { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2024, 2, 23, 13, 20, 0), RentalHours = 72},
+ new() { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2024, 2, 18, 10, 10, 0), RentalHours = 48 },
+ new() { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2024, 2, 28, 8, 30, 0), RentalHours = 36 },
+ new() { Id = 9, CarId = 4, ClientId = 9, RentalDate = new DateTime(2024, 2, 15, 12, 0, 0), RentalHours = 96 },
+ new() { Id = 10, CarId = 5, ClientId = 10, RentalDate = new DateTime(2024, 2, 28, 7, 0, 0), RentalHours = 168 },
+ new() { Id = 11, CarId = 6, ClientId = 11, RentalDate = new DateTime(2024, 2, 22, 15, 45, 0), RentalHours = 72 },
+ new() { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2024, 2, 26, 9, 20, 0), RentalHours = 48 },
+ new() { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2024, 2, 29, 22, 0, 0), RentalHours = 60 },
+ new() { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2024, 2, 24, 11, 30, 0), RentalHours = 96 },
+ new() { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2024, 2, 10, 14, 15, 0), RentalHours = 120 },
+ new() { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2024, 2, 29, 14, 0, 0), RentalHours = 48 },
+ new() { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2024, 2, 5, 16, 45, 0), RentalHours = 72 },
+ new() { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2024, 2, 12, 10, 10, 0), RentalHours = 36 },
+ new() { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2024, 2, 16, 13, 30, 0), RentalHours = 84 }
+ ];
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Car.cs b/CarRental/CarRental/CarRental.Domain/Entities/Car.cs
new file mode 100644
index 000000000..225757a16
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/Car.cs
@@ -0,0 +1,42 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Автомобиль в парке
+///
+[Table("cars")]
+public class Car
+{
+ ///
+ /// Уникальный ID автомобиля в парке
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Государственный номер
+ ///
+ [Column("license_plate")]
+ [MaxLength(20)]
+ public required string LicensePlate { get; set; }
+
+ ///
+ /// Цвет кузова
+ ///
+ [Column("color")]
+ [MaxLength(30)]
+ public required string Color { get; set; }
+
+ ///
+ /// Внешний ключ на поколение модели
+ ///
+ [Column("model_generation_id")]
+ public required int ModelGenerationId { get; set; }
+
+ ///
+ /// Ссылка на поколение модели этого автомобиля
+ ///
+ public ModelGeneration? ModelGeneration { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs b/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs
new file mode 100644
index 000000000..67883f208
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs
@@ -0,0 +1,51 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Модель автомобиля (справочник)
+///
+[Table("car_models")]
+public class CarModel
+{
+ ///
+ /// Уникальный идентификатор модели
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Название модели (например, "Toyota Camry")
+ ///
+ [Column("name")]
+ [MaxLength(50)]
+ public required string Name { get; set; }
+
+ ///
+ /// Тип привода
+ ///
+ [Column("drive_type")]
+ [MaxLength(10)]
+ public required string DriveType { get; set; }
+
+ ///
+ /// Количество посадочных мест
+ ///
+ [Column("seats_count")]
+ public required int SeatsCount { get; set; }
+
+ ///
+ /// Тип кузова
+ ///
+ [Column("body_type")]
+ [MaxLength(20)]
+ public required string BodyType { get; set; }
+
+ ///
+ /// Класс автомобиля
+ ///
+ [Column("class")]
+ [MaxLength(20)]
+ public required string Class { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Client.cs b/CarRental/CarRental/CarRental.Domain/Entities/Client.cs
new file mode 100644
index 000000000..831cf2e4b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/Client.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Клиент пункта проката
+///
+[Table("clients")]
+public class Client
+{
+ ///
+ /// Уникальный ID клиента
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Номер водительского удостоверения (уникален)
+ ///
+ [Column("license_number")]
+ [MaxLength(20)]
+ public required string LicenseNumber { get; set; }
+
+ ///
+ /// Полное имя клиента (ФИО)
+ ///
+ [Column("full_name")]
+ [MaxLength(100)]
+ public required string FullName { get; set; }
+
+ ///
+ /// Дата рождения
+ ///
+ [Column("birth_date")]
+ public required DateOnly BirthDate { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs b/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs
new file mode 100644
index 000000000..0cbdd814b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs
@@ -0,0 +1,53 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Поколение модели (справочник)
+///
+[Table("model_generations")]
+public class ModelGeneration
+{
+ ///
+ /// Уникальный идентификатор поколения
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Год выпуска
+ ///
+ [Column("year")]
+ public required int Year { get; set; }
+
+ ///
+ /// Объем двигателя в литрах
+ ///
+ [Column("engine_volume")]
+ public required double EngineVolume { get; set; }
+
+ ///
+ /// Тип коробки передач
+ ///
+ [Column("transmission")]
+ [MaxLength(10)]
+ public required string Transmission { get; set; }
+
+ ///
+ /// Стоимость аренды в час
+ ///
+ [Column("rental_price_per_hour")]
+ public required decimal RentalPricePerHour { get; set; }
+
+ ///
+ /// Внешний ключ на модель
+ ///
+ [Column("model_id")]
+ public required int ModelId { get; set; }
+
+ ///
+ /// Ссылка на модель (навигационное свойство)
+ ///
+ public CarModel? Model { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs
new file mode 100644
index 000000000..2cfafc7b7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs
@@ -0,0 +1,52 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Договор аренды (контракт)
+/// Фиксирует факт выдачи автомобиля клиенту
+///
+[Table("rentals")]
+public class Rental
+{
+ ///
+ /// Уникальный ID контракта
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Дата и время выдачи автомобиля клиенту
+ ///
+ [Column("rental_date")]
+ public required DateTime RentalDate { get; set; }
+
+ ///
+ /// Длительность аренды в часах
+ ///
+ [Column("rental_hours")]
+ public required int RentalHours { get; set; }
+
+ ///
+ /// Внешний ключ на автомобиль
+ ///
+ [Column("car_id")]
+ public required int CarId { get; set; }
+
+ ///
+ /// Внешний ключ на клиента
+ ///
+ [Column("client_id")]
+ public required int ClientId { get; set; }
+
+ ///
+ /// Ссылка на арендуемый автомобиль
+ ///
+ public Car? Car { get; set; }
+
+ ///
+ /// Ссылка на клиента, арендовавшего машину
+ ///
+ public Client? Client { get; set; }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs
new file mode 100644
index 000000000..26c2c675c
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs
@@ -0,0 +1,16 @@
+using System.Linq.Expressions;
+
+namespace CarRental.Domain.Interfaces;
+
+public interface IRepository where T : class
+{
+ public Task GetByIdAsync(int id);
+ public Task> GetAllAsync();
+ public Task AddAsync(T entity);
+ public Task UpdateAsync(T entity);
+ public Task DeleteAsync(int id);
+
+ public Task GetByIdAsync(int id, Func, IQueryable>? include = null);
+ public Task> GetAllAsync(Func, IQueryable>? include = null);
+ public IQueryable GetQueryable(Func, IQueryable>? include = null);
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/CarRental.Generator.Kafka.Host.csproj b/CarRental/CarRental/CarRental.Generator.Kafka.Host/CarRental.Generator.Kafka.Host.csproj
new file mode 100644
index 000000000..25b298417
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/CarRental.Generator.Kafka.Host.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Controllers/GeneratorController.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Controllers/GeneratorController.cs
new file mode 100644
index 000000000..f3ae637fc
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Controllers/GeneratorController.cs
@@ -0,0 +1,77 @@
+using CarRental.Application.Contracts.Dto;
+using CarRental.Generator.Kafka.Host.Generator;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.Generator.Kafka.Host.Controllers;
+
+///
+/// Kafka
+///
+/// Kafka producer
+///
+[ApiController]
+[Route("api/[controller]")]
+public class GeneratorController(
+ RentalKafkaProducer producer,
+ ILogger logger) : ControllerBase
+{
+ ///
+ /// Kafka
+ ///
+ /// DTO
+ ///
+ ///
+ ///
+ /// DTO
+ [HttpPost("rentals")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ [ProducesResponseType(500)]
+ public async Task>> GenerateRentals(
+ [FromQuery] int listSize,
+ [FromQuery] int batchSize,
+ [FromQuery] int delayMs,
+ CancellationToken cancellationToken)
+ {
+ logger.LogInformation("{method} called with listSize={listSize} batchSize={batchSize} delayMs={delayMs}", nameof(GenerateRentals), listSize, batchSize, delayMs);
+
+ if (listSize <= 0 || listSize > 10000)
+ return BadRequest("listSize must be between 1 and 10000");
+
+ if (batchSize <= 0 || listSize > 10000)
+ return BadRequest("batchSize must be between 1 and 10000");
+
+ try
+ {
+ var items = RentalGenerator.Generate(listSize);
+
+ foreach (var batch in items.Chunk(batchSize))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await producer.SendAsync([.. batch], cancellationToken);
+
+ await Task.Delay(delayMs, cancellationToken);
+ }
+
+ logger.LogInformation(
+ "{method} executed successfully listSize={listSize} batchSize={batchSize} delayMs={delayMs}",
+ nameof(GenerateRentals),
+ listSize,
+ batchSize,
+ delayMs);
+
+ return Ok(items);
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ logger.LogWarning("{method} cancelled by request", nameof(GenerateRentals));
+ return BadRequest("Request cancelled");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An exception happened during {method}", nameof(GenerateRentals));
+ return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Generator/RentalGenerator.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Generator/RentalGenerator.cs
new file mode 100644
index 000000000..ddb700447
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Generator/RentalGenerator.cs
@@ -0,0 +1,25 @@
+using Bogus;
+using CarRental.Application.Contracts.Dto;
+
+namespace CarRental.Generator.Kafka.Host.Generator;
+
+///
+/// Генератор тестовых DTO записей об аренде для отправки в Kafka
+///
+public class RentalGenerator
+{
+ ///
+ /// Сгенерировать список DTO для создания или обновления записей об аренде
+ ///
+ /// Количество генерируемых DTO
+ /// Список DTO для создания или обновления записей об аренде
+ public static IList Generate(int count) =>
+ new Faker()
+ .CustomInstantiator(f => new RentalEditDto(
+ RentalDate: f.Date.Between(DateTime.Now, DateTime.Now.AddMonths(1)),
+ RentalHours: f.Random.Int(1, 24),
+ CarId: f.Random.Int(1, 15),
+ ClientId: f.Random.Int(1, 15)
+ ))
+ .Generate(count);
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Program.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Program.cs
new file mode 100644
index 000000000..229dda8fb
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Program.cs
@@ -0,0 +1,57 @@
+using CarRental.Application.Contracts.Dto;
+using CarRental.Generator.Kafka.Host;
+using CarRental.Generator.Kafka.Host.Serializers;
+using Confluent.Kafka;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddKafkaProducer>("carrental-kafka",
+ configureBuilder: builder =>
+ {
+ builder.SetKeySerializer(new GuidKeySerializer());
+ builder.SetValueSerializer(new RentalValueSerializer());
+ },
+ configureSettings: settings =>
+ {
+ settings.Config.Acks = Acks.All;
+ }
+);
+
+builder.Services.AddScoped();
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("CarRental"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Properties/launchSettings.json b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..0e3a21f24
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:49315",
+ "sslPort": 44324
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5153",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7007;http://localhost:5153",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/RentalKafkaProducer.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/RentalKafkaProducer.cs
new file mode 100644
index 000000000..66ea71cf0
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/RentalKafkaProducer.cs
@@ -0,0 +1,63 @@
+using CarRental.Application.Contracts.Dto;
+using Confluent.Kafka;
+
+namespace CarRental.Generator.Kafka.Host;
+
+///
+/// Kafka producer для отправки пачек DTO записей об аренде в указанный топик
+///
+/// Конфигурация для чтения Kafka настроек
+/// Kafka producer
+/// Логгер
+public class RentalKafkaProducer(
+ IConfiguration configuration,
+ IProducer> producer,
+ ILogger logger)
+{
+ private readonly string _topic = configuration.GetSection("Kafka")["RentalTopicName"] ?? throw new KeyNotFoundException("RentalTopicName section of Kafka is missing");
+
+ ///
+ /// Отправить пачку DTO записей об аренде в Kafka
+ ///
+ /// Пачка DTO для отправки
+ /// Токен отмены
+ public async Task SendAsync(IList batch, CancellationToken cancellationToken = default)
+ {
+ if (batch is null || batch.Count == 0)
+ {
+ logger.LogWarning("Skipping send because batch is empty");
+ return;
+ }
+
+ var key = Guid.NewGuid();
+
+ try
+ {
+ logger.LogInformation("Sending a batch of {count} contracts to {topic} key={key}", batch.Count, _topic, key);
+
+ var message = new Message>
+ {
+ Key = key,
+ Value = batch
+ };
+
+ var delivery = await producer.ProduceAsync(_topic, message, cancellationToken);
+
+ logger.LogInformation(
+ "Batch sent to {topic} partition={partition} offset={offset} key={key} count={count}",
+ delivery.Topic,
+ delivery.Partition.Value,
+ delivery.Offset.Value,
+ key,
+ batch.Count);
+ }
+ catch (ProduceException> ex)
+ {
+ logger.LogError(ex, "Kafka produce failed topic={topic} reason={reason} key={key} count={count}", _topic, ex.Error.Reason, key, batch.Count);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Exception occurred during sending a batch of {count} contracts to {topic} key={key}", batch.Count, _topic, key);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/GuidKeySerializer.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/GuidKeySerializer.cs
new file mode 100644
index 000000000..083290a08
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/GuidKeySerializer.cs
@@ -0,0 +1,18 @@
+using Confluent.Kafka;
+
+namespace CarRental.Generator.Kafka.Host.Serializers;
+
+///
+/// Сериализатор Kafka ключа Guid в бинарное представление
+///
+public sealed class GuidKeySerializer : ISerializer
+{
+ ///
+ /// Сериализовать ключ Guid в массив байт
+ ///
+ /// Значение ключа
+ /// Контекст сериализации
+ /// Бинарное представление Guid
+ public byte[] Serialize(Guid data, SerializationContext context)
+ => data.ToByteArray();
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/RentalValueSerializer.cs b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/RentalValueSerializer.cs
new file mode 100644
index 000000000..b09d539df
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/Serializers/RentalValueSerializer.cs
@@ -0,0 +1,20 @@
+using CarRental.Application.Contracts.Dto;
+using Confluent.Kafka;
+using System.Text.Json;
+
+namespace CarRental.Generator.Kafka.Host.Serializers;
+
+///
+/// JSON сериализатор Kafka значения для списка DTO записей об аренде
+///
+public class RentalValueSerializer : ISerializer>
+{
+ ///
+ /// Сериализовать список DTO записей об аренде в JSON массив байт
+ ///
+ /// Список DTO записей об аренде
+ /// Контекст сериализации
+ /// JSON в UTF-8
+ public byte[] Serialize(IList data, SerializationContext context)
+ => JsonSerializer.SerializeToUtf8Bytes(data);
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.Development.json b/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.json b/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Kafka.Host/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Kafka/CarRental.Infrastructure.Kafka.csproj b/CarRental/CarRental/CarRental.Infrastructure.Kafka/CarRental.Infrastructure.Kafka.csproj
new file mode 100644
index 000000000..7eac18ecb
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Kafka/CarRental.Infrastructure.Kafka.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/GuidKeyDeserializer.cs b/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/GuidKeyDeserializer.cs
new file mode 100644
index 000000000..9c45072ec
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/GuidKeyDeserializer.cs
@@ -0,0 +1,27 @@
+using Confluent.Kafka;
+
+namespace CarRental.Infrastructure.Kafka.Deserializers;
+
+///
+/// Десериализатор Kafka ключа Guid из бинарного представления
+///
+public class GuidKeyDeserializer : IDeserializer
+{
+ ///
+ /// Десериализовать ключ сообщения в Guid
+ ///
+ /// Сырые байты ключа
+ /// Признак отсутствия ключа
+ /// Контекст десериализации
+ /// Десериализованный Guid ключ
+ public Guid Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context)
+ {
+ if (isNull || data.IsEmpty)
+ return Guid.Empty;
+
+ if (data.Length != 16)
+ throw new InvalidOperationException($"Invalid Guid key length={data.Length} expected 16");
+
+ return new Guid(data);
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/RentalValueDeserializer.cs b/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/RentalValueDeserializer.cs
new file mode 100644
index 000000000..bb8ff051b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Kafka/Deserializers/RentalValueDeserializer.cs
@@ -0,0 +1,26 @@
+using CarRental.Application.Contracts.Dto;
+using Confluent.Kafka;
+using System.Text.Json;
+
+namespace CarRental.Infrastructure.Kafka.Deserializers;
+
+///
+/// JSON десериализатор Kafka сообщений для списка DTO записей об аренде машин
+///
+public class RentalValueDeserializer : IDeserializer>
+{
+ ///
+ /// Десериализовать список DTO записей об аренде из массива байт
+ ///
+ /// байты значения Kafka сообщения
+ /// Признак отсутствия значения
+ /// Контекст десериализации
+ /// Список DTO записей об аренде
+ public IList Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context)
+ {
+ if (isNull || data.IsEmpty)
+ return [];
+
+ return JsonSerializer.Deserialize>(data) ?? [];
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Kafka/RentalKafkaConsumer.cs b/CarRental/CarRental/CarRental.Infrastructure.Kafka/RentalKafkaConsumer.cs
new file mode 100644
index 000000000..e59dfdb9c
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Kafka/RentalKafkaConsumer.cs
@@ -0,0 +1,157 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Confluent.Kafka;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CarRental.Infrastructure.Kafka;
+
+///
+/// Kafka consumer для обработки сообщений с записями об аренде машин
+///
+/// Экземпляр Kafka consumer
+/// Фабрика scope для получения scoped зависимостей
+/// Конфигурация для чтения Kafka настроек
+/// Логгер
+public class RentalKafkaConsumer(
+ IConsumer> consumer,
+ ILogger logger,
+ IConfiguration configuration,
+ IMapper mapper,
+ IServiceScopeFactory scopeFactory) : BackgroundService
+{
+ private readonly string _topic = configuration.GetSection("Kafka")["RentalTopicName"] ?? throw new KeyNotFoundException("RentalTopicName section of Kafka is missing");
+
+ ///
+ /// Запуск цикла чтения Kafka сообщений и создания записей об аренде машин
+ ///
+ /// Токен отмены
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await Task.Yield();
+
+ try
+ {
+ consumer.Subscribe(_topic);
+
+ logger.LogInformation("Consumer {consumer} subscribed to topic {topic}", consumer.Name, _topic);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to subscribe consumer {consumer} to topic {topic}", consumer.Name, _topic);
+ return;
+ }
+
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ ConsumeResult>? msg = null;
+
+ try
+ {
+ msg = consumer.Consume(stoppingToken);
+
+ var payload = msg?.Message?.Value;
+ if (payload is null || payload.Count == 0)
+ continue;
+
+ await ProcessMessage(payload, msg!.Message.Key, stoppingToken);
+
+ consumer.Commit(msg);
+
+ logger.LogInformation("Committed message {key} from topic {topic} via consumer {consumer}", msg.Message.Key, _topic, consumer.Name);
+ }
+ catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart)
+ {
+ logger.LogWarning("Topic {topic} is not available yet, retrying...", _topic);
+ await Task.Delay(2000, stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ logger.LogInformation("Operation was canceled on consumer {consumer}", consumer.Name);
+ break;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to consume or process message from topic {topic}", _topic);
+ await Task.Delay(1000, stoppingToken);
+ }
+ }
+ }
+ finally
+ {
+ try
+ {
+ consumer.Close();
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Error during consumer close");
+ }
+ }
+ }
+
+ ///
+ /// Обработать одно Kafka сообщение и создать запись для валидных контрактов
+ ///
+ /// Список DTO записей об аренде
+ /// Ключ сообщения Kafka
+ /// Токен отмены
+ private async Task ProcessMessage(IList payload, Guid messageKey, CancellationToken stoppingToken)
+ {
+ logger.LogInformation("Processing message {key} from topic {topic} with {count} contracts", messageKey, _topic, payload.Count);
+
+ using var scope = scopeFactory.CreateScope();
+
+ var rentalRepo = scope.ServiceProvider.GetRequiredService>();
+ var carRepo = scope.ServiceProvider.GetRequiredService>();
+ var clientRepo = scope.ServiceProvider.GetRequiredService>();
+
+ var carIds = payload.Select(p => p.CarId).Distinct().ToList();
+ var clientIds = payload.Select(p => p.ClientId).Distinct().ToList();
+
+ var existingCarIds = await carRepo.GetQueryable()
+ .Where(c => carIds.Contains(c.Id))
+ .Select(c => c.Id)
+ .ToListAsync(stoppingToken);
+
+ var existingClientIds = await clientRepo.GetQueryable()
+ .Where(c => clientIds.Contains(c.Id))
+ .Select(c => c.Id)
+ .ToListAsync(stoppingToken);
+
+ var validCarIds = existingCarIds.ToHashSet();
+ var validClientIds = existingClientIds.ToHashSet();
+
+ foreach (var dto in payload)
+ {
+ stoppingToken.ThrowIfCancellationRequested();
+
+ if (!validCarIds.Contains(dto.CarId) || !validClientIds.Contains(dto.ClientId))
+ {
+ logger.LogWarning("Skipping Rental contract from message {key} because related car or client doesn't exist, CarId={carId} ClientId={clientId}",
+ messageKey, dto.CarId, dto.ClientId);
+ continue;
+ }
+
+ try
+ {
+ var rental = mapper.Map(dto);
+
+ await rentalRepo.AddAsync(rental);
+
+ logger.LogInformation("Created rental from message {key} CarId={carId} ClientId={clientId}", messageKey, dto.CarId, dto.ClientId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to create rental from message {key} CarId={carId} ClientId={clientId}", messageKey, dto.CarId, dto.ClientId);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj
new file mode 100644
index 000000000..0513ff5aa
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj
@@ -0,0 +1,13 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs
new file mode 100644
index 000000000..52321e4f6
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs
@@ -0,0 +1,889 @@
+//
+using System;
+using CarRental.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CarRental.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260220055307_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Car", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("color");
+
+ b.Property("LicensePlate")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("license_plate");
+
+ b.Property("ModelGenerationId")
+ .HasColumnType("int")
+ .HasColumnName("model_generation_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModelGenerationId");
+
+ b.ToTable("cars");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Color = "Black",
+ LicensePlate = "A001AA163",
+ ModelGenerationId = 1
+ },
+ new
+ {
+ Id = 2,
+ Color = "Red",
+ LicensePlate = "B777BC163",
+ ModelGenerationId = 2
+ },
+ new
+ {
+ Id = 3,
+ Color = "White",
+ LicensePlate = "C123ET163",
+ ModelGenerationId = 3
+ },
+ new
+ {
+ Id = 4,
+ Color = "Green",
+ LicensePlate = "E555KH163",
+ ModelGenerationId = 4
+ },
+ new
+ {
+ Id = 5,
+ Color = "Silver",
+ LicensePlate = "K234MR163",
+ ModelGenerationId = 5
+ },
+ new
+ {
+ Id = 6,
+ Color = "Gray",
+ LicensePlate = "M888OA163",
+ ModelGenerationId = 6
+ },
+ new
+ {
+ Id = 7,
+ Color = "Blue",
+ LicensePlate = "N456RS163",
+ ModelGenerationId = 7
+ },
+ new
+ {
+ Id = 8,
+ Color = "Brown",
+ LicensePlate = "O789TU163",
+ ModelGenerationId = 8
+ },
+ new
+ {
+ Id = 9,
+ Color = "White",
+ LicensePlate = "P321XO163",
+ ModelGenerationId = 9
+ },
+ new
+ {
+ Id = 10,
+ Color = "Black",
+ LicensePlate = "S654AM163",
+ ModelGenerationId = 10
+ },
+ new
+ {
+ Id = 11,
+ Color = "Orange",
+ LicensePlate = "T987RE163",
+ ModelGenerationId = 11
+ },
+ new
+ {
+ Id = 12,
+ Color = "White",
+ LicensePlate = "U246KN163",
+ ModelGenerationId = 12
+ },
+ new
+ {
+ Id = 13,
+ Color = "Black",
+ LicensePlate = "H135VT163",
+ ModelGenerationId = 13
+ },
+ new
+ {
+ Id = 14,
+ Color = "Gray",
+ LicensePlate = "SH579SA163",
+ ModelGenerationId = 14
+ },
+ new
+ {
+ Id = 15,
+ Color = "Blue",
+ LicensePlate = "SCH864RO163",
+ ModelGenerationId = 15
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.CarModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BodyType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("body_type");
+
+ b.Property("Class")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("class");
+
+ b.Property("DriveType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("drive_type");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)")
+ .HasColumnName("name");
+
+ b.Property("SeatsCount")
+ .HasColumnType("int")
+ .HasColumnName("seats_count");
+
+ b.HasKey("Id");
+
+ b.ToTable("car_models");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BodyType = "Sedan",
+ Class = "Premium",
+ DriveType = "RWD",
+ Name = "BMW 3 Series",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 2,
+ BodyType = "Coupe",
+ Class = "Sports",
+ DriveType = "RWD",
+ Name = "Ford Mustang",
+ SeatsCount = 4
+ },
+ new
+ {
+ Id = 3,
+ BodyType = "Sedan",
+ Class = "Compact",
+ DriveType = "FWD",
+ Name = "Honda Civic",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 4,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "Jeep Wrangler",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 5,
+ BodyType = "Coupe",
+ Class = "Luxury",
+ DriveType = "RWD",
+ Name = "Porsche 911",
+ SeatsCount = 4
+ },
+ new
+ {
+ Id = 6,
+ BodyType = "SUV",
+ Class = "Full-size",
+ DriveType = "AWD",
+ Name = "Chevrolet Tahoe",
+ SeatsCount = 8
+ },
+ new
+ {
+ Id = 7,
+ BodyType = "Sedan",
+ Class = "Economy",
+ DriveType = "FWD",
+ Name = "Lada Vesta",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 8,
+ BodyType = "SUV",
+ Class = "Mid-size",
+ DriveType = "AWD",
+ Name = "Subaru Outback",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 9,
+ BodyType = "Van",
+ Class = "Commercial",
+ DriveType = "RWD",
+ Name = "GAZ Gazelle Next",
+ SeatsCount = 3
+ },
+ new
+ {
+ Id = 10,
+ BodyType = "Hatchback",
+ Class = "Hybrid",
+ DriveType = "FWD",
+ Name = "Toyota Prius",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 11,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "UAZ Patriot",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 12,
+ BodyType = "SUV",
+ Class = "Premium",
+ DriveType = "AWD",
+ Name = "Lexus RX",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 13,
+ BodyType = "SUV",
+ Class = "Luxury",
+ DriveType = "AWD",
+ Name = "Range Rover Sport",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 14,
+ BodyType = "Sedan",
+ Class = "Premium",
+ DriveType = "AWD",
+ Name = "Audi A4",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 15,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "Lada Niva Travel",
+ SeatsCount = 5
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Client", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("date")
+ .HasColumnName("birth_date");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("full_name");
+
+ b.Property("LicenseNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("license_number");
+
+ b.HasKey("Id");
+
+ b.ToTable("clients");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BirthDate = new DateOnly(1988, 3, 15),
+ FullName = "Alexander Smirnov",
+ LicenseNumber = "2023-001"
+ },
+ new
+ {
+ Id = 2,
+ BirthDate = new DateOnly(1992, 7, 22),
+ FullName = "Marina Kovalenko",
+ LicenseNumber = "2022-045"
+ },
+ new
+ {
+ Id = 3,
+ BirthDate = new DateOnly(1995, 11, 10),
+ FullName = "Denis Popov",
+ LicenseNumber = "2024-012"
+ },
+ new
+ {
+ Id = 4,
+ BirthDate = new DateOnly(1985, 5, 3),
+ FullName = "Elena Vasnetsova",
+ LicenseNumber = "2021-078"
+ },
+ new
+ {
+ Id = 5,
+ BirthDate = new DateOnly(1990, 9, 30),
+ FullName = "Igor Kozlovsky",
+ LicenseNumber = "2023-056"
+ },
+ new
+ {
+ Id = 6,
+ BirthDate = new DateOnly(1993, 2, 14),
+ FullName = "Anna Orlova",
+ LicenseNumber = "2022-123"
+ },
+ new
+ {
+ Id = 7,
+ BirthDate = new DateOnly(1987, 8, 18),
+ FullName = "Artem Belov",
+ LicenseNumber = "2024-034"
+ },
+ new
+ {
+ Id = 8,
+ BirthDate = new DateOnly(1994, 12, 25),
+ FullName = "Sofia Grigorieva",
+ LicenseNumber = "2021-099"
+ },
+ new
+ {
+ Id = 9,
+ BirthDate = new DateOnly(1991, 6, 7),
+ FullName = "Pavel Melnikov",
+ LicenseNumber = "2023-087"
+ },
+ new
+ {
+ Id = 10,
+ BirthDate = new DateOnly(1989, 4, 12),
+ FullName = "Olga Zakharova",
+ LicenseNumber = "2022-067"
+ },
+ new
+ {
+ Id = 11,
+ BirthDate = new DateOnly(1996, 10, 28),
+ FullName = "Mikhail Tikhonov",
+ LicenseNumber = "2024-005"
+ },
+ new
+ {
+ Id = 12,
+ BirthDate = new DateOnly(1986, 1, 19),
+ FullName = "Ksenia Fedorova",
+ LicenseNumber = "2021-112"
+ },
+ new
+ {
+ Id = 13,
+ BirthDate = new DateOnly(1997, 7, 3),
+ FullName = "Roman Sokolov",
+ LicenseNumber = "2023-092"
+ },
+ new
+ {
+ Id = 14,
+ BirthDate = new DateOnly(1984, 3, 22),
+ FullName = "Tatiana Krylova",
+ LicenseNumber = "2022-031"
+ },
+ new
+ {
+ Id = 15,
+ BirthDate = new DateOnly(1998, 11, 15),
+ FullName = "Andrey Davydov",
+ LicenseNumber = "2024-021"
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EngineVolume")
+ .HasColumnType("float")
+ .HasColumnName("engine_volume");
+
+ b.Property("ModelId")
+ .HasColumnType("int")
+ .HasColumnName("model_id");
+
+ b.Property("RentalPricePerHour")
+ .HasColumnType("decimal(18,2)")
+ .HasColumnName("rental_price_per_hour");
+
+ b.Property("Transmission")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("transmission");
+
+ b.Property("Year")
+ .HasColumnType("int")
+ .HasColumnName("year");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModelId");
+
+ b.ToTable("model_generations");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ EngineVolume = 2.0,
+ ModelId = 1,
+ RentalPricePerHour = 2200m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 2,
+ EngineVolume = 5.0,
+ ModelId = 2,
+ RentalPricePerHour = 5000m,
+ Transmission = "AT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 3,
+ EngineVolume = 1.5,
+ ModelId = 3,
+ RentalPricePerHour = 1200m,
+ Transmission = "CVT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 4,
+ EngineVolume = 3.6000000000000001,
+ ModelId = 4,
+ RentalPricePerHour = 2800m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 5,
+ EngineVolume = 3.0,
+ ModelId = 5,
+ RentalPricePerHour = 8000m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 6,
+ EngineVolume = 5.2999999999999998,
+ ModelId = 6,
+ RentalPricePerHour = 3500m,
+ Transmission = "AT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 7,
+ EngineVolume = 1.6000000000000001,
+ ModelId = 7,
+ RentalPricePerHour = 700m,
+ Transmission = "MT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 8,
+ EngineVolume = 2.5,
+ ModelId = 8,
+ RentalPricePerHour = 1800m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 9,
+ EngineVolume = 2.7000000000000002,
+ ModelId = 9,
+ RentalPricePerHour = 1500m,
+ Transmission = "MT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 10,
+ EngineVolume = 1.8,
+ ModelId = 10,
+ RentalPricePerHour = 1600m,
+ Transmission = "CVT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 11,
+ EngineVolume = 2.7000000000000002,
+ ModelId = 11,
+ RentalPricePerHour = 1400m,
+ Transmission = "MT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 12,
+ EngineVolume = 3.5,
+ ModelId = 12,
+ RentalPricePerHour = 3200m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 13,
+ EngineVolume = 3.0,
+ ModelId = 13,
+ RentalPricePerHour = 6000m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 14,
+ EngineVolume = 2.0,
+ ModelId = 14,
+ RentalPricePerHour = 2800m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 15,
+ EngineVolume = 1.7,
+ ModelId = 15,
+ RentalPricePerHour = 900m,
+ Transmission = "MT",
+ Year = 2023
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Rental", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CarId")
+ .HasColumnType("int")
+ .HasColumnName("car_id");
+
+ b.Property("ClientId")
+ .HasColumnType("int")
+ .HasColumnName("client_id");
+
+ b.Property("RentalDate")
+ .HasColumnType("datetime2")
+ .HasColumnName("rental_date");
+
+ b.Property("RentalHours")
+ .HasColumnType("int")
+ .HasColumnName("rental_hours");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CarId");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("rentals");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ CarId = 7,
+ ClientId = 1,
+ RentalDate = new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 2,
+ CarId = 7,
+ ClientId = 3,
+ RentalDate = new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 3,
+ CarId = 7,
+ ClientId = 5,
+ RentalDate = new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 24
+ },
+ new
+ {
+ Id = 4,
+ CarId = 1,
+ ClientId = 2,
+ RentalDate = new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 5,
+ CarId = 1,
+ ClientId = 4,
+ RentalDate = new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 120
+ },
+ new
+ {
+ Id = 6,
+ CarId = 2,
+ ClientId = 6,
+ RentalDate = new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 7,
+ CarId = 2,
+ ClientId = 8,
+ RentalDate = new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 8,
+ CarId = 3,
+ ClientId = 7,
+ RentalDate = new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 36
+ },
+ new
+ {
+ Id = 9,
+ CarId = 4,
+ ClientId = 9,
+ RentalDate = new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 10,
+ CarId = 5,
+ ClientId = 10,
+ RentalDate = new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 168
+ },
+ new
+ {
+ Id = 11,
+ CarId = 6,
+ ClientId = 11,
+ RentalDate = new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 12,
+ CarId = 8,
+ ClientId = 12,
+ RentalDate = new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 13,
+ CarId = 9,
+ ClientId = 13,
+ RentalDate = new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 60
+ },
+ new
+ {
+ Id = 14,
+ CarId = 10,
+ ClientId = 14,
+ RentalDate = new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 15,
+ CarId = 11,
+ ClientId = 15,
+ RentalDate = new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 120
+ },
+ new
+ {
+ Id = 16,
+ CarId = 12,
+ ClientId = 1,
+ RentalDate = new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 17,
+ CarId = 13,
+ ClientId = 2,
+ RentalDate = new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 18,
+ CarId = 14,
+ ClientId = 3,
+ RentalDate = new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 36
+ },
+ new
+ {
+ Id = 19,
+ CarId = 15,
+ ClientId = 4,
+ RentalDate = new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 84
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Car", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.ModelGeneration", "ModelGeneration")
+ .WithMany()
+ .HasForeignKey("ModelGenerationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ModelGeneration");
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.CarModel", "Model")
+ .WithMany()
+ .HasForeignKey("ModelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Model");
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Rental", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.Car", "Car")
+ .WithMany()
+ .HasForeignKey("CarId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("CarRental.Domain.Entities.Client", "Client")
+ .WithMany()
+ .HasForeignKey("ClientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Car");
+
+ b.Navigation("Client");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs
new file mode 100644
index 000000000..5c5f1c8d0
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs
@@ -0,0 +1,274 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace CarRental.Infrastructure.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "car_models",
+ columns: table => new
+ {
+ id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
+ drive_type = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false),
+ seats_count = table.Column(type: "int", nullable: false),
+ body_type = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false),
+ @class = table.Column(name: "class", type: "nvarchar(20)", maxLength: 20, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_car_models", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "clients",
+ columns: table => new
+ {
+ id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ license_number = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false),
+ full_name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
+ birth_date = table.Column(type: "date", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_clients", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "model_generations",
+ columns: table => new
+ {
+ id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ year = table.Column(type: "int", nullable: false),
+ engine_volume = table.Column(type: "float", nullable: false),
+ transmission = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false),
+ rental_price_per_hour = table.Column(type: "decimal(18,2)", nullable: false),
+ model_id = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_model_generations", x => x.id);
+ table.ForeignKey(
+ name: "FK_model_generations_car_models_model_id",
+ column: x => x.model_id,
+ principalTable: "car_models",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "cars",
+ columns: table => new
+ {
+ id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ license_plate = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false),
+ color = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false),
+ model_generation_id = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_cars", x => x.id);
+ table.ForeignKey(
+ name: "FK_cars_model_generations_model_generation_id",
+ column: x => x.model_generation_id,
+ principalTable: "model_generations",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "rentals",
+ columns: table => new
+ {
+ id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ rental_date = table.Column(type: "datetime2", nullable: false),
+ rental_hours = table.Column(type: "int", nullable: false),
+ car_id = table.Column(type: "int", nullable: false),
+ client_id = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_rentals", x => x.id);
+ table.ForeignKey(
+ name: "FK_rentals_cars_car_id",
+ column: x => x.car_id,
+ principalTable: "cars",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_rentals_clients_client_id",
+ column: x => x.client_id,
+ principalTable: "clients",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "car_models",
+ columns: new[] { "id", "body_type", "class", "drive_type", "name", "seats_count" },
+ values: new object[,]
+ {
+ { 1, "Sedan", "Premium", "RWD", "BMW 3 Series", 5 },
+ { 2, "Coupe", "Sports", "RWD", "Ford Mustang", 4 },
+ { 3, "Sedan", "Compact", "FWD", "Honda Civic", 5 },
+ { 4, "SUV", "Off-road", "4WD", "Jeep Wrangler", 5 },
+ { 5, "Coupe", "Luxury", "RWD", "Porsche 911", 4 },
+ { 6, "SUV", "Full-size", "AWD", "Chevrolet Tahoe", 8 },
+ { 7, "Sedan", "Economy", "FWD", "Lada Vesta", 5 },
+ { 8, "SUV", "Mid-size", "AWD", "Subaru Outback", 5 },
+ { 9, "Van", "Commercial", "RWD", "GAZ Gazelle Next", 3 },
+ { 10, "Hatchback", "Hybrid", "FWD", "Toyota Prius", 5 },
+ { 11, "SUV", "Off-road", "4WD", "UAZ Patriot", 5 },
+ { 12, "SUV", "Premium", "AWD", "Lexus RX", 5 },
+ { 13, "SUV", "Luxury", "AWD", "Range Rover Sport", 5 },
+ { 14, "Sedan", "Premium", "AWD", "Audi A4", 5 },
+ { 15, "SUV", "Off-road", "4WD", "Lada Niva Travel", 5 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "clients",
+ columns: new[] { "id", "birth_date", "full_name", "license_number" },
+ values: new object[,]
+ {
+ { 1, new DateOnly(1988, 3, 15), "Alexander Smirnov", "2023-001" },
+ { 2, new DateOnly(1992, 7, 22), "Marina Kovalenko", "2022-045" },
+ { 3, new DateOnly(1995, 11, 10), "Denis Popov", "2024-012" },
+ { 4, new DateOnly(1985, 5, 3), "Elena Vasnetsova", "2021-078" },
+ { 5, new DateOnly(1990, 9, 30), "Igor Kozlovsky", "2023-056" },
+ { 6, new DateOnly(1993, 2, 14), "Anna Orlova", "2022-123" },
+ { 7, new DateOnly(1987, 8, 18), "Artem Belov", "2024-034" },
+ { 8, new DateOnly(1994, 12, 25), "Sofia Grigorieva", "2021-099" },
+ { 9, new DateOnly(1991, 6, 7), "Pavel Melnikov", "2023-087" },
+ { 10, new DateOnly(1989, 4, 12), "Olga Zakharova", "2022-067" },
+ { 11, new DateOnly(1996, 10, 28), "Mikhail Tikhonov", "2024-005" },
+ { 12, new DateOnly(1986, 1, 19), "Ksenia Fedorova", "2021-112" },
+ { 13, new DateOnly(1997, 7, 3), "Roman Sokolov", "2023-092" },
+ { 14, new DateOnly(1984, 3, 22), "Tatiana Krylova", "2022-031" },
+ { 15, new DateOnly(1998, 11, 15), "Andrey Davydov", "2024-021" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "model_generations",
+ columns: new[] { "id", "engine_volume", "model_id", "rental_price_per_hour", "transmission", "year" },
+ values: new object[,]
+ {
+ { 1, 2.0, 1, 2200m, "AT", 2023 },
+ { 2, 5.0, 2, 5000m, "AT", 2022 },
+ { 3, 1.5, 3, 1200m, "CVT", 2024 },
+ { 4, 3.6000000000000001, 4, 2800m, "AT", 2023 },
+ { 5, 3.0, 5, 8000m, "AT", 2024 },
+ { 6, 5.2999999999999998, 6, 3500m, "AT", 2022 },
+ { 7, 1.6000000000000001, 7, 700m, "MT", 2023 },
+ { 8, 2.5, 8, 1800m, "AT", 2024 },
+ { 9, 2.7000000000000002, 9, 1500m, "MT", 2022 },
+ { 10, 1.8, 10, 1600m, "CVT", 2023 },
+ { 11, 2.7000000000000002, 11, 1400m, "MT", 2022 },
+ { 12, 3.5, 12, 3200m, "AT", 2024 },
+ { 13, 3.0, 13, 6000m, "AT", 2023 },
+ { 14, 2.0, 14, 2800m, "AT", 2024 },
+ { 15, 1.7, 15, 900m, "MT", 2023 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "cars",
+ columns: new[] { "id", "color", "license_plate", "model_generation_id" },
+ values: new object[,]
+ {
+ { 1, "Black", "A001AA163", 1 },
+ { 2, "Red", "B777BC163", 2 },
+ { 3, "White", "C123ET163", 3 },
+ { 4, "Green", "E555KH163", 4 },
+ { 5, "Silver", "K234MR163", 5 },
+ { 6, "Gray", "M888OA163", 6 },
+ { 7, "Blue", "N456RS163", 7 },
+ { 8, "Brown", "O789TU163", 8 },
+ { 9, "White", "P321XO163", 9 },
+ { 10, "Black", "S654AM163", 10 },
+ { 11, "Orange", "T987RE163", 11 },
+ { 12, "White", "U246KN163", 12 },
+ { 13, "Black", "H135VT163", 13 },
+ { 14, "Gray", "SH579SA163", 14 },
+ { 15, "Blue", "SCH864RO163", 15 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "rentals",
+ columns: new[] { "id", "car_id", "client_id", "rental_date", "rental_hours" },
+ values: new object[,]
+ {
+ { 1, 7, 1, new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 2, 7, 3, new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 3, 7, 5, new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), 24 },
+ { 4, 1, 2, new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 5, 1, 4, new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified), 120 },
+ { 6, 2, 6, new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 7, 2, 8, new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 8, 3, 7, new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), 36 },
+ { 9, 4, 9, new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 10, 5, 10, new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), 168 },
+ { 11, 6, 11, new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 12, 8, 12, new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 13, 9, 13, new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified), 60 },
+ { 14, 10, 14, new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 15, 11, 15, new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), 120 },
+ { 16, 12, 1, new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 17, 13, 2, new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 18, 14, 3, new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), 36 },
+ { 19, 15, 4, new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified), 84 }
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_cars_model_generation_id",
+ table: "cars",
+ column: "model_generation_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_model_generations_model_id",
+ table: "model_generations",
+ column: "model_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_rentals_car_id",
+ table: "rentals",
+ column: "car_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_rentals_client_id",
+ table: "rentals",
+ column: "client_id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "rentals");
+
+ migrationBuilder.DropTable(
+ name: "cars");
+
+ migrationBuilder.DropTable(
+ name: "clients");
+
+ migrationBuilder.DropTable(
+ name: "model_generations");
+
+ migrationBuilder.DropTable(
+ name: "car_models");
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 000000000..47bd37913
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,886 @@
+//
+using System;
+using CarRental.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CarRental.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Car", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("color");
+
+ b.Property("LicensePlate")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("license_plate");
+
+ b.Property("ModelGenerationId")
+ .HasColumnType("int")
+ .HasColumnName("model_generation_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModelGenerationId");
+
+ b.ToTable("cars");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Color = "Black",
+ LicensePlate = "A001AA163",
+ ModelGenerationId = 1
+ },
+ new
+ {
+ Id = 2,
+ Color = "Red",
+ LicensePlate = "B777BC163",
+ ModelGenerationId = 2
+ },
+ new
+ {
+ Id = 3,
+ Color = "White",
+ LicensePlate = "C123ET163",
+ ModelGenerationId = 3
+ },
+ new
+ {
+ Id = 4,
+ Color = "Green",
+ LicensePlate = "E555KH163",
+ ModelGenerationId = 4
+ },
+ new
+ {
+ Id = 5,
+ Color = "Silver",
+ LicensePlate = "K234MR163",
+ ModelGenerationId = 5
+ },
+ new
+ {
+ Id = 6,
+ Color = "Gray",
+ LicensePlate = "M888OA163",
+ ModelGenerationId = 6
+ },
+ new
+ {
+ Id = 7,
+ Color = "Blue",
+ LicensePlate = "N456RS163",
+ ModelGenerationId = 7
+ },
+ new
+ {
+ Id = 8,
+ Color = "Brown",
+ LicensePlate = "O789TU163",
+ ModelGenerationId = 8
+ },
+ new
+ {
+ Id = 9,
+ Color = "White",
+ LicensePlate = "P321XO163",
+ ModelGenerationId = 9
+ },
+ new
+ {
+ Id = 10,
+ Color = "Black",
+ LicensePlate = "S654AM163",
+ ModelGenerationId = 10
+ },
+ new
+ {
+ Id = 11,
+ Color = "Orange",
+ LicensePlate = "T987RE163",
+ ModelGenerationId = 11
+ },
+ new
+ {
+ Id = 12,
+ Color = "White",
+ LicensePlate = "U246KN163",
+ ModelGenerationId = 12
+ },
+ new
+ {
+ Id = 13,
+ Color = "Black",
+ LicensePlate = "H135VT163",
+ ModelGenerationId = 13
+ },
+ new
+ {
+ Id = 14,
+ Color = "Gray",
+ LicensePlate = "SH579SA163",
+ ModelGenerationId = 14
+ },
+ new
+ {
+ Id = 15,
+ Color = "Blue",
+ LicensePlate = "SCH864RO163",
+ ModelGenerationId = 15
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.CarModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BodyType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("body_type");
+
+ b.Property("Class")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("class");
+
+ b.Property("DriveType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("drive_type");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)")
+ .HasColumnName("name");
+
+ b.Property("SeatsCount")
+ .HasColumnType("int")
+ .HasColumnName("seats_count");
+
+ b.HasKey("Id");
+
+ b.ToTable("car_models");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BodyType = "Sedan",
+ Class = "Premium",
+ DriveType = "RWD",
+ Name = "BMW 3 Series",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 2,
+ BodyType = "Coupe",
+ Class = "Sports",
+ DriveType = "RWD",
+ Name = "Ford Mustang",
+ SeatsCount = 4
+ },
+ new
+ {
+ Id = 3,
+ BodyType = "Sedan",
+ Class = "Compact",
+ DriveType = "FWD",
+ Name = "Honda Civic",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 4,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "Jeep Wrangler",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 5,
+ BodyType = "Coupe",
+ Class = "Luxury",
+ DriveType = "RWD",
+ Name = "Porsche 911",
+ SeatsCount = 4
+ },
+ new
+ {
+ Id = 6,
+ BodyType = "SUV",
+ Class = "Full-size",
+ DriveType = "AWD",
+ Name = "Chevrolet Tahoe",
+ SeatsCount = 8
+ },
+ new
+ {
+ Id = 7,
+ BodyType = "Sedan",
+ Class = "Economy",
+ DriveType = "FWD",
+ Name = "Lada Vesta",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 8,
+ BodyType = "SUV",
+ Class = "Mid-size",
+ DriveType = "AWD",
+ Name = "Subaru Outback",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 9,
+ BodyType = "Van",
+ Class = "Commercial",
+ DriveType = "RWD",
+ Name = "GAZ Gazelle Next",
+ SeatsCount = 3
+ },
+ new
+ {
+ Id = 10,
+ BodyType = "Hatchback",
+ Class = "Hybrid",
+ DriveType = "FWD",
+ Name = "Toyota Prius",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 11,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "UAZ Patriot",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 12,
+ BodyType = "SUV",
+ Class = "Premium",
+ DriveType = "AWD",
+ Name = "Lexus RX",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 13,
+ BodyType = "SUV",
+ Class = "Luxury",
+ DriveType = "AWD",
+ Name = "Range Rover Sport",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 14,
+ BodyType = "Sedan",
+ Class = "Premium",
+ DriveType = "AWD",
+ Name = "Audi A4",
+ SeatsCount = 5
+ },
+ new
+ {
+ Id = 15,
+ BodyType = "SUV",
+ Class = "Off-road",
+ DriveType = "4WD",
+ Name = "Lada Niva Travel",
+ SeatsCount = 5
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Client", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("date")
+ .HasColumnName("birth_date");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("full_name");
+
+ b.Property("LicenseNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("license_number");
+
+ b.HasKey("Id");
+
+ b.ToTable("clients");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BirthDate = new DateOnly(1988, 3, 15),
+ FullName = "Alexander Smirnov",
+ LicenseNumber = "2023-001"
+ },
+ new
+ {
+ Id = 2,
+ BirthDate = new DateOnly(1992, 7, 22),
+ FullName = "Marina Kovalenko",
+ LicenseNumber = "2022-045"
+ },
+ new
+ {
+ Id = 3,
+ BirthDate = new DateOnly(1995, 11, 10),
+ FullName = "Denis Popov",
+ LicenseNumber = "2024-012"
+ },
+ new
+ {
+ Id = 4,
+ BirthDate = new DateOnly(1985, 5, 3),
+ FullName = "Elena Vasnetsova",
+ LicenseNumber = "2021-078"
+ },
+ new
+ {
+ Id = 5,
+ BirthDate = new DateOnly(1990, 9, 30),
+ FullName = "Igor Kozlovsky",
+ LicenseNumber = "2023-056"
+ },
+ new
+ {
+ Id = 6,
+ BirthDate = new DateOnly(1993, 2, 14),
+ FullName = "Anna Orlova",
+ LicenseNumber = "2022-123"
+ },
+ new
+ {
+ Id = 7,
+ BirthDate = new DateOnly(1987, 8, 18),
+ FullName = "Artem Belov",
+ LicenseNumber = "2024-034"
+ },
+ new
+ {
+ Id = 8,
+ BirthDate = new DateOnly(1994, 12, 25),
+ FullName = "Sofia Grigorieva",
+ LicenseNumber = "2021-099"
+ },
+ new
+ {
+ Id = 9,
+ BirthDate = new DateOnly(1991, 6, 7),
+ FullName = "Pavel Melnikov",
+ LicenseNumber = "2023-087"
+ },
+ new
+ {
+ Id = 10,
+ BirthDate = new DateOnly(1989, 4, 12),
+ FullName = "Olga Zakharova",
+ LicenseNumber = "2022-067"
+ },
+ new
+ {
+ Id = 11,
+ BirthDate = new DateOnly(1996, 10, 28),
+ FullName = "Mikhail Tikhonov",
+ LicenseNumber = "2024-005"
+ },
+ new
+ {
+ Id = 12,
+ BirthDate = new DateOnly(1986, 1, 19),
+ FullName = "Ksenia Fedorova",
+ LicenseNumber = "2021-112"
+ },
+ new
+ {
+ Id = 13,
+ BirthDate = new DateOnly(1997, 7, 3),
+ FullName = "Roman Sokolov",
+ LicenseNumber = "2023-092"
+ },
+ new
+ {
+ Id = 14,
+ BirthDate = new DateOnly(1984, 3, 22),
+ FullName = "Tatiana Krylova",
+ LicenseNumber = "2022-031"
+ },
+ new
+ {
+ Id = 15,
+ BirthDate = new DateOnly(1998, 11, 15),
+ FullName = "Andrey Davydov",
+ LicenseNumber = "2024-021"
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("EngineVolume")
+ .HasColumnType("float")
+ .HasColumnName("engine_volume");
+
+ b.Property("ModelId")
+ .HasColumnType("int")
+ .HasColumnName("model_id");
+
+ b.Property("RentalPricePerHour")
+ .HasColumnType("decimal(18,2)")
+ .HasColumnName("rental_price_per_hour");
+
+ b.Property("Transmission")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("transmission");
+
+ b.Property("Year")
+ .HasColumnType("int")
+ .HasColumnName("year");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModelId");
+
+ b.ToTable("model_generations");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ EngineVolume = 2.0,
+ ModelId = 1,
+ RentalPricePerHour = 2200m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 2,
+ EngineVolume = 5.0,
+ ModelId = 2,
+ RentalPricePerHour = 5000m,
+ Transmission = "AT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 3,
+ EngineVolume = 1.5,
+ ModelId = 3,
+ RentalPricePerHour = 1200m,
+ Transmission = "CVT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 4,
+ EngineVolume = 3.6000000000000001,
+ ModelId = 4,
+ RentalPricePerHour = 2800m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 5,
+ EngineVolume = 3.0,
+ ModelId = 5,
+ RentalPricePerHour = 8000m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 6,
+ EngineVolume = 5.2999999999999998,
+ ModelId = 6,
+ RentalPricePerHour = 3500m,
+ Transmission = "AT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 7,
+ EngineVolume = 1.6000000000000001,
+ ModelId = 7,
+ RentalPricePerHour = 700m,
+ Transmission = "MT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 8,
+ EngineVolume = 2.5,
+ ModelId = 8,
+ RentalPricePerHour = 1800m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 9,
+ EngineVolume = 2.7000000000000002,
+ ModelId = 9,
+ RentalPricePerHour = 1500m,
+ Transmission = "MT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 10,
+ EngineVolume = 1.8,
+ ModelId = 10,
+ RentalPricePerHour = 1600m,
+ Transmission = "CVT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 11,
+ EngineVolume = 2.7000000000000002,
+ ModelId = 11,
+ RentalPricePerHour = 1400m,
+ Transmission = "MT",
+ Year = 2022
+ },
+ new
+ {
+ Id = 12,
+ EngineVolume = 3.5,
+ ModelId = 12,
+ RentalPricePerHour = 3200m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 13,
+ EngineVolume = 3.0,
+ ModelId = 13,
+ RentalPricePerHour = 6000m,
+ Transmission = "AT",
+ Year = 2023
+ },
+ new
+ {
+ Id = 14,
+ EngineVolume = 2.0,
+ ModelId = 14,
+ RentalPricePerHour = 2800m,
+ Transmission = "AT",
+ Year = 2024
+ },
+ new
+ {
+ Id = 15,
+ EngineVolume = 1.7,
+ ModelId = 15,
+ RentalPricePerHour = 900m,
+ Transmission = "MT",
+ Year = 2023
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Rental", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CarId")
+ .HasColumnType("int")
+ .HasColumnName("car_id");
+
+ b.Property("ClientId")
+ .HasColumnType("int")
+ .HasColumnName("client_id");
+
+ b.Property("RentalDate")
+ .HasColumnType("datetime2")
+ .HasColumnName("rental_date");
+
+ b.Property("RentalHours")
+ .HasColumnType("int")
+ .HasColumnName("rental_hours");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CarId");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("rentals");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ CarId = 7,
+ ClientId = 1,
+ RentalDate = new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 2,
+ CarId = 7,
+ ClientId = 3,
+ RentalDate = new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 3,
+ CarId = 7,
+ ClientId = 5,
+ RentalDate = new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 24
+ },
+ new
+ {
+ Id = 4,
+ CarId = 1,
+ ClientId = 2,
+ RentalDate = new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 5,
+ CarId = 1,
+ ClientId = 4,
+ RentalDate = new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 120
+ },
+ new
+ {
+ Id = 6,
+ CarId = 2,
+ ClientId = 6,
+ RentalDate = new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 7,
+ CarId = 2,
+ ClientId = 8,
+ RentalDate = new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 8,
+ CarId = 3,
+ ClientId = 7,
+ RentalDate = new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 36
+ },
+ new
+ {
+ Id = 9,
+ CarId = 4,
+ ClientId = 9,
+ RentalDate = new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 10,
+ CarId = 5,
+ ClientId = 10,
+ RentalDate = new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 168
+ },
+ new
+ {
+ Id = 11,
+ CarId = 6,
+ ClientId = 11,
+ RentalDate = new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 12,
+ CarId = 8,
+ ClientId = 12,
+ RentalDate = new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 13,
+ CarId = 9,
+ ClientId = 13,
+ RentalDate = new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 60
+ },
+ new
+ {
+ Id = 14,
+ CarId = 10,
+ ClientId = 14,
+ RentalDate = new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 96
+ },
+ new
+ {
+ Id = 15,
+ CarId = 11,
+ ClientId = 15,
+ RentalDate = new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 120
+ },
+ new
+ {
+ Id = 16,
+ CarId = 12,
+ ClientId = 1,
+ RentalDate = new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 48
+ },
+ new
+ {
+ Id = 17,
+ CarId = 13,
+ ClientId = 2,
+ RentalDate = new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 72
+ },
+ new
+ {
+ Id = 18,
+ CarId = 14,
+ ClientId = 3,
+ RentalDate = new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 36
+ },
+ new
+ {
+ Id = 19,
+ CarId = 15,
+ ClientId = 4,
+ RentalDate = new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified),
+ RentalHours = 84
+ });
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Car", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.ModelGeneration", "ModelGeneration")
+ .WithMany()
+ .HasForeignKey("ModelGenerationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ModelGeneration");
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.CarModel", "Model")
+ .WithMany()
+ .HasForeignKey("ModelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Model");
+ });
+
+ modelBuilder.Entity("CarRental.Domain.Entities.Rental", b =>
+ {
+ b.HasOne("CarRental.Domain.Entities.Car", "Car")
+ .WithMany()
+ .HasForeignKey("CarId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("CarRental.Domain.Entities.Client", "Client")
+ .WithMany()
+ .HasForeignKey("ClientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Car");
+
+ b.Navigation("Client");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs
new file mode 100644
index 000000000..d19c310c7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs
@@ -0,0 +1,62 @@
+using CarRental.Domain.Data;
+using CarRental.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Infrastructure.Persistence;
+
+public class AppDbContext(DbContextOptions options, CarRentalFixture fixture) : DbContext(options)
+{
+ public DbSet Cars { get; set; }
+ public DbSet Clients { get; set; }
+ public DbSet CarModels { get; set; }
+ public DbSet ModelGenerations { get; set; }
+ public DbSet Rentals { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(c => c.Id);
+ entity.HasOne(c => c.ModelGeneration)
+ .WithMany()
+ .HasForeignKey(c => c.ModelGenerationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(mg => mg.Id);
+ entity.HasOne(mg => mg.Model)
+ .WithMany()
+ .HasForeignKey(mg => mg.ModelId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(r => r.Id);
+ entity.HasOne(r => r.Car)
+ .WithMany()
+ .HasForeignKey(r => r.CarId)
+ .OnDelete(DeleteBehavior.Cascade);
+ entity.HasOne(r => r.Client)
+ .WithMany()
+ .HasForeignKey(r => r.ClientId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity()
+ .HasKey(c => c.Id);
+
+ modelBuilder.Entity()
+ .HasKey(cm => cm.Id);
+
+ modelBuilder.Entity().HasData(fixture.Cars);
+ modelBuilder.Entity().HasData(fixture.CarModels);
+ modelBuilder.Entity().HasData(fixture.Clients);
+ modelBuilder.Entity().HasData(fixture.ModelGenerations);
+ modelBuilder.Entity().HasData(fixture.Rentals);
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs
new file mode 100644
index 000000000..02620b4e0
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs
@@ -0,0 +1,62 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Infrastructure.Repositories;
+
+public class DbRepository(AppDbContext context) : IRepository where T : class
+{
+ protected readonly DbSet _set = context.Set();
+
+ public async Task