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..2686cf64b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/CarRental.API.csproj
@@ -0,0 +1,25 @@
+
+
+ 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..03e53cbba
--- /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..7f99fbd4a
--- /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..f02632935
--- /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..6f20b751c
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Program.cs
@@ -0,0 +1,71 @@
+using CarRental.Application.Contracts;
+using CarRental.Domain.Data;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using CarRental.Infrastructure.Persistence;
+using CarRental.Infrastructure.Repositories;
+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>();
+
+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..3b3d5007d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+ 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..f0a289646
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Program.cs
@@ -0,0 +1,11 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+
+var sqlServer = builder.AddSqlServer("carrental-sql-server")
+ .AddDatabase("CarRentalDb");
+
+var api = builder.AddProject("carrental-api")
+ .WithReference(sqlServer, "DefaultConnection")
+ .WaitFor(sqlServer);
+
+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..a37731897
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "CarRental.AppHost": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "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..c093f65d5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": ""
+ }
+}
\ 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..4b9cc15f8
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj
@@ -0,0 +1,14 @@
+
+
+
+ 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.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 GetByIdAsync(int id) => await _set.FindAsync(id);
+
+ public async Task> GetAllAsync() => await _set.ToListAsync();
+
+ public async Task AddAsync(T entity)
+ {
+ await _set.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task UpdateAsync(T entity)
+ {
+ _set.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await _set.FindAsync(id);
+ if (entity == null) return false;
+ _set.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ public async Task GetByIdAsync(int id, Func, IQueryable>? include = null)
+ {
+ var query = _set.AsQueryable();
+ if (include != null)
+ query = include(query);
+
+ return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id);
+ }
+
+ public async Task> GetAllAsync(Func, IQueryable>? include = null)
+ {
+ var query = _set.AsQueryable();
+ if (include != null)
+ query = include(query);
+ return await query.ToListAsync();
+ }
+
+ public IQueryable GetQueryable(Func, IQueryable>? include = null)
+ {
+ var query = _set.AsQueryable();
+ if (include != null)
+ query = include(query);
+ return query;
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj
new file mode 100644
index 000000000..2afa7684e
--- /dev/null
+++ b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj
@@ -0,0 +1,18 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs
new file mode 100644
index 000000000..87618230d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs
@@ -0,0 +1,88 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+public static class Extensions
+{
+ public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.ConfigureOpenTelemetry();
+ builder.AddDefaultHealthChecks();
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ http.AddStandardResilienceHandler();
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddSource(builder.Environment.ApplicationName)
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ return builder;
+ }
+
+ public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Services.AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ if (app.Environment.IsDevelopment())
+ {
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj
new file mode 100644
index 000000000..7361aa8b2
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs
new file mode 100644
index 000000000..7788d9bd7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs
@@ -0,0 +1,190 @@
+using CarRental.Domain.Data;
+
+namespace CarRental.Tests;
+
+
+///
+/// Юнит-тесты для пункта проката автомобилей
+///
+public class CarRentalTests(CarRentalFixture fixture) : IClassFixture
+{
+ ///
+ /// ТЕСТ 1: Вывести информацию обо всех клиентах,
+ /// которые брали в аренду автомобили указанной модели, упорядочить по ФИО.
+ ///
+ [Fact]
+ public void GetClientsByModelSortedByName()
+ {
+ const string targetModel = "Lada Vesta";
+ const int expectedCount = 3;
+ const string expectedFirstName = "Alexander Smirnov";
+ const string expectedSecondName = "Denis Popov";
+ const string expectedThirdName = "Igor Kozlovsky";
+
+ var modelId = fixture.CarModels.FirstOrDefault(m => m.Name == targetModel)?.Id;
+
+ var generationIds = fixture.ModelGenerations
+ .Where(mg => mg.ModelId == modelId)
+ .Select(mg => mg.Id)
+ .ToList();
+
+ var carIds = fixture.Cars
+ .Where(c => generationIds.Contains(c.ModelGenerationId))
+ .Select(c => c.Id)
+ .ToList();
+
+ var clientIds = fixture.Rentals
+ .Where(r => carIds.Contains(r.CarId))
+ .Select(r => r.ClientId)
+ .Distinct()
+ .ToList();
+
+ var clients = fixture.Clients
+ .Where(c => clientIds.Contains(c.Id))
+ .OrderBy(c => c.FullName)
+ .ToList();
+
+ Assert.Equal(expectedCount, clients.Count);
+ Assert.Equal(expectedFirstName, clients[0].FullName);
+ Assert.Equal(expectedSecondName, clients[1].FullName);
+ Assert.Equal(expectedThirdName, clients[2].FullName);
+ }
+
+ ///
+ /// ТЕСТ 2: Вывести информацию об автомобилях, находящихся в аренде.
+ ///
+ [Fact]
+ public void GetCurrentlyRentedCars()
+ {
+ var testDate = new DateTime(2024, 3, 5, 12, 0, 0);
+ var expectedPlate = "K234MR163";
+
+ var rentedCarIds = fixture.Rentals
+ .Where(r => r.RentalDate.AddHours(r.RentalHours) > testDate)
+ .Select(r => r.CarId)
+ .Distinct()
+ .ToList();
+
+ var rentedCars = fixture.Cars
+ .Where(c => rentedCarIds.Contains(c.Id))
+ .ToList();
+
+ Assert.Contains(rentedCars, c => c.LicensePlate == expectedPlate);
+ }
+
+ ///
+ /// ТЕСТ 3: Вывести топ 5 наиболее часто арендуемых автомобилей.
+ ///
+ [Fact]
+ public void GetTop5MostRentedCars()
+ {
+ const int expectedCount = 5;
+ const string expectedTopCarPlate = "N456RS163";
+ const int expectedTopCarRentalCount = 3;
+
+ var topCarStats = fixture.Rentals
+ .GroupBy(r => r.CarId)
+ .Select(g => new { CarId = g.Key, RentalCount = g.Count() })
+ .OrderByDescending(x => x.RentalCount)
+ .Take(5)
+ .ToList();
+
+ var topCarIds = topCarStats.Select(x => x.CarId).ToList();
+ var carsDict = fixture.Cars
+ .Where(c => topCarIds.Contains(c.Id))
+ .ToDictionary(c => c.Id);
+
+ var topCars = topCarStats
+ .Select(x => new
+ {
+ Car = carsDict.GetValueOrDefault(x.CarId),
+ x.RentalCount
+ })
+ .Where(x => x.Car != null)
+ .ToList();
+
+ Assert.Equal(expectedCount, topCars.Count);
+ Assert.Equal(expectedTopCarPlate, topCars[0].Car?.LicensePlate);
+ Assert.Equal(expectedTopCarRentalCount, topCars[0].RentalCount);
+ }
+
+ ///
+ /// ТЕСТ 4: Для каждого автомобиля вывести число аренд.
+ ///
+ [Fact]
+ public void GetRentalCountPerCar()
+ {
+ const int expectedTotalCars = 15;
+ const int expectedLadaVestaRentalCount = 3;
+ const int expectedBmwRentalCount = 2;
+ const int ladaVestaCarId = 7;
+ const int bmwCarId = 1;
+
+ var rentalCounts = fixture.Rentals
+ .GroupBy(r => r.CarId)
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ var carsWithRentalCount = fixture.Cars
+ .Select(car => new
+ {
+ Car = car,
+ RentalCount = rentalCounts.GetValueOrDefault(car.Id, 0)
+ })
+ .ToList();
+
+ Assert.Equal(expectedTotalCars, carsWithRentalCount.Count);
+
+ var ladaVesta = carsWithRentalCount.First(c => c.Car.Id == ladaVestaCarId);
+ var bmw = carsWithRentalCount.First(c => c.Car.Id == bmwCarId);
+
+ Assert.Equal(expectedLadaVestaRentalCount, ladaVesta.RentalCount);
+ Assert.Equal(expectedBmwRentalCount, bmw.RentalCount);
+ Assert.True(carsWithRentalCount.All(x => x.RentalCount >= 0));
+ }
+
+ ///
+ /// ТЕСТ 5: Вывести топ 5 клиентов по сумме аренды.
+ ///
+ [Fact]
+ public void GetTop5ClientsByRentalAmount()
+ {
+ const int expectedCount = 5;
+ const string expectedTopClientName = "Olga Zakharova";
+
+ var carPrices = fixture.Cars
+ .Join(fixture.ModelGenerations,
+ c => c.ModelGenerationId,
+ g => g.Id,
+ (c, g) => new { CarId = c.Id, Price = g.RentalPricePerHour })
+ .ToDictionary(x => x.CarId, x => x.Price);
+
+ var topClientStats = fixture.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 clientsDict = fixture.Clients
+ .Where(c => topClientIds.Contains(c.Id))
+ .ToDictionary(c => c.Id);
+
+ var topClients = topClientStats
+ .Select(x => new
+ {
+ Client = clientsDict.GetValueOrDefault(x.ClientId),
+ x.TotalAmount
+ })
+ .Where(x => x.Client != null)
+ .ToList();
+
+ Assert.Equal(expectedCount, topClients.Count);
+ Assert.Equal(expectedTopClientName, topClients[0].Client?.FullName);
+ }
+}
+
diff --git a/CarRental/CarRental/CarRental.slnx b/CarRental/CarRental/CarRental.slnx
new file mode 100644
index 000000000..933fe8c5a
--- /dev/null
+++ b/CarRental/CarRental/CarRental.slnx
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 76afcbfdd..5cce7c175 100644
--- a/README.md
+++ b/README.md
@@ -1,137 +1,37 @@
-# Разработка корпоративных приложений
-[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing)
+# Лабораторная работа 2 и 3: Сервер - Реализация серверного приложения с использованием REST API,
+# ORM - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации
-## Задание
-### Цель
-Реализация проекта сервисно-ориентированного приложения.
-### Задачи
-* Реализация объектно-ориентированной модели данных,
-* Изучение реализации серверных приложений на базе WebAPI/OpenAPI,
-* Изучение работы с брокерами сообщений,
-* Изучение паттернов проектирования,
-* Изучение работы со средствами оркестрации на примере .NET Aspire,
-* Повторение основ работы с системами контроля версий,
-* Unit-тестирование.
+## Вариант 80
-### Лабораторные работы
-
-1. «Классы» - Реализация объектной модели данных и unit-тестов
-
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты.
+* **Platform**: .NET 8 (C# 12)
+* **Database**: SqlServer
+* **ORM**: Entity Framework Core 8
+* **Messaging**: Apache Kafka
+* **Mapping**: AutoMapper
+* **Testing**: xUnit, Bogus (Fake Data)
-Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций.
-Необходимо включить **как минимум 10** экземпляров каждого класса в датасид.
-
-
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API
-
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно:
-- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями
-- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания)
-Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций.
-
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации
-
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных.
-Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения.
-
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных.
-
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером
-
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд.
-Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами.
-Отправка контрактов при помощи gRPC должна выполняться в потоковом виде.
-При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру.
+## Описание предметной области
-Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений.
-
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором
-
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка".
-
-
+Реализована объектная модель для пункта проката автомобилей со следующими сущностями:
-## Задание. Общая часть
-**Обязательно**:
-* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview).
-* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet).
-* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs).
-* Использование хранения данных в базе данных согласно варианту задания.
-* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview)
-* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания.
-* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
-* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация.
+- **`CarModel`** – модель автомобиля (справочник): тип привода, класс, тип кузова, количество мест.
+- **`ModelGeneration`** – поколение модели: год выпуска, объём двигателя, коробка передач, стоимость часа аренды.
+- **`Car`** – физический экземпляр автомобиля: госномер, цвет, поколение модели.
+- **`Client`** – клиент: номер водительского удостоверения, ФИО, дата рождения.
+- **`Rental`** – договор аренды: клиент, автомобиль, время выдачи, длительность в часах.
-**Факультативно**:
-* Реализация авторизации/аутентификации.
-* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал.
-* Реализация интеграционных тестов при помощи .NET Aspire.
-* Реализация клиента на Blazor WASM.
+## Реализованные тесты (xUnit)
-Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью.
-Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю.
+1. **`GetClientsByModelSortedByName`** – клиенты, бравшие в аренду указанную модель, отсортированные по ФИО.
+2. **`GetCurrentlyRentedCars`** – автомобили, которые сейчас находятся в аренде (нет даты возврата).
+3. **`GetTop5MostRentedCars`**` – топ‑5 автомобилей по количеству аренд.
+4. **`GetRentalCountPerCar`** – количество аренд для каждого автомобиля.
+5. **`GetTop5ClientsByRentalAmount`** – топ‑5 клиентов по сумме стоимости аренды.
-По итогу работы в семестре должна получиться следующая информационная система:
-
-C4 диаграмма
+## Результат 2 и 3 лабораторной работы
-
-
-
-
-## Варианты заданий
-Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи.
-
-[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing)
-[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing)
-[Вопросы к экзамену](https://docs.google.com/document/d/1bjfvtzjyMljJbcu8YCvC8DzDegDUAmDeNtBz9M6FQes/edit?usp=sharing)
-
-## Схема сдачи
-
-На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests).
-
-Общая схема:
-1. Сделать форк данного репозитория
-2. Выполнить задание
-3. Сделать PR в данный репозиторий
-4. Исправить замечания после code review
-5. Получить approve
-6. Прийти на занятие и защитить работу
-
-## Критерии оценивания
-
-Конкурентный принцип.
-Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки:
-1. Скорость разработки
-2. Качество разработки
-3. Полнота выполнения задания
-
-Быстрее делаете PR - у вас преимущество.
-Быстрее получаете Approve - у вас преимущество.
-Выполните нечто немного выходящее за рамки проекта - у вас преимущество.
-
-### Шкала оценивания
-
-- **3 балла** за качество кода, из них:
- - 2 балла - базовая оценка
- - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов:
- - Реализация факультативного функционала
- - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл
-- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл
-
-У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы.
-
-## Вопросы и обратная связь по курсу
-
-Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new).
-Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas).
+- Реализовано серверное приложение, которое осуществляет базовые CRUD-операции с реализованными в первой лабораторной сущностями и предоставляет результаты аналитических запросов
+- Созданы миграции для создания таблиц в бд и их первоначального заполнения.
+- Также был создан оркестратор Aspire на запуск сервера и базы данных.