diff --git a/CarRental/CarRental/.github/workflows/dotnet-tests.yml b/CarRental/CarRental/.github/workflows/dotnet-tests.yml
new file mode 100644
index 000000000..52a139620
--- /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.sln
+
+ - name: Build
+ run: dotnet build ./CarRental/CarRental.sln --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..cf7a6f20b
--- /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
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
new file mode 100644
index 000000000..db6d343ab
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
@@ -0,0 +1,164 @@
+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/[controller]")]
+public class AnalyticsController(
+ IRepository rentalRepo,
+ IRepository carRepo,
+ IRepository clientRepo,
+ IRepository generationRepo,
+ IMapper mapper) : ControllerBase
+{
+ ///
+ /// Клиенты, арендовавшие ТС указанной модели, отсортированные по ФИО
+ ///
+ [HttpGet("clients-by-model")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetClientsByModel([FromQuery] string modelName)
+ {
+ var query = rentalRepo.GetQueryable(q => q
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+
+ var clients = await query
+ .Where(r => r.Car!.ModelGeneration!.Model!.Name == modelName)
+ .Select(r => r.Client)
+ .Distinct()
+ .OrderBy(c => c!.FullName)
+ .ToListAsync();
+
+ return Ok(clients.Select(mapper.Map));
+ }
+
+ ///
+ /// Автомобили, находящиеся в аренде на указанный момент
+ ///
+ [HttpGet("currently-rented")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetCurrentlyRented([FromQuery] DateTime currentDate)
+ {
+ var activeCarIds = await rentalRepo.GetQueryable()
+ .Where(r => r.RentalDate.AddHours(r.RentalHours) > currentDate)
+ .Select(r => r.CarId)
+ .Distinct()
+ .ToListAsync();
+
+ var cars = await carRepo.GetQueryable()
+ .Where(c => activeCarIds.Contains(c.Id))
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .ToListAsync();
+
+ return Ok(cars.Select(mapper.Map));
+ }
+
+ ///
+ /// Топ-5 наиболее часто арендуемых автомобилей
+ ///
+ [HttpGet("top-rented-cars")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetTopRentedCars()
+ {
+ var stats = await rentalRepo.GetQueryable()
+ .GroupBy(r => r.CarId)
+ .Select(g => new { CarId = g.Key, Count = g.Count() })
+ .OrderByDescending(x => x.Count)
+ .Take(5)
+ .ToListAsync();
+
+ var ids = stats.Select(s => s.CarId).ToList();
+ var cars = await carRepo.GetQueryable()
+ .Where(c => ids.Contains(c.Id))
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .ToListAsync();
+
+ var dict = cars.ToDictionary(c => c.Id);
+ var result = stats
+ .Where(s => dict.ContainsKey(s.CarId))
+ .Select(s => new CarRentalCountDto(mapper.Map(dict[s.CarId]), s.Count));
+
+ return Ok(result);
+ }
+
+ ///
+ /// Число аренд для каждого автомобиля в парке
+ ///
+ [HttpGet("rentals-per-car")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetRentalsPerCar()
+ {
+ var counts = await rentalRepo.GetQueryable()
+ .GroupBy(r => r.CarId)
+ .Select(g => new { CarId = g.Key, Count = g.Count() })
+ .ToDictionaryAsync(x => x.CarId, x => x.Count);
+
+ var cars = await carRepo.GetQueryable()
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .ToListAsync();
+
+ var result = cars
+ .Select(c => new CarRentalCountDto(
+ mapper.Map(c),
+ counts.GetValueOrDefault(c.Id, 0)))
+ .OrderByDescending(x => x.RentalCount);
+
+ return Ok(result);
+ }
+
+ ///
+ /// Топ-5 клиентов по суммарной стоимости аренды
+ ///
+ [HttpGet("top-clients-by-amount")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetTopClientsByAmount()
+ {
+ var rentals = await rentalRepo.GetQueryable()
+ .Select(r => new { r.ClientId, r.CarId, r.RentalHours })
+ .ToListAsync();
+
+ var carPrices = await carRepo.GetQueryable()
+ .Join(generationRepo.GetQueryable(),
+ c => c.ModelGenerationId,
+ g => g.Id,
+ (c, g) => new { CarId = c.Id, g.RentalPricePerHour })
+ .ToDictionaryAsync(x => x.CarId, x => x.RentalPricePerHour);
+
+ var topStats = 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 topIds = topStats.Select(s => s.ClientId).ToList();
+ var clients = await clientRepo.GetQueryable()
+ .Where(c => topIds.Contains(c.Id))
+ .ToDictionaryAsync(c => c.Id);
+
+ var result = topStats
+ .Where(s => clients.ContainsKey(s.ClientId))
+ .Select(s => new ClientRentalAmountDto(
+ mapper.Map(clients[s.ClientId]),
+ s.TotalAmount));
+
+ return Ok(result);
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs
new file mode 100644
index 000000000..968d6d19e
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs
@@ -0,0 +1,70 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.API.Controllers;
+
+///
+/// CRUD-операции над справочником моделей автомобилей
+///
+[ApiController]
+[Route("api/car-models")]
+public class CarModelsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ /// Получить список всех моделей
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var items = await repo.GetAllAsync();
+ return Ok(items.Select(mapper.Map));
+ }
+
+ /// Получить модель по идентификатору
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var item = await repo.GetByIdAsync(id);
+ return item is null ? NotFound() : Ok(mapper.Map(item));
+ }
+
+ /// Создать новую модель
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] CarModelEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, mapper.Map(created));
+ }
+
+ /// Обновить модель
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] CarModelEditDto dto)
+ {
+ var existing = await repo.GetByIdAsync(id);
+ if (existing is null) return NotFound();
+
+ mapper.Map(dto, existing);
+ var updated = await repo.UpdateAsync(existing);
+ return Ok(mapper.Map(updated));
+ }
+
+ /// Удалить модель
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var deleted = await repo.DeleteAsync(id);
+ return deleted ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs
new file mode 100644
index 000000000..c2af8175a
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs
@@ -0,0 +1,75 @@
+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;
+
+///
+/// CRUD-операции над транспортными средствами
+///
+[ApiController]
+[Route("api/[controller]")]
+public class CarsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ /// Получить список всех ТС с деталями поколения и модели
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var items = await repo.GetAllAsync(q => q
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ return Ok(items.Select(mapper.Map));
+ }
+
+ /// Получить ТС по идентификатору
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var item = await repo.GetByIdAsync(id, q => q
+ .Include(c => c.ModelGeneration)
+ .ThenInclude(mg => mg!.Model));
+ return item is null ? NotFound() : Ok(mapper.Map(item));
+ }
+
+ /// Добавить новое ТС
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] CarEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, mapper.Map(created));
+ }
+
+ /// Обновить данные ТС
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] CarEditDto dto)
+ {
+ var existing = await repo.GetByIdAsync(id);
+ if (existing is null) return NotFound();
+
+ mapper.Map(dto, existing);
+ var updated = await repo.UpdateAsync(existing);
+ return Ok(mapper.Map(updated));
+ }
+
+ /// Удалить ТС
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var deleted = await repo.DeleteAsync(id);
+ return deleted ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs
new file mode 100644
index 000000000..7865cb292
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs
@@ -0,0 +1,70 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CarRental.API.Controllers;
+
+///
+/// CRUD-операции над клиентами
+///
+[ApiController]
+[Route("api/[controller]")]
+public class ClientsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ /// Получить список всех клиентов
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var items = await repo.GetAllAsync();
+ return Ok(items.Select(mapper.Map));
+ }
+
+ /// Получить клиента по идентификатору
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var item = await repo.GetByIdAsync(id);
+ return item is null ? NotFound() : Ok(mapper.Map(item));
+ }
+
+ /// Создать нового клиента
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] ClientEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, mapper.Map(created));
+ }
+
+ /// Обновить данные клиента
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] ClientEditDto dto)
+ {
+ var existing = await repo.GetByIdAsync(id);
+ if (existing is null) return NotFound();
+
+ mapper.Map(dto, existing);
+ var updated = await repo.UpdateAsync(existing);
+ return Ok(mapper.Map(updated));
+ }
+
+ /// Удалить клиента
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var deleted = await repo.DeleteAsync(id);
+ return deleted ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs
new file mode 100644
index 000000000..8702e2688
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs
@@ -0,0 +1,71 @@
+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;
+
+///
+/// CRUD-операции над справочником поколений моделей
+///
+[ApiController]
+[Route("api/model-generations")]
+public class ModelGenerationsController(
+ IRepository repo,
+ IMapper mapper) : ControllerBase
+{
+ /// Получить список всех поколений
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var items = await repo.GetAllAsync(q => q.Include(mg => mg.Model));
+ return Ok(items.Select(mapper.Map));
+ }
+
+ /// Получить поколение по идентификатору
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var item = await repo.GetByIdAsync(id, q => q.Include(mg => mg.Model));
+ return item is null ? NotFound() : Ok(mapper.Map(item));
+ }
+
+ /// Создать поколение
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task> Create([FromBody] ModelGenerationEditDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var created = await repo.AddAsync(entity);
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, mapper.Map(created));
+ }
+
+ /// Обновить поколение
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] ModelGenerationEditDto dto)
+ {
+ var existing = await repo.GetByIdAsync(id);
+ if (existing is null) return NotFound();
+
+ mapper.Map(dto, existing);
+ var updated = await repo.UpdateAsync(existing);
+ return Ok(mapper.Map(updated));
+ }
+
+ /// Удалить поколение
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var deleted = await repo.DeleteAsync(id);
+ return deleted ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs
new file mode 100644
index 000000000..56f6c86b4
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs
@@ -0,0 +1,90 @@
+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;
+
+///
+/// CRUD-операции над договорами аренды
+///
+[ApiController]
+[Route("api/[controller]")]
+public class RentalsController(
+ IRepository rentalRepo,
+ IRepository carRepo,
+ IRepository clientRepo,
+ IMapper mapper) : ControllerBase
+{
+ /// Получить список всех договоров аренды
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll()
+ {
+ var items = await rentalRepo.GetAllAsync(q => q
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ return Ok(items.Select(mapper.Map));
+ }
+
+ /// Получить договор аренды по идентификатору
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id)
+ {
+ var item = await rentalRepo.GetByIdAsync(id, q => q
+ .Include(r => r.Car)
+ .ThenInclude(c => c!.ModelGeneration)
+ .ThenInclude(mg => mg!.Model)
+ .Include(r => r.Client));
+ return item is null ? NotFound() : Ok(mapper.Map(item));
+ }
+
+ /// Создать договор аренды
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task> Create([FromBody] RentalEditDto dto)
+ {
+ var carExists = await carRepo.GetByIdAsync(dto.CarId);
+ if (carExists is null)
+ return BadRequest($"Автомобиль с Id={dto.CarId} не найден");
+
+ var clientExists = await clientRepo.GetByIdAsync(dto.ClientId);
+ if (clientExists is null)
+ return BadRequest($"Клиент с Id={dto.ClientId} не найден");
+
+ var entity = mapper.Map(dto);
+ var created = await rentalRepo.AddAsync(entity);
+ return CreatedAtAction(nameof(GetById), new { id = created.Id }, mapper.Map(created));
+ }
+
+ /// Обновить договор аренды
+ [HttpPut("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> Update(int id, [FromBody] RentalEditDto dto)
+ {
+ var existing = await rentalRepo.GetByIdAsync(id);
+ if (existing is null) return NotFound();
+
+ mapper.Map(dto, existing);
+ var updated = await rentalRepo.UpdateAsync(existing);
+ return Ok(mapper.Map(updated));
+ }
+
+ /// Удалить договор аренды
+ [HttpDelete("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Delete(int id)
+ {
+ var deleted = await rentalRepo.DeleteAsync(id);
+ return deleted ? NoContent() : NotFound();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.API/Program.cs b/CarRental/CarRental/CarRental.API/Program.cs
new file mode 100644
index 000000000..3d253b1c3
--- /dev/null
+++ b/CarRental/CarRental/CarRental.API/Program.cs
@@ -0,0 +1,61 @@
+using CarRental.Application.Contracts;
+using CarRental.Domain.Data;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using CarRental.Infrastructure.Messaging;
+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(opts =>
+ opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var basePath = AppContext.BaseDirectory;
+ foreach (var xmlFile in new[] { "CarRental.API.xml", "CarRental.Application.Contracts.xml", "CarRental.Domain.xml" })
+ {
+ var path = Path.Combine(basePath, xmlFile);
+ if (File.Exists(path))
+ c.IncludeXmlComments(path, includeControllerXmlComments: true);
+ }
+});
+
+builder.Services.AddAutoMapper(cfg => cfg.AddProfile());
+
+builder.Services.AddDbContext(opts =>
+ opts.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>();
+
+// Регистрация RabbitMQ-соединения и фонового потребителя
+builder.AddRabbitMQClient("carrental-rabbitmq");
+builder.Services.AddHostedService();
+
+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 v1"));
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapDefaultEndpoints();
+app.MapControllers();
+app.Run();
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..abdc12351
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ a7b3c9d1-2e4f-5a6b-8c0d-e1f234567890
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs
new file mode 100644
index 000000000..170fdd4b2
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Program.cs
@@ -0,0 +1,25 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var sqlServer = builder.AddSqlServer("carrental-sql")
+ .AddDatabase("CarRentalDb");
+
+// RabbitMQ с панелью управления (Management UI на порту 15672)
+var rabbitMq = builder.AddRabbitMQ("carrental-rabbitmq")
+ .WithManagementPlugin();
+
+var api = builder.AddProject("carrental-api")
+ .WithReference(sqlServer, "DefaultConnection")
+ .WithReference(rabbitMq)
+ .WithEnvironment("RabbitMQ__ExchangeName", "rental-exchange")
+ .WithEnvironment("RabbitMQ__QueueName", "rental-queue")
+ .WaitFor(sqlServer)
+ .WaitFor(rabbitMq);
+
+builder.AddProject("carrental-generator")
+ .WithReference(rabbitMq)
+ .WithReference(api)
+ .WithEnvironment("RabbitMQ__ExchangeName", "rental-exchange")
+ .WaitFor(rabbitMq)
+ .WaitFor(api);
+
+builder.Build().Run();
diff --git a/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..a4ef35c1d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "CarRental.AppHost": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15058",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19141",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20144",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..b09d7ac12
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "None"
+ }
+ },
+ "Aspire": {
+ "Dashboard": {
+ "Frontend": {
+ "BrowserAuthMode": "Unsecured"
+ },
+ "ResourceServiceClient": {
+ "AuthMode": "Unsecured"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.json b/CarRental/CarRental/CarRental.AppHost/appsettings.json
new file mode 100644
index 000000000..d87c105e5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": ""
+ },
+ "Kafka": {
+ "RentalTopicName": "rentals"
+ }
+}
\ No newline at end of file
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj
new file mode 100644
index 000000000..3f83a4b38
--- /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..49567a667
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs
@@ -0,0 +1,15 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO аналитики: автомобиль и число его аренд
+///
+/// Данные автомобиля
+/// Количество аренд
+public record CarRentalCountDto(CarGetDto Car, int RentalCount);
+
+///
+/// DTO аналитики: клиент и суммарная стоимость его аренд
+///
+/// Данные клиента
+/// Суммарная стоимость аренды в рублях
+public record ClientRentalAmountDto(ClientGetDto Client, decimal TotalAmount);
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..9dcf74569
--- /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
+);
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..16ac509d4
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для чтения данных автомобиля
+///
+/// Идентификатор
+/// Государственный регистрационный номер
+/// Цвет кузова
+/// Идентификатор поколения модели
+/// Данные поколения модели
+public record CarGetDto(
+ int Id,
+ string LicensePlate,
+ string Color,
+ int ModelGenerationId,
+ ModelGenerationGetDto? ModelGeneration
+);
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..41589077c
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления модели автомобиля
+///
+/// Название модели
+/// Тип привода
+/// Число мест
+/// Тип кузова
+/// Класс автомобиля
+public record CarModelEditDto(
+ string Name,
+ string DriveType,
+ int SeatsCount,
+ string BodyType,
+ string Class
+);
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..146e77f8b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs
@@ -0,0 +1,19 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для чтения данных модели автомобиля
+///
+/// Идентификатор
+/// Название модели
+/// Тип привода
+/// Число мест
+/// Тип кузова
+/// Класс автомобиля
+public record CarModelGetDto(
+ int Id,
+ string Name,
+ string DriveType,
+ int SeatsCount,
+ string BodyType,
+ string Class
+);
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..e800bef72
--- /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
+);
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..a64b73a0a
--- /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
+);
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..46cd41b82
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления поколения модели
+///
+/// Идентификатор модели
+/// Год выпуска
+/// Объём двигателя (л)
+/// Тип КПП
+/// Стоимость аренды в час (₽)
+public record ModelGenerationEditDto(
+ int ModelId,
+ int Year,
+ double EngineVolume,
+ string Transmission,
+ decimal RentalPricePerHour
+);
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..fa836dcc6
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs
@@ -0,0 +1,21 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для чтения данных поколения модели
+///
+/// Идентификатор
+/// Идентификатор модели
+/// Год выпуска
+/// Объём двигателя (л)
+/// Тип КПП
+/// Стоимость аренды в час (₽)
+/// Данные модели
+public record ModelGenerationGetDto(
+ int Id,
+ int ModelId,
+ int Year,
+ double EngineVolume,
+ string Transmission,
+ decimal RentalPricePerHour,
+ CarModelGetDto? Model
+);
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..acd772a6f
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs
@@ -0,0 +1,16 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для создания и обновления договора аренды.
+/// Используется в качестве контракта между генератором и сервером.
+///
+/// Дата и время начала аренды
+/// Продолжительность аренды в часах
+/// Идентификатор арендованного автомобиля
+/// Идентификатор клиента
+public record RentalEditDto(
+ DateTime RentalDate,
+ int RentalHours,
+ int CarId,
+ int ClientId
+);
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..68532b379
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs
@@ -0,0 +1,21 @@
+namespace CarRental.Application.Contracts.Dto;
+
+///
+/// DTO для чтения данных договора аренды
+///
+/// Идентификатор
+/// Идентификатор автомобиля
+/// Идентификатор клиента
+/// Дата и время выдачи
+/// Продолжительность аренды в часах
+/// Данные автомобиля
+/// Данные клиента
+public record RentalGetDto(
+ int Id,
+ int CarId,
+ int ClientId,
+ DateTime RentalDate,
+ int RentalHours,
+ CarGetDto? Car,
+ ClientGetDto? Client
+);
diff --git a/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs
new file mode 100644
index 000000000..f3533cf69
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs
@@ -0,0 +1,30 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+
+namespace CarRental.Application.Contracts;
+
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap();
+ CreateMap();
+
+ CreateMap()
+ .ForMember(d => d.Model, o => o.MapFrom(s => s.Model));
+ CreateMap();
+
+ CreateMap()
+ .ForMember(d => d.ModelGeneration, o => o.MapFrom(s => s.ModelGeneration));
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap()
+ .ForMember(d => d.Car, o => o.MapFrom(s => s.Car))
+ .ForMember(d => d.Client, o => o.MapFrom(s => s.Client));
+ CreateMap();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj
new file mode 100644
index 000000000..5900beb48
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj
@@ -0,0 +1,9 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs
new file mode 100644
index 000000000..1f073049f
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs
@@ -0,0 +1,138 @@
+using CarRental.Domain.Entities;
+
+namespace CarRental.Domain.Data;
+
+///
+/// Тестовые данные для пункта проката автомобилей.
+/// Используется для первоначального наполнения БД через EF Core HasData.
+///
+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 = "Mercedes C-Class", DriveType = "RWD", SeatsCount = 5, BodyType = "Sedan", Class = "Premium" },
+ new() { Id = 2, Name = "Volkswagen Passat", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Business" },
+ new() { Id = 3, Name = "Kia Rio", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Economy" },
+ new() { Id = 4, Name = "Toyota RAV4", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Mid-size" },
+ new() { Id = 5, Name = "Ferrari 488", DriveType = "RWD", SeatsCount = 2, BodyType = "Coupe", Class = "Supercar" },
+ new() { Id = 6, Name = "Nissan Patrol", DriveType = "4WD", SeatsCount = 7, BodyType = "SUV", Class = "Full-size" },
+ new() { Id = 7, Name = "Renault Logan", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Economy" },
+ new() { Id = 8, Name = "Mazda CX-5", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Mid-size" },
+ new() { Id = 9, Name = "Ford Transit", DriveType = "RWD", SeatsCount = 3, BodyType = "Van", Class = "Commercial"},
+ new() { Id = 10, Name = "Mitsubishi Outlander", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Mid-size" },
+ new() { Id = 11, Name = "Land Rover Defender", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Luxury" },
+ new() { Id = 12, Name = "Volvo XC60", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Premium" },
+ new() { Id = 13, Name = "Cadillac Escalade", DriveType = "AWD", SeatsCount = 7, BodyType = "SUV", Class = "Luxury" },
+ new() { Id = 14, Name = "Skoda Octavia", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Business" },
+ new() { Id = 15, Name = "Niva Legend", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" },
+ ];
+
+ ModelGenerations =
+ [
+ new() { Id = 1, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2500, ModelId = 1 },
+ new() { Id = 2, Year = 2022, EngineVolume = 1.8, Transmission = "AT", RentalPricePerHour = 1800, ModelId = 2 },
+ new() { Id = 3, Year = 2024, EngineVolume = 1.4, Transmission = "AT", RentalPricePerHour = 900, ModelId = 3 },
+ new() { Id = 4, Year = 2023, EngineVolume = 2.5, Transmission = "AT", RentalPricePerHour = 2200, ModelId = 4 },
+ new() { Id = 5, Year = 2021, EngineVolume = 3.9, Transmission = "AT", RentalPricePerHour = 15000, ModelId = 5 },
+ new() { Id = 6, Year = 2023, EngineVolume = 4.0, Transmission = "AT", RentalPricePerHour = 4000, ModelId = 6 },
+ new() { Id = 7, Year = 2024, EngineVolume = 1.6, Transmission = "MT", RentalPricePerHour = 800, ModelId = 7 },
+ new() { Id = 8, Year = 2024, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2000, ModelId = 8 },
+ new() { Id = 9, Year = 2022, EngineVolume = 2.2, Transmission = "MT", RentalPricePerHour = 1600, ModelId = 9 },
+ new() { Id = 10, Year = 2023, EngineVolume = 2.0, Transmission = "CVT", RentalPricePerHour = 1900, ModelId = 10 },
+ new() { Id = 11, Year = 2024, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 7000, ModelId = 11 },
+ new() { Id = 12, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 3500, ModelId = 12 },
+ new() { Id = 13, Year = 2022, EngineVolume = 6.2, Transmission = "AT", RentalPricePerHour = 5500, ModelId = 13 },
+ new() { Id = 14, Year = 2024, EngineVolume = 1.5, Transmission = "AT", RentalPricePerHour = 1400, ModelId = 14 },
+ new() { Id = 15, Year = 2023, EngineVolume = 1.7, Transmission = "MT", RentalPricePerHour = 950, ModelId = 15 },
+ ];
+
+ Cars =
+ [
+ new() { Id = 1, LicensePlate = "A001MB77", Color = "Black", ModelGenerationId = 1 },
+ new() { Id = 2, LicensePlate = "B222NO77", Color = "White", ModelGenerationId = 2 },
+ new() { Id = 3, LicensePlate = "C333RT99", Color = "Silver", ModelGenerationId = 3 },
+ new() { Id = 4, LicensePlate = "E444UF77", Color = "Blue", ModelGenerationId = 4 },
+ new() { Id = 5, LicensePlate = "K555FH77", Color = "Red", ModelGenerationId = 5 },
+ new() { Id = 6, LicensePlate = "M666HC99", Color = "Gray", ModelGenerationId = 6 },
+ new() { Id = 7, LicensePlate = "N777CH77", Color = "White", ModelGenerationId = 7 },
+ new() { Id = 8, LicensePlate = "O888SH77", Color = "Brown", ModelGenerationId = 8 },
+ new() { Id = 9, LicensePlate = "P999SH99", Color = "Yellow", ModelGenerationId = 9 },
+ new() { Id = 10, LicensePlate = "R100SE77", Color = "Black", ModelGenerationId = 10 },
+ new() { Id = 11, LicensePlate = "S200EY77", Color = "Green", ModelGenerationId = 11 },
+ new() { Id = 12, LicensePlate = "T300YA99", Color = "White", ModelGenerationId = 12 },
+ new() { Id = 13, LicensePlate = "U400AB77", Color = "Black", ModelGenerationId = 13 },
+ new() { Id = 14, LicensePlate = "H500BV99", Color = "Gray", ModelGenerationId = 14 },
+ new() { Id = 15, LicensePlate = "SH600VG77", Color = "Beige", ModelGenerationId = 15 },
+ ];
+
+ Clients =
+ [
+ new() { Id = 1, LicenseNumber = "2025-011", FullName = "Vasily Nekrasov", BirthDate = new DateOnly(1985, 3, 20) },
+ new() { Id = 2, LicenseNumber = "2025-022", FullName = "Irina Morozova", BirthDate = new DateOnly(1990, 7, 15) },
+ new() { Id = 3, LicenseNumber = "2025-033", FullName = "Sergei Volkov", BirthDate = new DateOnly(1988, 11, 5) },
+ new() { Id = 4, LicenseNumber = "2025-044", FullName = "Natalia Stepanova", BirthDate = new DateOnly(1992, 5, 28) },
+ new() { Id = 5, LicenseNumber = "2025-055", FullName = "Alexei Nikitin", BirthDate = new DateOnly(1978, 9, 12) },
+ new() { Id = 6, LicenseNumber = "2025-066", FullName = "Yulia Borisova", BirthDate = new DateOnly(1995, 2, 3) },
+ new() { Id = 7, LicenseNumber = "2025-077", FullName = "Dmitry Kirillov", BirthDate = new DateOnly(1983, 8, 25) },
+ new() { Id = 8, LicenseNumber = "2025-088", FullName = "Vera Sorokina", BirthDate = new DateOnly(1997, 12, 18) },
+ new() { Id = 9, LicenseNumber = "2025-099", FullName = "Konstantin Zhukov", BirthDate = new DateOnly(1986, 6, 30) },
+ new() { Id = 10, LicenseNumber = "2025-100", FullName = "Polina Veselova", BirthDate = new DateOnly(1993, 4, 7) },
+ new() { Id = 11, LicenseNumber = "2025-111", FullName = "Nikolai Kuznetsov", BirthDate = new DateOnly(1980, 10, 14) },
+ new() { Id = 12, LicenseNumber = "2025-122", FullName = "Ekaterina Savelyeva", BirthDate = new DateOnly(1998, 1, 22) },
+ new() { Id = 13, LicenseNumber = "2025-133", FullName = "Andrei Kotov", BirthDate = new DateOnly(1975, 7, 9) },
+ new() { Id = 14, LicenseNumber = "2025-144", FullName = "Valentina Osipova", BirthDate = new DateOnly(1982, 3, 16) },
+ new() { Id = 15, LicenseNumber = "2025-155", FullName = "Maxim Panin", BirthDate = new DateOnly(1999, 11, 1) },
+ ];
+
+ Rentals =
+ [
+ new() { Id = 1, CarId = 4, ClientId = 1, RentalDate = new DateTime(2025, 3, 4, 10, 0, 0), RentalHours = 48 },
+ new() { Id = 2, CarId = 4, ClientId = 3, RentalDate = new DateTime(2025, 2, 25, 14, 30, 0), RentalHours = 72 },
+ new() { Id = 3, CarId = 4, ClientId = 5, RentalDate = new DateTime(2025, 2, 20, 9, 15, 0), RentalHours = 24 },
+ new() { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2025, 2, 27, 11, 45, 0), RentalHours = 96 },
+ new() { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2025, 3, 1, 16, 0, 0), RentalHours = 120 },
+ new() { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2025, 2, 23, 13, 20, 0), RentalHours = 72 },
+ new() { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2025, 2, 18, 10, 10, 0), RentalHours = 48 },
+ new() { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2025, 2, 28, 8, 30, 0), RentalHours = 36 },
+ new() { Id = 9, CarId = 5, ClientId = 9, RentalDate = new DateTime(2025, 3, 3, 12, 0, 0), RentalHours = 96 },
+ new() { Id = 10, CarId = 6, ClientId = 10, RentalDate = new DateTime(2025, 2, 28, 7, 0, 0), RentalHours = 168 },
+ new() { Id = 11, CarId = 7, ClientId = 11, RentalDate = new DateTime(2025, 2, 22, 15, 45, 0), RentalHours = 72 },
+ new() { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2025, 2, 26, 9, 20, 0), RentalHours = 48 },
+ new() { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2025, 2, 28, 22, 0, 0), RentalHours = 60 },
+ new() { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2025, 2, 24, 11, 30, 0), RentalHours = 96 },
+ new() { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2025, 2, 10, 14, 15, 0), RentalHours = 120 },
+ new() { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2025, 2, 28, 14, 0, 0), RentalHours = 48 },
+ new() { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2025, 2, 5, 16, 45, 0), RentalHours = 72 },
+ new() { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2025, 2, 12, 10, 10, 0), RentalHours = 36 },
+ new() { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2025, 2, 16, 13, 30, 0), RentalHours = 84 },
+ ];
+
+ }
+
+ ///
+ /// Связывает навигационные свойства для использования в in-memory LINQ-запросах (тесты).
+ /// НЕ вызывать при передаче данных в EF Core HasData.
+ ///
+ public void WireNavigations()
+ {
+ foreach (var mg in ModelGenerations)
+ mg.Model = CarModels.First(m => m.Id == mg.ModelId);
+
+ foreach (var car in Cars)
+ car.ModelGeneration = ModelGenerations.First(mg => mg.Id == car.ModelGenerationId);
+
+ foreach (var rental in Rentals)
+ {
+ rental.Car = Cars.First(c => c.Id == rental.CarId);
+ rental.Client = Clients.First(c => c.Id == rental.ClientId);
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Car.cs b/CarRental/CarRental/CarRental.Domain/Entities/Car.cs
new file mode 100644
index 000000000..0ddb8d4db
--- /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
+{
+ ///
+ /// Уникальный идентификатор ТС
+ ///
+ [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; }
+
+ ///
+ /// FK на поколение модели
+ ///
+ [Column("model_generation_id")]
+ public required int ModelGenerationId { get; set; }
+
+ ///
+ /// Навигационное свойство: поколение модели
+ ///
+ public ModelGeneration? ModelGeneration { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs b/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs
new file mode 100644
index 000000000..9eb8bc59c
--- /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 RAV4")
+ ///
+ [Column("name")]
+ [MaxLength(100)]
+ public required string Name { get; set; }
+
+ ///
+ /// Тип привода (FWD / RWD / AWD / 4WD)
+ ///
+ [Column("drive_type")]
+ [MaxLength(10)]
+ public required string DriveType { get; set; }
+
+ ///
+ /// Число посадочных мест
+ ///
+ [Column("seats_count")]
+ public required int SeatsCount { get; set; }
+
+ ///
+ /// Тип кузова (Sedan, SUV, Coupe и т.д.)
+ ///
+ [Column("body_type")]
+ [MaxLength(30)]
+ public required string BodyType { get; set; }
+
+ ///
+ /// Класс автомобиля (Economy, Premium, Luxury и т.д.)
+ ///
+ [Column("class")]
+ [MaxLength(30)]
+ public required string Class { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Client.cs b/CarRental/CarRental/CarRental.Domain/Entities/Client.cs
new file mode 100644
index 000000000..89c63669b
--- /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
+{
+ ///
+ /// Уникальный идентификатор клиента
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Серия и номер водительского удостоверения
+ ///
+ [Column("license_number")]
+ [MaxLength(20)]
+ public required string LicenseNumber { get; set; }
+
+ ///
+ /// ФИО клиента
+ ///
+ [Column("full_name")]
+ [MaxLength(150)]
+ public required string FullName { get; set; }
+
+ ///
+ /// Дата рождения
+ ///
+ [Column("birth_date")]
+ public required DateOnly BirthDate { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs b/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs
new file mode 100644
index 000000000..c7e283cdf
--- /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; }
+
+ ///
+ /// FK на модель автомобиля
+ ///
+ [Column("model_id")]
+ public required int ModelId { get; set; }
+
+ ///
+ /// Год выпуска поколения
+ ///
+ [Column("year")]
+ public required int Year { get; set; }
+
+ ///
+ /// Объем двигателя в литрах
+ ///
+ [Column("engine_volume")]
+ public required double EngineVolume { get; set; }
+
+ ///
+ /// Тип коробки передач (MT / AT / CVT)
+ ///
+ [Column("transmission")]
+ [MaxLength(10)]
+ public required string Transmission { get; set; }
+
+ ///
+ /// Стоимость аренды в рублях за час
+ ///
+ [Column("rental_price_per_hour")]
+ public required decimal RentalPricePerHour { get; set; }
+
+ ///
+ /// Навигационное свойство: модель автомобиля
+ ///
+ public CarModel? Model { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs
new file mode 100644
index 000000000..1a3decca5
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs
@@ -0,0 +1,51 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace CarRental.Domain.Entities;
+
+///
+/// Договор аренды автомобиля.
+/// Контракт — фиксирует факт выдачи ТС клиенту.
+///
+[Table("rentals")]
+public class Rental
+{
+ ///
+ /// Уникальный идентификатор договора
+ ///
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// FK на арендованный автомобиль
+ ///
+ [Column("car_id")]
+ public required int CarId { get; set; }
+
+ ///
+ /// FK на клиента-арендатора
+ ///
+ [Column("client_id")]
+ public required int ClientId { get; set; }
+
+ ///
+ /// Дата и время выдачи автомобиля
+ ///
+ [Column("rental_date")]
+ public required DateTime RentalDate { get; set; }
+
+ ///
+ /// Продолжительность аренды в часах
+ ///
+ [Column("rental_hours")]
+ public required int RentalHours { get; set; }
+
+ ///
+ /// Навигационное свойство: арендованный автомобиль
+ ///
+ public Car? Car { get; set; }
+
+ ///
+ /// Навигационное свойство: клиент
+ ///
+ public Client? Client { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs
new file mode 100644
index 000000000..99ca51192
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs
@@ -0,0 +1,17 @@
+namespace CarRental.Domain.Interfaces;
+
+///
+/// Обобщённый интерфейс репозитория с поддержкой eager loading
+///
+public interface IRepository where T : class
+{
+ Task GetByIdAsync(int id);
+ Task> GetAllAsync();
+ Task AddAsync(T entity);
+ Task UpdateAsync(T entity);
+ Task DeleteAsync(int id);
+
+ Task GetByIdAsync(int id, Func, IQueryable>? include = null);
+ Task> GetAllAsync(Func, IQueryable>? include = null);
+ IQueryable GetQueryable(Func, IQueryable>? include = null);
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Models/Car.cs b/CarRental/CarRental/CarRental.Domain/Models/Car.cs
new file mode 100644
index 000000000..d8ff071c7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Models/Car.cs
@@ -0,0 +1,32 @@
+namespace CarRental.Domain.Models;
+
+///
+/// Транспортное средство в парке проката
+///
+public class Car
+{
+ ///
+ /// Уникальный идентификатор ТС
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// FK на поколение модели
+ ///
+ public required int ModelGenerationId { get; set; }
+
+ ///
+ /// Государственный регистрационный номер
+ ///
+ public required string LicensePlate { get; set; }
+
+ ///
+ /// Цвет кузова
+ ///
+ public required string Color { get; set; }
+
+ ///
+ /// Навигационное свойство: поколение модели
+ ///
+ public required ModelGeneration ModelGeneration { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Models/CarModel.cs b/CarRental/CarRental/CarRental.Domain/Models/CarModel.cs
new file mode 100644
index 000000000..f7568d574
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Models/CarModel.cs
@@ -0,0 +1,37 @@
+namespace CarRental.Domain.Models;
+
+///
+/// Модель автомобиля (справочник)
+///
+public class CarModel
+{
+ ///
+ /// Уникальный идентификатор модели
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Название модели (например, "Toyota RAV4")
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// Тип привода (FWD / RWD / AWD / 4WD)
+ ///
+ public required string DriveType { get; set; }
+
+ ///
+ /// Число посадочных мест
+ ///
+ public required int SeatsCount { get; set; }
+
+ ///
+ /// Тип кузова (Sedan, SUV, Coupe и т.д.)
+ ///
+ public required string BodyType { get; set; }
+
+ ///
+ /// Класс автомобиля (Economy, Premium, Luxury и т.д.)
+ ///
+ public required string Class { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Models/Client.cs b/CarRental/CarRental/CarRental.Domain/Models/Client.cs
new file mode 100644
index 000000000..11345477d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Models/Client.cs
@@ -0,0 +1,27 @@
+namespace CarRental.Domain.Models;
+
+///
+/// Клиент пункта проката
+///
+public class Client
+{
+ ///
+ /// Уникальный идентификатор клиента
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Серия и номер водительского удостоверения
+ ///
+ public required string LicenseNumber { get; set; }
+
+ ///
+ /// ФИО клиента
+ ///
+ public required string FullName { get; set; }
+
+ ///
+ /// Дата рождения
+ ///
+ public required DateOnly BirthDate { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Models/ModelGeneration.cs b/CarRental/CarRental/CarRental.Domain/Models/ModelGeneration.cs
new file mode 100644
index 000000000..39f53049d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Models/ModelGeneration.cs
@@ -0,0 +1,42 @@
+namespace CarRental.Domain.Models;
+
+///
+/// Поколение модели автомобиля (справочник)
+///
+public class ModelGeneration
+{
+ ///
+ /// Уникальный идентификатор поколения
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// FK на модель автомобиля
+ ///
+ public required int ModelId { get; set; }
+
+ ///
+ /// Год выпуска данного поколения
+ ///
+ public required int Year { get; set; }
+
+ ///
+ /// Объем двигателя в литрах
+ ///
+ public required double EngineVolume { get; set; }
+
+ ///
+ /// Тип коробки передач (MT / AT / CVT)
+ ///
+ public required string Transmission { get; set; }
+
+ ///
+ /// Стоимость аренды в рублях за час
+ ///
+ public required decimal RentalPricePerHour { get; set; }
+
+ ///
+ /// Навигационное свойство: модель автомобиля
+ ///
+ public required CarModel Model { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Domain/Models/Rental.cs b/CarRental/CarRental/CarRental.Domain/Models/Rental.cs
new file mode 100644
index 000000000..f24f87840
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Domain/Models/Rental.cs
@@ -0,0 +1,43 @@
+namespace CarRental.Domain.Models;
+
+///
+/// Договор аренды автомобиля.
+/// Используется в качестве контракта — фиксирует факт выдачи ТС клиенту.
+///
+public class Rental
+{
+ ///
+ /// Уникальный идентификатор договора аренды
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// FK на арендованный автомобиль
+ ///
+ public required int CarId { get; set; }
+
+ ///
+ /// FK на клиента
+ ///
+ public required int ClientId { get; set; }
+
+ ///
+ /// Дата и время выдачи автомобиля
+ ///
+ public required DateTime RentalDate { get; set; }
+
+ ///
+ /// Продолжительность аренды в часах
+ ///
+ public required int RentalHours { get; set; }
+
+ ///
+ /// Навигационное свойство: арендованный автомобиль
+ ///
+ public required Car Car { get; set; }
+
+ ///
+ /// Навигационное свойство: клиент-арендатор
+ ///
+ public required Client Client { get; set; }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/CarRental.Generator.Host.csproj b/CarRental/CarRental/CarRental.Generator.Host/CarRental.Generator.Host.csproj
new file mode 100644
index 000000000..c58f3f401
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/CarRental.Generator.Host.csproj
@@ -0,0 +1,18 @@
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs b/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs
new file mode 100644
index 000000000..386c6a25a
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs
@@ -0,0 +1,95 @@
+using CarRental.Application.Contracts.Dto;
+using CarRental.Generator.Host.Generator;
+using CarRental.Generator.Host.Messaging;
+using Microsoft.AspNetCore.Mvc;
+using System.Net.Http.Json;
+
+namespace CarRental.Generator.Host.Controllers;
+
+///
+/// Контроллер генератора договоров аренды.
+/// Генерирует тестовые данные и публикует их в RabbitMQ.
+///
+/// Публикатор сообщений RabbitMQ
+/// Фабрика HTTP-клиентов для запросов к API
+/// Логгер
+[ApiController]
+[Route("api/[controller]")]
+public class GeneratorController(
+ RentalPublisher publisher,
+ IHttpClientFactory httpClientFactory,
+ ILogger logger) : ControllerBase
+{
+ ///
+ /// Сгенерировать договоры и отправить их в RabbitMQ пакетами.
+ /// Идентификаторы автомобилей и клиентов берутся из базы данных через API.
+ ///
+ /// Общее количество генерируемых DTO
+ /// Размер одного пакета
+ /// Задержка между пакетами (мс)
+ /// Токен отмены
+ [HttpPost("rentals")]
+ [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GenerateRentals(
+ [FromQuery] int totalCount,
+ [FromQuery] int batchSize,
+ [FromQuery] int delayMs,
+ CancellationToken cancellationToken)
+ {
+ logger.LogInformation("{method}: totalCount={total} batchSize={batch} delayMs={delay}",
+ nameof(GenerateRentals), totalCount, batchSize, delayMs);
+
+ if (totalCount is <= 0 or > 10000)
+ return BadRequest("totalCount должно быть от 1 до 10 000");
+
+ if (batchSize is <= 0 or > 1000)
+ return BadRequest("batchSize должно быть от 1 до 1 000");
+
+ try
+ {
+ var http = httpClientFactory.CreateClient("carrental-api");
+
+ var cars = await http.GetFromJsonAsync>(
+ "/api/Cars", cancellationToken);
+ var clients = await http.GetFromJsonAsync>(
+ "/api/Clients", cancellationToken);
+
+ if (cars is null || cars.Count == 0)
+ return BadRequest("Не удалось получить список автомобилей из API");
+ if (clients is null || clients.Count == 0)
+ return BadRequest("Не удалось получить список клиентов из API");
+
+ var carIds = cars.Select(c => c.Id).ToList();
+ var clientIds = clients.Select(c => c.Id).ToList();
+
+ logger.LogInformation("Fetched {cars} cars and {clients} clients from API",
+ carIds.Count, clientIds.Count);
+
+ var items = RentalGenerator.Generate(totalCount, carIds, clientIds);
+
+ foreach (var chunk in items.Chunk(batchSize))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ publisher.Publish([.. chunk]);
+ await Task.Delay(delayMs, cancellationToken);
+ }
+
+ logger.LogInformation("{method} finished: sent {total} records in batches of {batch}",
+ nameof(GenerateRentals), totalCount, batchSize);
+
+ return Ok(items);
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ logger.LogWarning("{method} was cancelled", nameof(GenerateRentals));
+ return BadRequest("Запрос был отменён");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unexpected error in {method}", nameof(GenerateRentals));
+ return StatusCode(500, $"{ex.Message}\n{ex.InnerException?.Message}");
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs b/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs
new file mode 100644
index 000000000..f87e37804
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs
@@ -0,0 +1,26 @@
+using Bogus;
+using CarRental.Application.Contracts.Dto;
+
+namespace CarRental.Generator.Host.Generator;
+
+///
+/// Генерирует случайные договоры аренды с помощью библиотеки Bogus
+///
+public static class RentalGenerator
+{
+ ///
+ /// Сгенерировать список DTO договоров аренды на основе реальных идентификаторов
+ ///
+ /// Количество записей
+ /// Список допустимых идентификаторов автомобилей из базы данных
+ /// Список допустимых идентификаторов клиентов из базы данных
+ /// Список сгенерированных DTO
+ public static IList Generate(int count, IList carIds, IList clientIds) =>
+ new Faker()
+ .CustomInstantiator(f => new RentalEditDto(
+ RentalDate: f.Date.Between(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow.AddMonths(2)),
+ RentalHours: f.Random.Int(1, 72),
+ CarId: f.PickRandom(carIds),
+ ClientId: f.PickRandom(clientIds)))
+ .Generate(count);
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/Messaging/RentalPublisher.cs b/CarRental/CarRental/CarRental.Generator.Host/Messaging/RentalPublisher.cs
new file mode 100644
index 000000000..c5dddf1fd
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/Messaging/RentalPublisher.cs
@@ -0,0 +1,73 @@
+using CarRental.Application.Contracts.Dto;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using RabbitMQ.Client;
+using System.Text;
+using System.Text.Json;
+
+namespace CarRental.Generator.Host.Messaging;
+
+///
+/// Публикует пакеты договоров аренды в обменник RabbitMQ
+///
+public class RentalPublisher
+{
+ private readonly IModel _channel;
+ private readonly ILogger _logger;
+ private readonly string _exchangeName;
+
+ public RentalPublisher(
+ IConnection connection,
+ IConfiguration configuration,
+ ILogger logger)
+ {
+ _logger = logger;
+ _exchangeName = configuration.GetSection("RabbitMQ")["ExchangeName"]
+ ?? throw new KeyNotFoundException("RabbitMQ:ExchangeName is missing");
+
+ _channel = connection.CreateModel();
+ _channel.ExchangeDeclare(_exchangeName, ExchangeType.Fanout, durable: true);
+
+ _logger.LogInformation("RentalPublisher initialized, exchange={exchange}", _exchangeName);
+ }
+
+ ///
+ /// Отправить пакет договоров аренды в RabbitMQ
+ ///
+ /// Пакет DTO для отправки
+ public void Publish(IList batch)
+ {
+ if (batch is null || batch.Count == 0)
+ {
+ _logger.LogWarning("Publish called with empty batch, skipping");
+ return;
+ }
+
+ var msgId = Guid.NewGuid().ToString();
+
+ try
+ {
+ var json = JsonSerializer.Serialize(batch);
+ var body = Encoding.UTF8.GetBytes(json);
+
+ var props = _channel.CreateBasicProperties();
+ props.Persistent = true;
+ props.MessageId = msgId;
+ props.ContentType = "application/json";
+
+ _channel.BasicPublish(
+ exchange: _exchangeName,
+ routingKey: "",
+ basicProperties: props,
+ body: body);
+
+ _logger.LogInformation("Published batch msgId={msgId} count={count} to exchange={exchange}",
+ msgId, batch.Count, _exchangeName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to publish batch msgId={msgId} count={count}", msgId, batch.Count);
+ throw;
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/Program.cs b/CarRental/CarRental/CarRental.Generator.Host/Program.cs
new file mode 100644
index 000000000..3dbf48d7b
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/Program.cs
@@ -0,0 +1,46 @@
+using CarRental.Generator.Host.Messaging;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+// Регистрация RabbitMQ-соединения через Aspire
+builder.AddRabbitMQClient("carrental-rabbitmq");
+
+builder.Services.AddScoped();
+
+// HTTP-клиент для запросов к CarRental API (Aspire service discovery)
+builder.Services.AddHttpClient("carrental-api", c =>
+{
+ c.BaseAddress = new Uri("https+http://carrental-api");
+});
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name?.StartsWith("CarRental") == true);
+
+ foreach (var asm in assemblies)
+ {
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{asm.GetName().Name}.xml");
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+app.Run();
diff --git a/CarRental/CarRental/CarRental.Generator.Host/Properties/launchSettings.json b/CarRental/CarRental/CarRental.Generator.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..8f7261302
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "CarRental.Generator.Host": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7200;http://localhost:5200",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/appsettings.Development.json b/CarRental/CarRental/CarRental.Generator.Host/appsettings.Development.json
new file mode 100644
index 000000000..a6e86ace7
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Generator.Host/appsettings.json b/CarRental/CarRental/CarRental.Generator.Host/appsettings.json
new file mode 100644
index 000000000..2953d93ec
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Generator.Host/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "RabbitMQ": {
+ "ExchangeName": "rental-exchange"
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Messaging/CarRental.Infrastructure.Messaging.csproj b/CarRental/CarRental/CarRental.Infrastructure.Messaging/CarRental.Infrastructure.Messaging.csproj
new file mode 100644
index 000000000..2500eb817
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Messaging/CarRental.Infrastructure.Messaging.csproj
@@ -0,0 +1,16 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Infrastructure.Messaging/RentalQueueConsumer.cs b/CarRental/CarRental/CarRental.Infrastructure.Messaging/RentalQueueConsumer.cs
new file mode 100644
index 000000000..85f025bc8
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure.Messaging/RentalQueueConsumer.cs
@@ -0,0 +1,191 @@
+using AutoMapper;
+using CarRental.Application.Contracts.Dto;
+using CarRental.Domain.Entities;
+using CarRental.Domain.Interfaces;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using RabbitMQ.Client;
+using RabbitMQ.Client.Events;
+using System.Text;
+using System.Text.Json;
+
+namespace CarRental.Infrastructure.Messaging;
+
+///
+/// Фоновый сервис — потребитель сообщений из очереди RabbitMQ.
+/// Получает пакеты договоров аренды и сохраняет их в базу данных.
+///
+public class RentalQueueConsumer : BackgroundService
+{
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly ILogger _logger;
+ private readonly IMapper _mapper;
+ private readonly string _exchangeName;
+ private readonly string _queueName;
+ private IConnection? _connection;
+ private IModel? _channel;
+
+ public RentalQueueConsumer(
+ IConnectionFactory connectionFactory,
+ IServiceScopeFactory scopeFactory,
+ IConfiguration configuration,
+ ILogger logger,
+ IMapper mapper)
+ {
+ _scopeFactory = scopeFactory;
+ _logger = logger;
+ _mapper = mapper;
+ _exchangeName = configuration.GetSection("RabbitMQ")["ExchangeName"]
+ ?? throw new KeyNotFoundException("RabbitMQ:ExchangeName is missing");
+ _queueName = configuration.GetSection("RabbitMQ")["QueueName"]
+ ?? throw new KeyNotFoundException("RabbitMQ:QueueName is missing");
+
+ InitializeConnection(connectionFactory);
+ }
+
+ private void InitializeConnection(IConnectionFactory factory)
+ {
+ const int maxRetries = 10;
+ const int delayMs = 3000;
+
+ for (int attempt = 1; attempt <= maxRetries; attempt++)
+ {
+ try
+ {
+ _connection = factory.CreateConnection();
+ _channel = _connection.CreateModel();
+
+ _channel.ExchangeDeclare(_exchangeName, ExchangeType.Fanout, durable: true);
+ _channel.QueueDeclare(_queueName, durable: true, exclusive: false, autoDelete: false);
+ _channel.QueueBind(_queueName, _exchangeName, routingKey: "");
+ _channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
+
+ _logger.LogInformation("Connected to RabbitMQ, exchange={exchange}, queue={queue}",
+ _exchangeName, _queueName);
+ return;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "RabbitMQ connection attempt {attempt}/{max} failed, retrying in {delay}ms",
+ attempt, maxRetries, delayMs);
+
+ if (attempt == maxRetries)
+ throw;
+
+ Thread.Sleep(delayMs);
+ }
+ }
+ }
+
+ ///
+ /// Запускает цикл потребления сообщений из очереди RabbitMQ
+ ///
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ stoppingToken.Register(() => _logger.LogInformation("RentalQueueConsumer is stopping"));
+
+ if (_channel is null)
+ {
+ _logger.LogError("RabbitMQ channel is not initialized");
+ return Task.CompletedTask;
+ }
+
+ var consumer = new EventingBasicConsumer(_channel);
+ consumer.Received += async (_, ea) =>
+ {
+ string? msgId = null;
+ try
+ {
+ msgId = ea.BasicProperties?.MessageId;
+ var json = Encoding.UTF8.GetString(ea.Body.Span);
+ var batch = JsonSerializer.Deserialize>(json);
+
+ if (batch is null || batch.Count == 0)
+ {
+ _logger.LogWarning("Received empty batch, msgId={msgId}", msgId);
+ _channel.BasicAck(ea.DeliveryTag, multiple: false);
+ return;
+ }
+
+ _logger.LogInformation("Processing message msgId={msgId} with {count} contracts", msgId, batch.Count);
+ await ProcessBatchAsync(batch, msgId, stoppingToken);
+ _channel.BasicAck(ea.DeliveryTag, multiple: false);
+
+ _logger.LogInformation("Acknowledged message msgId={msgId}", msgId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to process message msgId={msgId}", msgId);
+ _channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);
+ }
+ };
+
+ _channel.BasicConsume(_queueName, autoAck: false, consumer);
+ _logger.LogInformation("Started consuming from queue {queue}", _queueName);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Обрабатывает пакет DTO: проверяет ссылочную целостность и сохраняет валидные записи
+ ///
+ private async Task ProcessBatchAsync(
+ IList batch,
+ string? msgId,
+ CancellationToken cancellationToken)
+ {
+ using var scope = _scopeFactory.CreateScope();
+ var rentalRepo = scope.ServiceProvider.GetRequiredService>();
+ var carRepo = scope.ServiceProvider.GetRequiredService>();
+ var clientRepo = scope.ServiceProvider.GetRequiredService>();
+
+ var carIds = batch.Select(r => r.CarId).Distinct().ToList();
+ var clientIds = batch.Select(r => r.ClientId).Distinct().ToList();
+
+ var validCarIds = (await carRepo.GetQueryable()
+ .Where(c => carIds.Contains(c.Id))
+ .Select(c => c.Id)
+ .ToListAsync(cancellationToken)).ToHashSet();
+
+ var validClientIds = (await clientRepo.GetQueryable()
+ .Where(c => clientIds.Contains(c.Id))
+ .Select(c => c.Id)
+ .ToListAsync(cancellationToken)).ToHashSet();
+
+ foreach (var dto in batch)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!validCarIds.Contains(dto.CarId) || !validClientIds.Contains(dto.ClientId))
+ {
+ _logger.LogWarning(
+ "Skipping contract in msgId={msgId}: CarId={carId} or ClientId={clientId} not found",
+ msgId, dto.CarId, dto.ClientId);
+ continue;
+ }
+
+ try
+ {
+ var rental = _mapper.Map(dto);
+ await rentalRepo.AddAsync(rental);
+ _logger.LogInformation("Saved rental from msgId={msgId} CarId={carId} ClientId={clientId}",
+ msgId, dto.CarId, dto.ClientId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving rental from msgId={msgId} CarId={carId} ClientId={clientId}",
+ msgId, dto.CarId, dto.ClientId);
+ }
+ }
+ }
+
+ public override void Dispose()
+ {
+ try { _channel?.Close(); } catch { /* ignore */ }
+ try { _connection?.Close(); } catch { /* ignore */ }
+ base.Dispose();
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj
new file mode 100644
index 000000000..b252c0800
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj
@@ -0,0 +1,13 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.Designer.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.Designer.cs
new file mode 100644
index 000000000..3024425ba
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.Designer.cs
@@ -0,0 +1,336 @@
+//
+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("20260222120000_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 = "A001MB77", ModelGenerationId = 1 },
+ new { Id = 2, Color = "White", LicensePlate = "B222NO77", ModelGenerationId = 2 },
+ new { Id = 3, Color = "Silver", LicensePlate = "C333RT99", ModelGenerationId = 3 },
+ new { Id = 4, Color = "Blue", LicensePlate = "E444UF77", ModelGenerationId = 4 },
+ new { Id = 5, Color = "Red", LicensePlate = "K555FH77", ModelGenerationId = 5 },
+ new { Id = 6, Color = "Gray", LicensePlate = "M666HC99", ModelGenerationId = 6 },
+ new { Id = 7, Color = "White", LicensePlate = "N777CH77", ModelGenerationId = 7 },
+ new { Id = 8, Color = "Brown", LicensePlate = "O888SH77", ModelGenerationId = 8 },
+ new { Id = 9, Color = "Yellow", LicensePlate = "P999SH99", ModelGenerationId = 9 },
+ new { Id = 10, Color = "Black", LicensePlate = "R100SE77", ModelGenerationId = 10 },
+ new { Id = 11, Color = "Green", LicensePlate = "S200EY77", ModelGenerationId = 11 },
+ new { Id = 12, Color = "White", LicensePlate = "T300YA99", ModelGenerationId = 12 },
+ new { Id = 13, Color = "Black", LicensePlate = "U400AB77", ModelGenerationId = 13 },
+ new { Id = 14, Color = "Gray", LicensePlate = "H500BV99", ModelGenerationId = 14 },
+ new { Id = 15, Color = "Beige", LicensePlate = "SH600VG77", 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(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("body_type");
+
+ b.Property("Class")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("class");
+
+ b.Property("DriveType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("drive_type");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .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 = "Mercedes C-Class", SeatsCount = 5 },
+ new { Id = 2, BodyType = "Sedan", Class = "Business", DriveType = "FWD", Name = "Volkswagen Passat", SeatsCount = 5 },
+ new { Id = 3, BodyType = "Sedan", Class = "Economy", DriveType = "FWD", Name = "Kia Rio", SeatsCount = 5 },
+ new { Id = 4, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Toyota RAV4", SeatsCount = 5 },
+ new { Id = 5, BodyType = "Coupe", Class = "Supercar", DriveType = "RWD", Name = "Ferrari 488", SeatsCount = 2 },
+ new { Id = 6, BodyType = "SUV", Class = "Full-size", DriveType = "4WD", Name = "Nissan Patrol", SeatsCount = 7 },
+ new { Id = 7, BodyType = "Sedan", Class = "Economy", DriveType = "FWD", Name = "Renault Logan", SeatsCount = 5 },
+ new { Id = 8, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Mazda CX-5", SeatsCount = 5 },
+ new { Id = 9, BodyType = "Van", Class = "Commercial", DriveType = "RWD", Name = "Ford Transit", SeatsCount = 3 },
+ new { Id = 10, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Mitsubishi Outlander", SeatsCount = 5 },
+ new { Id = 11, BodyType = "SUV", Class = "Luxury", DriveType = "4WD", Name = "Land Rover Defender", SeatsCount = 5 },
+ new { Id = 12, BodyType = "SUV", Class = "Premium", DriveType = "AWD", Name = "Volvo XC60", SeatsCount = 5 },
+ new { Id = 13, BodyType = "SUV", Class = "Luxury", DriveType = "AWD", Name = "Cadillac Escalade", SeatsCount = 7 },
+ new { Id = 14, BodyType = "Sedan", Class = "Business", DriveType = "FWD", Name = "Skoda Octavia", SeatsCount = 5 },
+ new { Id = 15, BodyType = "SUV", Class = "Off-road", DriveType = "4WD", Name = "Niva Legend", 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(150)
+ .HasColumnType("nvarchar(150)")
+ .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(1985, 3, 20), FullName = "Vasily Nekrasov", LicenseNumber = "2025-011" },
+ new { Id = 2, BirthDate = new DateOnly(1990, 7, 15), FullName = "Irina Morozova", LicenseNumber = "2025-022" },
+ new { Id = 3, BirthDate = new DateOnly(1988, 11, 5), FullName = "Sergei Volkov", LicenseNumber = "2025-033" },
+ new { Id = 4, BirthDate = new DateOnly(1992, 5, 28), FullName = "Natalia Stepanova", LicenseNumber = "2025-044" },
+ new { Id = 5, BirthDate = new DateOnly(1978, 9, 12), FullName = "Alexei Nikitin", LicenseNumber = "2025-055" },
+ new { Id = 6, BirthDate = new DateOnly(1995, 2, 3), FullName = "Yulia Borisova", LicenseNumber = "2025-066" },
+ new { Id = 7, BirthDate = new DateOnly(1983, 8, 25), FullName = "Dmitry Kirillov", LicenseNumber = "2025-077" },
+ new { Id = 8, BirthDate = new DateOnly(1997, 12, 18), FullName = "Vera Sorokina", LicenseNumber = "2025-088" },
+ new { Id = 9, BirthDate = new DateOnly(1986, 6, 30), FullName = "Konstantin Zhukov", LicenseNumber = "2025-099" },
+ new { Id = 10, BirthDate = new DateOnly(1993, 4, 7), FullName = "Polina Veselova", LicenseNumber = "2025-100" },
+ new { Id = 11, BirthDate = new DateOnly(1980, 10, 14), FullName = "Nikolai Kuznetsov", LicenseNumber = "2025-111" },
+ new { Id = 12, BirthDate = new DateOnly(1998, 1, 22), FullName = "Ekaterina Savelyeva", LicenseNumber = "2025-122" },
+ new { Id = 13, BirthDate = new DateOnly(1975, 7, 9), FullName = "Andrei Kotov", LicenseNumber = "2025-133" },
+ new { Id = 14, BirthDate = new DateOnly(1982, 3, 16), FullName = "Valentina Osipova", LicenseNumber = "2025-144" },
+ new { Id = 15, BirthDate = new DateOnly(1999, 11, 1), FullName = "Maxim Panin", LicenseNumber = "2025-155" });
+ });
+
+ 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 = 2500m, Transmission = "AT", Year = 2023 },
+ new { Id = 2, EngineVolume = 1.8, ModelId = 2, RentalPricePerHour = 1800m, Transmission = "AT", Year = 2022 },
+ new { Id = 3, EngineVolume = 1.4, ModelId = 3, RentalPricePerHour = 900m, Transmission = "AT", Year = 2024 },
+ new { Id = 4, EngineVolume = 2.5, ModelId = 4, RentalPricePerHour = 2200m, Transmission = "AT", Year = 2023 },
+ new { Id = 5, EngineVolume = 3.9, ModelId = 5, RentalPricePerHour = 15000m, Transmission = "AT", Year = 2021 },
+ new { Id = 6, EngineVolume = 4.0, ModelId = 6, RentalPricePerHour = 4000m, Transmission = "AT", Year = 2023 },
+ new { Id = 7, EngineVolume = 1.6, ModelId = 7, RentalPricePerHour = 800m, Transmission = "MT", Year = 2024 },
+ new { Id = 8, EngineVolume = 2.0, ModelId = 8, RentalPricePerHour = 2000m, Transmission = "AT", Year = 2024 },
+ new { Id = 9, EngineVolume = 2.2, ModelId = 9, RentalPricePerHour = 1600m, Transmission = "MT", Year = 2022 },
+ new { Id = 10, EngineVolume = 2.0, ModelId = 10, RentalPricePerHour = 1900m, Transmission = "CVT", Year = 2023 },
+ new { Id = 11, EngineVolume = 3.0, ModelId = 11, RentalPricePerHour = 7000m, Transmission = "AT", Year = 2024 },
+ new { Id = 12, EngineVolume = 2.0, ModelId = 12, RentalPricePerHour = 3500m, Transmission = "AT", Year = 2023 },
+ new { Id = 13, EngineVolume = 6.2, ModelId = 13, RentalPricePerHour = 5500m, Transmission = "AT", Year = 2022 },
+ new { Id = 14, EngineVolume = 1.5, ModelId = 14, RentalPricePerHour = 1400m, Transmission = "AT", Year = 2024 },
+ new { Id = 15, EngineVolume = 1.7, ModelId = 15, RentalPricePerHour = 950m, 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 = 4, ClientId = 1, RentalDate = new DateTime(2025, 3, 4, 10, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 2, CarId = 4, ClientId = 3, RentalDate = new DateTime(2025, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 3, CarId = 4, ClientId = 5, RentalDate = new DateTime(2025, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), RentalHours = 24 },
+ new { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2025, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2025, 3, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 120 },
+ new { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2025, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2025, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2025, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 36 },
+ new { Id = 9, CarId = 5, ClientId = 9, RentalDate = new DateTime(2025, 3, 3, 12, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 10, CarId = 6, ClientId = 10, RentalDate = new DateTime(2025, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 168 },
+ new { Id = 11, CarId = 7, ClientId = 11, RentalDate = new DateTime(2025, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2025, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2025, 2, 28, 22, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 60 },
+ new { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2025, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2025, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), RentalHours = 120 },
+ new { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2025, 2, 28, 14, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2025, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2025, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), RentalHours = 36 },
+ new { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2025, 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/20260222120000_InitialCreate.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.cs
new file mode 100644
index 000000000..18472af34
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.cs
@@ -0,0 +1,265 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814
+
+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(100)", maxLength: 100, 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(30)", maxLength: 30, nullable: false),
+ @class = table.Column(name: "class", type: "nvarchar(30)", maxLength: 30, 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(150)", maxLength: 150, 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"),
+ model_id = table.Column(type: "int", nullable: false),
+ 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)
+ },
+ 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"),
+ car_id = table.Column(type: "int", nullable: false),
+ client_id = table.Column(type: "int", nullable: false),
+ rental_date = table.Column(type: "datetime2", nullable: false),
+ rental_hours = 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", "Mercedes C-Class", 5 },
+ { 2, "Sedan", "Business", "FWD", "Volkswagen Passat", 5 },
+ { 3, "Sedan", "Economy", "FWD", "Kia Rio", 5 },
+ { 4, "SUV", "Mid-size", "AWD", "Toyota RAV4", 5 },
+ { 5, "Coupe", "Supercar", "RWD", "Ferrari 488", 2 },
+ { 6, "SUV", "Full-size", "4WD", "Nissan Patrol", 7 },
+ { 7, "Sedan", "Economy", "FWD", "Renault Logan", 5 },
+ { 8, "SUV", "Mid-size", "AWD", "Mazda CX-5", 5 },
+ { 9, "Van", "Commercial", "RWD", "Ford Transit", 3 },
+ { 10, "SUV", "Mid-size", "AWD", "Mitsubishi Outlander", 5 },
+ { 11, "SUV", "Luxury", "4WD", "Land Rover Defender", 5 },
+ { 12, "SUV", "Premium", "AWD", "Volvo XC60", 5 },
+ { 13, "SUV", "Luxury", "AWD", "Cadillac Escalade", 7 },
+ { 14, "Sedan", "Business", "FWD", "Skoda Octavia", 5 },
+ { 15, "SUV", "Off-road", "4WD", "Niva Legend", 5 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "clients",
+ columns: new[] { "id", "birth_date", "full_name", "license_number" },
+ values: new object[,]
+ {
+ { 1, new DateOnly(1985, 3, 20), "Vasily Nekrasov", "2025-011" },
+ { 2, new DateOnly(1990, 7, 15), "Irina Morozova", "2025-022" },
+ { 3, new DateOnly(1988, 11, 5), "Sergei Volkov", "2025-033" },
+ { 4, new DateOnly(1992, 5, 28), "Natalia Stepanova", "2025-044" },
+ { 5, new DateOnly(1978, 9, 12), "Alexei Nikitin", "2025-055" },
+ { 6, new DateOnly(1995, 2, 3), "Yulia Borisova", "2025-066" },
+ { 7, new DateOnly(1983, 8, 25), "Dmitry Kirillov", "2025-077" },
+ { 8, new DateOnly(1997, 12, 18), "Vera Sorokina", "2025-088" },
+ { 9, new DateOnly(1986, 6, 30), "Konstantin Zhukov", "2025-099" },
+ { 10, new DateOnly(1993, 4, 7), "Polina Veselova", "2025-100" },
+ { 11, new DateOnly(1980, 10, 14), "Nikolai Kuznetsov", "2025-111" },
+ { 12, new DateOnly(1998, 1, 22), "Ekaterina Savelyeva", "2025-122" },
+ { 13, new DateOnly(1975, 7, 9), "Andrei Kotov", "2025-133" },
+ { 14, new DateOnly(1982, 3, 16), "Valentina Osipova", "2025-144" },
+ { 15, new DateOnly(1999, 11, 1), "Maxim Panin", "2025-155" }
+ });
+
+ 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, 2500m, "AT", 2023 },
+ { 2, 1.8, 2, 1800m, "AT", 2022 },
+ { 3, 1.4, 3, 900m, "AT", 2024 },
+ { 4, 2.5, 4, 2200m, "AT", 2023 },
+ { 5, 3.9, 5, 15000m, "AT", 2021 },
+ { 6, 4.0, 6, 4000m, "AT", 2023 },
+ { 7, 1.6, 7, 800m, "MT", 2024 },
+ { 8, 2.0, 8, 2000m, "AT", 2024 },
+ { 9, 2.2, 9, 1600m, "MT", 2022 },
+ { 10, 2.0, 10, 1900m, "CVT", 2023 },
+ { 11, 3.0, 11, 7000m, "AT", 2024 },
+ { 12, 2.0, 12, 3500m, "AT", 2023 },
+ { 13, 6.2, 13, 5500m, "AT", 2022 },
+ { 14, 1.5, 14, 1400m, "AT", 2024 },
+ { 15, 1.7, 15, 950m, "MT", 2023 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "cars",
+ columns: new[] { "id", "color", "license_plate", "model_generation_id" },
+ values: new object[,]
+ {
+ { 1, "Black", "A001MB77", 1 },
+ { 2, "White", "B222NO77", 2 },
+ { 3, "Silver", "C333RT99", 3 },
+ { 4, "Blue", "E444UF77", 4 },
+ { 5, "Red", "K555FH77", 5 },
+ { 6, "Gray", "M666HC99", 6 },
+ { 7, "White", "N777CH77", 7 },
+ { 8, "Brown", "O888SH77", 8 },
+ { 9, "Yellow", "P999SH99", 9 },
+ { 10, "Black", "R100SE77", 10 },
+ { 11, "Green", "S200EY77", 11 },
+ { 12, "White", "T300YA99", 12 },
+ { 13, "Black", "U400AB77", 13 },
+ { 14, "Gray", "H500BV99", 14 },
+ { 15, "Beige", "SH600VG77", 15 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "rentals",
+ columns: new[] { "id", "car_id", "client_id", "rental_date", "rental_hours" },
+ values: new object[,]
+ {
+ { 1, 4, 1, new DateTime(2025, 3, 4, 10, 0, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 2, 4, 3, new DateTime(2025, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 3, 4, 5, new DateTime(2025, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), 24 },
+ { 4, 1, 2, new DateTime(2025, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 5, 1, 4, new DateTime(2025, 3, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), 120 },
+ { 6, 2, 6, new DateTime(2025, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 7, 2, 8, new DateTime(2025, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 8, 3, 7, new DateTime(2025, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), 36 },
+ { 9, 5, 9, new DateTime(2025, 3, 3, 12, 0, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 10, 6, 10, new DateTime(2025, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), 168 },
+ { 11, 7, 11, new DateTime(2025, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 12, 8, 12, new DateTime(2025, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 13, 9, 13, new DateTime(2025, 2, 28, 22, 0, 0, 0, DateTimeKind.Unspecified), 60 },
+ { 14, 10, 14, new DateTime(2025, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), 96 },
+ { 15, 11, 15, new DateTime(2025, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), 120 },
+ { 16, 12, 1, new DateTime(2025, 2, 28, 14, 0, 0, 0, DateTimeKind.Unspecified), 48 },
+ { 17, 13, 2, new DateTime(2025, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), 72 },
+ { 18, 14, 3, new DateTime(2025, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), 36 },
+ { 19, 15, 4, new DateTime(2025, 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..72391c8cd
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,333 @@
+//
+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 = "A001MB77", ModelGenerationId = 1 },
+ new { Id = 2, Color = "White", LicensePlate = "B222NO77", ModelGenerationId = 2 },
+ new { Id = 3, Color = "Silver", LicensePlate = "C333RT99", ModelGenerationId = 3 },
+ new { Id = 4, Color = "Blue", LicensePlate = "E444UF77", ModelGenerationId = 4 },
+ new { Id = 5, Color = "Red", LicensePlate = "K555FH77", ModelGenerationId = 5 },
+ new { Id = 6, Color = "Gray", LicensePlate = "M666HC99", ModelGenerationId = 6 },
+ new { Id = 7, Color = "White", LicensePlate = "N777CH77", ModelGenerationId = 7 },
+ new { Id = 8, Color = "Brown", LicensePlate = "O888SH77", ModelGenerationId = 8 },
+ new { Id = 9, Color = "Yellow", LicensePlate = "P999SH99", ModelGenerationId = 9 },
+ new { Id = 10, Color = "Black", LicensePlate = "R100SE77", ModelGenerationId = 10 },
+ new { Id = 11, Color = "Green", LicensePlate = "S200EY77", ModelGenerationId = 11 },
+ new { Id = 12, Color = "White", LicensePlate = "T300YA99", ModelGenerationId = 12 },
+ new { Id = 13, Color = "Black", LicensePlate = "U400AB77", ModelGenerationId = 13 },
+ new { Id = 14, Color = "Gray", LicensePlate = "H500BV99", ModelGenerationId = 14 },
+ new { Id = 15, Color = "Beige", LicensePlate = "SH600VG77", 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(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("body_type");
+
+ b.Property("Class")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("nvarchar(30)")
+ .HasColumnName("class");
+
+ b.Property("DriveType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)")
+ .HasColumnName("drive_type");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .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 = "Mercedes C-Class", SeatsCount = 5 },
+ new { Id = 2, BodyType = "Sedan", Class = "Business", DriveType = "FWD", Name = "Volkswagen Passat", SeatsCount = 5 },
+ new { Id = 3, BodyType = "Sedan", Class = "Economy", DriveType = "FWD", Name = "Kia Rio", SeatsCount = 5 },
+ new { Id = 4, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Toyota RAV4", SeatsCount = 5 },
+ new { Id = 5, BodyType = "Coupe", Class = "Supercar", DriveType = "RWD", Name = "Ferrari 488", SeatsCount = 2 },
+ new { Id = 6, BodyType = "SUV", Class = "Full-size", DriveType = "4WD", Name = "Nissan Patrol", SeatsCount = 7 },
+ new { Id = 7, BodyType = "Sedan", Class = "Economy", DriveType = "FWD", Name = "Renault Logan", SeatsCount = 5 },
+ new { Id = 8, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Mazda CX-5", SeatsCount = 5 },
+ new { Id = 9, BodyType = "Van", Class = "Commercial", DriveType = "RWD", Name = "Ford Transit", SeatsCount = 3 },
+ new { Id = 10, BodyType = "SUV", Class = "Mid-size", DriveType = "AWD", Name = "Mitsubishi Outlander", SeatsCount = 5 },
+ new { Id = 11, BodyType = "SUV", Class = "Luxury", DriveType = "4WD", Name = "Land Rover Defender", SeatsCount = 5 },
+ new { Id = 12, BodyType = "SUV", Class = "Premium", DriveType = "AWD", Name = "Volvo XC60", SeatsCount = 5 },
+ new { Id = 13, BodyType = "SUV", Class = "Luxury", DriveType = "AWD", Name = "Cadillac Escalade", SeatsCount = 7 },
+ new { Id = 14, BodyType = "Sedan", Class = "Business", DriveType = "FWD", Name = "Skoda Octavia", SeatsCount = 5 },
+ new { Id = 15, BodyType = "SUV", Class = "Off-road", DriveType = "4WD", Name = "Niva Legend", 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(150)
+ .HasColumnType("nvarchar(150)")
+ .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(1985, 3, 20), FullName = "Vasily Nekrasov", LicenseNumber = "2025-011" },
+ new { Id = 2, BirthDate = new DateOnly(1990, 7, 15), FullName = "Irina Morozova", LicenseNumber = "2025-022" },
+ new { Id = 3, BirthDate = new DateOnly(1988, 11, 5), FullName = "Sergei Volkov", LicenseNumber = "2025-033" },
+ new { Id = 4, BirthDate = new DateOnly(1992, 5, 28), FullName = "Natalia Stepanova", LicenseNumber = "2025-044" },
+ new { Id = 5, BirthDate = new DateOnly(1978, 9, 12), FullName = "Alexei Nikitin", LicenseNumber = "2025-055" },
+ new { Id = 6, BirthDate = new DateOnly(1995, 2, 3), FullName = "Yulia Borisova", LicenseNumber = "2025-066" },
+ new { Id = 7, BirthDate = new DateOnly(1983, 8, 25), FullName = "Dmitry Kirillov", LicenseNumber = "2025-077" },
+ new { Id = 8, BirthDate = new DateOnly(1997, 12, 18), FullName = "Vera Sorokina", LicenseNumber = "2025-088" },
+ new { Id = 9, BirthDate = new DateOnly(1986, 6, 30), FullName = "Konstantin Zhukov", LicenseNumber = "2025-099" },
+ new { Id = 10, BirthDate = new DateOnly(1993, 4, 7), FullName = "Polina Veselova", LicenseNumber = "2025-100" },
+ new { Id = 11, BirthDate = new DateOnly(1980, 10, 14), FullName = "Nikolai Kuznetsov", LicenseNumber = "2025-111" },
+ new { Id = 12, BirthDate = new DateOnly(1998, 1, 22), FullName = "Ekaterina Savelyeva", LicenseNumber = "2025-122" },
+ new { Id = 13, BirthDate = new DateOnly(1975, 7, 9), FullName = "Andrei Kotov", LicenseNumber = "2025-133" },
+ new { Id = 14, BirthDate = new DateOnly(1982, 3, 16), FullName = "Valentina Osipova", LicenseNumber = "2025-144" },
+ new { Id = 15, BirthDate = new DateOnly(1999, 11, 1), FullName = "Maxim Panin", LicenseNumber = "2025-155" });
+ });
+
+ 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 = 2500m, Transmission = "AT", Year = 2023 },
+ new { Id = 2, EngineVolume = 1.8, ModelId = 2, RentalPricePerHour = 1800m, Transmission = "AT", Year = 2022 },
+ new { Id = 3, EngineVolume = 1.4, ModelId = 3, RentalPricePerHour = 900m, Transmission = "AT", Year = 2024 },
+ new { Id = 4, EngineVolume = 2.5, ModelId = 4, RentalPricePerHour = 2200m, Transmission = "AT", Year = 2023 },
+ new { Id = 5, EngineVolume = 3.9, ModelId = 5, RentalPricePerHour = 15000m, Transmission = "AT", Year = 2021 },
+ new { Id = 6, EngineVolume = 4.0, ModelId = 6, RentalPricePerHour = 4000m, Transmission = "AT", Year = 2023 },
+ new { Id = 7, EngineVolume = 1.6, ModelId = 7, RentalPricePerHour = 800m, Transmission = "MT", Year = 2024 },
+ new { Id = 8, EngineVolume = 2.0, ModelId = 8, RentalPricePerHour = 2000m, Transmission = "AT", Year = 2024 },
+ new { Id = 9, EngineVolume = 2.2, ModelId = 9, RentalPricePerHour = 1600m, Transmission = "MT", Year = 2022 },
+ new { Id = 10, EngineVolume = 2.0, ModelId = 10, RentalPricePerHour = 1900m, Transmission = "CVT", Year = 2023 },
+ new { Id = 11, EngineVolume = 3.0, ModelId = 11, RentalPricePerHour = 7000m, Transmission = "AT", Year = 2024 },
+ new { Id = 12, EngineVolume = 2.0, ModelId = 12, RentalPricePerHour = 3500m, Transmission = "AT", Year = 2023 },
+ new { Id = 13, EngineVolume = 6.2, ModelId = 13, RentalPricePerHour = 5500m, Transmission = "AT", Year = 2022 },
+ new { Id = 14, EngineVolume = 1.5, ModelId = 14, RentalPricePerHour = 1400m, Transmission = "AT", Year = 2024 },
+ new { Id = 15, EngineVolume = 1.7, ModelId = 15, RentalPricePerHour = 950m, 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 = 4, ClientId = 1, RentalDate = new DateTime(2025, 3, 4, 10, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 2, CarId = 4, ClientId = 3, RentalDate = new DateTime(2025, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 3, CarId = 4, ClientId = 5, RentalDate = new DateTime(2025, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), RentalHours = 24 },
+ new { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2025, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2025, 3, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 120 },
+ new { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2025, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2025, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2025, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 36 },
+ new { Id = 9, CarId = 5, ClientId = 9, RentalDate = new DateTime(2025, 3, 3, 12, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 10, CarId = 6, ClientId = 10, RentalDate = new DateTime(2025, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 168 },
+ new { Id = 11, CarId = 7, ClientId = 11, RentalDate = new DateTime(2025, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2025, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2025, 2, 28, 22, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 60 },
+ new { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2025, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), RentalHours = 96 },
+ new { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2025, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), RentalHours = 120 },
+ new { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2025, 2, 28, 14, 0, 0, 0, DateTimeKind.Unspecified), RentalHours = 48 },
+ new { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2025, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), RentalHours = 72 },
+ new { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2025, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), RentalHours = 36 },
+ new { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2025, 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..39a7cc4dd
--- /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 seed) : 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().HasKey(m => m.Id);
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasKey(mg => mg.Id);
+ e.Property(mg => mg.RentalPricePerHour).HasColumnType("decimal(18,2)");
+ e.HasOne(mg => mg.Model)
+ .WithMany()
+ .HasForeignKey(mg => mg.ModelId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasKey(c => c.Id);
+ e.HasOne(c => c.ModelGeneration)
+ .WithMany()
+ .HasForeignKey(c => c.ModelGenerationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity().HasKey(c => c.Id);
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasKey(r => r.Id);
+ e.HasOne(r => r.Car)
+ .WithMany()
+ .HasForeignKey(r => r.CarId)
+ .OnDelete(DeleteBehavior.Cascade);
+ e.HasOne(r => r.Client)
+ .WithMany()
+ .HasForeignKey(r => r.ClientId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Seed data
+ modelBuilder.Entity().HasData(seed.CarModels);
+ modelBuilder.Entity().HasData(seed.ModelGenerations);
+ modelBuilder.Entity().HasData(seed.Cars);
+ modelBuilder.Entity().HasData(seed.Clients);
+ modelBuilder.Entity().HasData(seed.Rentals);
+ }
+}
diff --git a/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs
new file mode 100644
index 000000000..046194d8d
--- /dev/null
+++ b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs
@@ -0,0 +1,61 @@
+using CarRental.Domain.Interfaces;
+using CarRental.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace CarRental.Infrastructure.Repositories;
+
+///
+/// Обобщённая реализация репозитория поверх EF Core DbContext
+///
+public class DbRepository(AppDbContext context) : IRepository where T : class
+{
+ private readonly DbSet _dbSet = context.Set();
+
+ public async Task GetByIdAsync(int id) =>
+ await _dbSet.FindAsync(id);
+
+ public async Task> GetAllAsync() =>
+ await _dbSet.ToListAsync();
+
+ public async Task AddAsync(T entity)
+ {
+ await _dbSet.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task UpdateAsync(T entity)
+ {
+ _dbSet.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task DeleteAsync(int id)
+ {
+ var entity = await _dbSet.FindAsync(id);
+ if (entity is null) return false;
+
+ _dbSet.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+
+ public async Task GetByIdAsync(int id, Func, IQueryable>? include = null)
+ {
+ var query = include is null ? _dbSet.AsQueryable() : include(_dbSet.AsQueryable());
+ return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id);
+ }
+
+ public async Task> GetAllAsync(Func