From 5fe090e39983cdd69ebfb80e4c3c4874ed1939a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:29:57 +0400 Subject: [PATCH 01/10] Added domain classes --- .../CarRental.Domain/CarRental.Domain.csproj | 11 +++++ .../CarRental/CarRental.Domain/Models/Car.cs | 32 ++++++++++++++ .../CarRental.Domain/Models/CarModel.cs | 37 ++++++++++++++++ .../CarRental.Domain/Models/Client.cs | 27 ++++++++++++ .../Models/ModelGeneration.cs | 42 ++++++++++++++++++ .../CarRental.Domain/Models/Rental.cs | 43 +++++++++++++++++++ CarRental/CarRental/CarRental.sln | 25 +++++++++++ 7 files changed, 217 insertions(+) create mode 100644 CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj create mode 100644 CarRental/CarRental/CarRental.Domain/Models/Car.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Models/CarModel.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Models/Client.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Models/ModelGeneration.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Models/Rental.cs create mode 100644 CarRental/CarRental/CarRental.sln diff --git a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj new file mode 100644 index 000000000..af1a7c1b1 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + 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.sln b/CarRental/CarRental/CarRental.sln new file mode 100644 index 000000000..2c051bbe7 --- /dev/null +++ b/CarRental/CarRental/CarRental.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Domain", "CarRental.Domain\CarRental.Domain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From 7d88be54501042585152fbf913df991cf6d5405f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:31:12 +0400 Subject: [PATCH 02/10] Added tests and data seeder --- .../CarRental.Domain/Data/CarRentalFixture.cs | 118 +++++++++++++++ .../CarRental.Tests/CarRental.Tests.csproj | 28 ++++ .../CarRental.Tests/CarRentalTests.cs | 136 ++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs create mode 100644 CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj create mode 100644 CarRental/CarRental/CarRental.Tests/CarRentalTests.cs diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs new file mode 100644 index 000000000..0d6bd410a --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs @@ -0,0 +1,118 @@ +using CarRental.Domain.Models; + +namespace CarRental.Domain.Data; + +/// +/// Тестовые данные для пункта проката автомобилей +/// +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, Model = CarModels[0] }, + new() { Id = 2, Year = 2022, EngineVolume = 1.8, Transmission = "AT", RentalPricePerHour = 1800, ModelId = 2, Model = CarModels[1] }, + new() { Id = 3, Year = 2024, EngineVolume = 1.4, Transmission = "AT", RentalPricePerHour = 900, ModelId = 3, Model = CarModels[2] }, + new() { Id = 4, Year = 2023, EngineVolume = 2.5, Transmission = "AT", RentalPricePerHour = 2200, ModelId = 4, Model = CarModels[3] }, + new() { Id = 5, Year = 2021, EngineVolume = 3.9, Transmission = "AT", RentalPricePerHour = 15000, ModelId = 5, Model = CarModels[4] }, + new() { Id = 6, Year = 2023, EngineVolume = 4.0, Transmission = "AT", RentalPricePerHour = 4000, ModelId = 6, Model = CarModels[5] }, + new() { Id = 7, Year = 2024, EngineVolume = 1.6, Transmission = "MT", RentalPricePerHour = 800, ModelId = 7, Model = CarModels[6] }, + new() { Id = 8, Year = 2024, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2000, ModelId = 8, Model = CarModels[7] }, + new() { Id = 9, Year = 2022, EngineVolume = 2.2, Transmission = "MT", RentalPricePerHour = 1600, ModelId = 9, Model = CarModels[8] }, + new() { Id = 10, Year = 2023, EngineVolume = 2.0, Transmission = "CVT", RentalPricePerHour = 1900, ModelId = 10, Model = CarModels[9] }, + new() { Id = 11, Year = 2024, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 7000, ModelId = 11, Model = CarModels[10] }, + new() { Id = 12, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 3500, ModelId = 12, Model = CarModels[11] }, + new() { Id = 13, Year = 2022, EngineVolume = 6.2, Transmission = "AT", RentalPricePerHour = 5500, ModelId = 13, Model = CarModels[12] }, + new() { Id = 14, Year = 2024, EngineVolume = 1.5, Transmission = "AT", RentalPricePerHour = 1400, ModelId = 14, Model = CarModels[13] }, + new() { Id = 15, Year = 2023, EngineVolume = 1.7, Transmission = "MT", RentalPricePerHour = 950, ModelId = 15, Model = CarModels[14] }, + ]; + + Cars = + [ + new() { Id = 1, LicensePlate = "A001MB77", Color = "Black", ModelGenerationId = 1, ModelGeneration = ModelGenerations[0] }, + new() { Id = 2, LicensePlate = "B222NO77", Color = "White", ModelGenerationId = 2, ModelGeneration = ModelGenerations[1] }, + new() { Id = 3, LicensePlate = "C333RT99", Color = "Silver", ModelGenerationId = 3, ModelGeneration = ModelGenerations[2] }, + new() { Id = 4, LicensePlate = "E444UF77", Color = "Blue", ModelGenerationId = 4, ModelGeneration = ModelGenerations[3] }, + new() { Id = 5, LicensePlate = "K555FH77", Color = "Red", ModelGenerationId = 5, ModelGeneration = ModelGenerations[4] }, + new() { Id = 6, LicensePlate = "M666HC99", Color = "Gray", ModelGenerationId = 6, ModelGeneration = ModelGenerations[5] }, + new() { Id = 7, LicensePlate = "N777CH77", Color = "White", ModelGenerationId = 7, ModelGeneration = ModelGenerations[6] }, + new() { Id = 8, LicensePlate = "O888SH77", Color = "Brown", ModelGenerationId = 8, ModelGeneration = ModelGenerations[7] }, + new() { Id = 9, LicensePlate = "P999SH99", Color = "Yellow", ModelGenerationId = 9, ModelGeneration = ModelGenerations[8] }, + new() { Id = 10, LicensePlate = "R100SE77", Color = "Black", ModelGenerationId = 10, ModelGeneration = ModelGenerations[9] }, + new() { Id = 11, LicensePlate = "S200EY77", Color = "Green", ModelGenerationId = 11, ModelGeneration = ModelGenerations[10] }, + new() { Id = 12, LicensePlate = "T300YA99", Color = "White", ModelGenerationId = 12, ModelGeneration = ModelGenerations[11] }, + new() { Id = 13, LicensePlate = "U400AB77", Color = "Black", ModelGenerationId = 13, ModelGeneration = ModelGenerations[12] }, + new() { Id = 14, LicensePlate = "H500BV99", Color = "Gray", ModelGenerationId = 14, ModelGeneration = ModelGenerations[13] }, + new() { Id = 15, LicensePlate = "SH600VG77", Color = "Beige", ModelGenerationId = 15, ModelGeneration = ModelGenerations[14] }, + ]; + + 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) }, + ]; + + // Аренды: Toyota RAV4 (Car Id=4) арендуется 3 раза — лидер по числу аренд + Rentals = + [ + new() { Id = 1, CarId = 4, ClientId = 1, RentalDate = new DateTime(2025, 3, 4, 10, 0, 0), RentalHours = 48, Car = Cars[3], Client = Clients[0] }, + new() { Id = 2, CarId = 4, ClientId = 3, RentalDate = new DateTime(2025, 2, 25, 14, 30, 0), RentalHours = 72, Car = Cars[3], Client = Clients[2] }, + new() { Id = 3, CarId = 4, ClientId = 5, RentalDate = new DateTime(2025, 2, 20, 9, 15, 0), RentalHours = 24, Car = Cars[3], Client = Clients[4] }, + new() { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2025, 2, 27, 11, 45, 0), RentalHours = 96, Car = Cars[0], Client = Clients[1] }, + new() { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2025, 3, 1, 16, 0, 0), RentalHours = 120, Car = Cars[0], Client = Clients[3] }, + new() { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2025, 2, 23, 13, 20, 0), RentalHours = 72, Car = Cars[1], Client = Clients[5] }, + new() { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2025, 2, 18, 10, 10, 0), RentalHours = 48, Car = Cars[1], Client = Clients[7] }, + new() { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2025, 2, 28, 8, 30, 0), RentalHours = 36, Car = Cars[2], Client = Clients[6] }, + new() { Id = 9, CarId = 5, ClientId = 9, RentalDate = new DateTime(2025, 3, 3, 12, 0, 0), RentalHours = 96, Car = Cars[4], Client = Clients[8] }, + new() { Id = 10, CarId = 6, ClientId = 10, RentalDate = new DateTime(2025, 2, 28, 7, 0, 0), RentalHours = 168, Car = Cars[5], Client = Clients[9] }, + new() { Id = 11, CarId = 7, ClientId = 11, RentalDate = new DateTime(2025, 2, 22, 15, 45, 0), RentalHours = 72, Car = Cars[6], Client = Clients[10] }, + new() { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2025, 2, 26, 9, 20, 0), RentalHours = 48, Car = Cars[7], Client = Clients[11] }, + new() { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2025, 2, 28, 22, 0, 0), RentalHours = 60, Car = Cars[8], Client = Clients[12] }, + new() { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2025, 2, 24, 11, 30, 0), RentalHours = 96, Car = Cars[9], Client = Clients[13] }, + new() { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2025, 2, 10, 14, 15, 0), RentalHours = 120, Car = Cars[10], Client = Clients[14] }, + new() { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2025, 2, 28, 14, 0, 0), RentalHours = 48, Car = Cars[11], Client = Clients[0] }, + new() { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2025, 2, 5, 16, 45, 0), RentalHours = 72, Car = Cars[12], Client = Clients[1] }, + new() { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2025, 2, 12, 10, 10, 0), RentalHours = 36, Car = Cars[13], Client = Clients[2] }, + new() { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2025, 2, 16, 13, 30, 0), RentalHours = 84, Car = Cars[14], Client = Clients[3] }, + ]; + } +} diff --git a/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj new file mode 100644 index 000000000..90445bb0d --- /dev/null +++ b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs new file mode 100644 index 000000000..e7db20341 --- /dev/null +++ b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs @@ -0,0 +1,136 @@ +using CarRental.Domain.Data; + +namespace CarRental.Tests; + +/// +/// Юнит-тесты для пункта проката автомобилей. +/// Петров Григорий Алексеевич, группа 6413-100503D +/// +public class CarRentalTests(CarRentalFixture fixture) : IClassFixture +{ + /// + /// ТЕСТ 1: Вывести информацию обо всех клиентах, + /// которые брали в аренду автомобили указанной модели, упорядочить по ФИО. + /// Ожидаем трёх клиентов, арендовавших Toyota RAV4. + /// + [Fact] + public void GetClientsByModelSortedByName() + { + const string targetModel = "Toyota RAV4"; + const int expectedCount = 3; + const string expectedFirstName = "Alexei Nikitin"; + const string expectedSecondName = "Sergei Volkov"; + const string expectedThirdName = "Vasily Nekrasov"; + + var result = fixture.Rentals + .Where(r => r.Car.ModelGeneration.Model.Name == targetModel) + .Select(r => r.Client) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + + Assert.Equal(expectedCount, result.Count); + Assert.Equal(expectedFirstName, result[0].FullName); + Assert.Equal(expectedSecondName, result[1].FullName); + Assert.Equal(expectedThirdName, result[2].FullName); + } + + /// + /// ТЕСТ 2: Вывести информацию об автомобилях, находящихся в аренде + /// на указанный момент времени. + /// + [Fact] + public void GetCurrentlyRentedCars() + { + var checkDate = new DateTime(2025, 3, 5, 12, 0, 0); + // Toyota RAV4 (Id=4, "E444UF77") взята 2025-03-04 10:00 на 48ч → возврат 2025-03-06 10:00 + const string expectedActivePlate = "E444UF77"; + + var activeCars = fixture.Rentals + .Where(r => r.RentalDate.AddHours(r.RentalHours) > checkDate) + .Select(r => r.Car) + .Distinct() + .ToList(); + + Assert.Contains(activeCars, c => c.LicensePlate == expectedActivePlate); + } + + /// + /// ТЕСТ 3: Вывести топ 5 наиболее часто арендуемых автомобилей. + /// Toyota RAV4 (Id=4) лидирует с 3 арендами. + /// + [Fact] + public void GetTop5MostRentedCars() + { + const int expectedTopCount = 5; + const string expectedTopPlate = "E444UF77"; // Toyota RAV4 + const int expectedTopRentalCount = 3; + + var topCars = fixture.Rentals + .GroupBy(r => r.Car) + .Select(g => new { Car = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .ToList(); + + Assert.Equal(expectedTopCount, topCars.Count); + Assert.Equal(expectedTopPlate, topCars[0].Car.LicensePlate); + Assert.Equal(expectedTopRentalCount, topCars[0].Count); + } + + /// + /// ТЕСТ 4: Для каждого автомобиля вывести число аренд. + /// Всего 15 ТС; Toyota RAV4 (Car.Id=4) — 3 аренды, Mercedes (Car.Id=1) — 2 аренды. + /// + [Fact] + public void GetRentalCountPerCar() + { + const int expectedTotalVehicles = 15; + const int toyotaCarId = 4; + const int mercedesCarId = 1; + const int expectedToyotaRentals = 3; + const int expectedMercedesRentals = 2; + + var stats = fixture.Cars + .Select(car => new + { + Car = car, + RentalCount = fixture.Rentals.Count(r => r.CarId == car.Id) + }) + .ToList(); + + Assert.Equal(expectedTotalVehicles, stats.Count); + + var toyotaStat = stats.Single(s => s.Car.Id == toyotaCarId); + var mercedesStat = stats.Single(s => s.Car.Id == mercedesCarId); + + Assert.Equal(expectedToyotaRentals, toyotaStat.RentalCount); + Assert.Equal(expectedMercedesRentals, mercedesStat.RentalCount); + Assert.True(stats.All(s => s.RentalCount >= 0)); + } + + /// + /// ТЕСТ 5: Вывести топ 5 клиентов по суммарной стоимости аренды. + /// Лидер — Konstantin Zhukov (аренда Ferrari 488: 96ч × 15 000₽ = 1 440 000₽). + /// + [Fact] + public void GetTop5ClientsByRentalAmount() + { + const int expectedCount = 5; + const string expectedTopClientName = "Konstantin Zhukov"; + + var topClients = fixture.Rentals + .GroupBy(r => r.Client) + .Select(g => new + { + Client = g.Key, + TotalAmount = g.Sum(r => r.RentalHours * r.Car.ModelGeneration.RentalPricePerHour) + }) + .OrderByDescending(x => x.TotalAmount) + .Take(5) + .ToList(); + + Assert.Equal(expectedCount, topClients.Count); + Assert.Equal(expectedTopClientName, topClients[0].Client.FullName); + } +} From 295cb0a496894ea535cc0a032bdb7a760ddc8236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:31:39 +0400 Subject: [PATCH 03/10] Added github action --- .../.github/workflows/dotnet-tests.yml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CarRental/CarRental/.github/workflows/dotnet-tests.yml 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 From fdd92bdd690f84a970f8b9ae9c104b9eefe32627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:35:24 +0400 Subject: [PATCH 04/10] Refactored domain add repositories and db context --- .../CarRental.Domain/CarRental.Domain.csproj | 2 - .../CarRental.Domain/Data/CarRentalFixture.cs | 134 +++---- .../CarRental.Domain/Entities/Car.cs | 42 +++ .../CarRental.Domain/Entities/CarModel.cs | 51 +++ .../CarRental.Domain/Entities/Client.cs | 37 ++ .../Entities/ModelGeneration.cs | 53 +++ .../CarRental.Domain/Entities/Rental.cs | 51 +++ .../Interfaces/IRepository.cs | 17 + .../CarRental.Infrastructure.csproj | 13 + .../20260222120000_InitialCreate.Designer.cs | 336 ++++++++++++++++++ .../20260222120000_InitialCreate.cs | 265 ++++++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 127 +++++++ .../Persistence/AppDbContext.cs | 61 ++++ .../Repositories/DbRepository.cs | 61 ++++ .../CarRental.ServiceDefaults.csproj | 18 + .../CarRental.ServiceDefaults/Extensions.cs | 78 ++++ 16 files changed, 1277 insertions(+), 69 deletions(-) create mode 100644 CarRental/CarRental/CarRental.Domain/Entities/Car.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Entities/Client.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Entities/Rental.cs create mode 100644 CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs create mode 100644 CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj create mode 100644 CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.Designer.cs create mode 100644 CarRental/CarRental/CarRental.Infrastructure/Migrations/20260222120000_InitialCreate.cs create mode 100644 CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs create mode 100644 CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs create mode 100644 CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs create mode 100644 CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj create mode 100644 CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs diff --git a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj index af1a7c1b1..5900beb48 100644 --- a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj +++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj @@ -1,5 +1,4 @@ - net8.0 enable @@ -7,5 +6,4 @@ true $(NoWarn);1591 - diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs index 0d6bd410a..0bd57bbf4 100644 --- a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs +++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs @@ -1,9 +1,10 @@ -using CarRental.Domain.Models; +using CarRental.Domain.Entities; namespace CarRental.Domain.Data; /// -/// Тестовые данные для пункта проката автомобилей +/// Тестовые данные для пункта проката автомобилей. +/// Используется для первоначального наполнения БД через EF Core HasData. /// public class CarRentalFixture { @@ -36,83 +37,82 @@ public CarRentalFixture() ModelGenerations = [ - new() { Id = 1, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2500, ModelId = 1, Model = CarModels[0] }, - new() { Id = 2, Year = 2022, EngineVolume = 1.8, Transmission = "AT", RentalPricePerHour = 1800, ModelId = 2, Model = CarModels[1] }, - new() { Id = 3, Year = 2024, EngineVolume = 1.4, Transmission = "AT", RentalPricePerHour = 900, ModelId = 3, Model = CarModels[2] }, - new() { Id = 4, Year = 2023, EngineVolume = 2.5, Transmission = "AT", RentalPricePerHour = 2200, ModelId = 4, Model = CarModels[3] }, - new() { Id = 5, Year = 2021, EngineVolume = 3.9, Transmission = "AT", RentalPricePerHour = 15000, ModelId = 5, Model = CarModels[4] }, - new() { Id = 6, Year = 2023, EngineVolume = 4.0, Transmission = "AT", RentalPricePerHour = 4000, ModelId = 6, Model = CarModels[5] }, - new() { Id = 7, Year = 2024, EngineVolume = 1.6, Transmission = "MT", RentalPricePerHour = 800, ModelId = 7, Model = CarModels[6] }, - new() { Id = 8, Year = 2024, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2000, ModelId = 8, Model = CarModels[7] }, - new() { Id = 9, Year = 2022, EngineVolume = 2.2, Transmission = "MT", RentalPricePerHour = 1600, ModelId = 9, Model = CarModels[8] }, - new() { Id = 10, Year = 2023, EngineVolume = 2.0, Transmission = "CVT", RentalPricePerHour = 1900, ModelId = 10, Model = CarModels[9] }, - new() { Id = 11, Year = 2024, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 7000, ModelId = 11, Model = CarModels[10] }, - new() { Id = 12, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 3500, ModelId = 12, Model = CarModels[11] }, - new() { Id = 13, Year = 2022, EngineVolume = 6.2, Transmission = "AT", RentalPricePerHour = 5500, ModelId = 13, Model = CarModels[12] }, - new() { Id = 14, Year = 2024, EngineVolume = 1.5, Transmission = "AT", RentalPricePerHour = 1400, ModelId = 14, Model = CarModels[13] }, - new() { Id = 15, Year = 2023, EngineVolume = 1.7, Transmission = "MT", RentalPricePerHour = 950, ModelId = 15, Model = CarModels[14] }, + 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, ModelGeneration = ModelGenerations[0] }, - new() { Id = 2, LicensePlate = "B222NO77", Color = "White", ModelGenerationId = 2, ModelGeneration = ModelGenerations[1] }, - new() { Id = 3, LicensePlate = "C333RT99", Color = "Silver", ModelGenerationId = 3, ModelGeneration = ModelGenerations[2] }, - new() { Id = 4, LicensePlate = "E444UF77", Color = "Blue", ModelGenerationId = 4, ModelGeneration = ModelGenerations[3] }, - new() { Id = 5, LicensePlate = "K555FH77", Color = "Red", ModelGenerationId = 5, ModelGeneration = ModelGenerations[4] }, - new() { Id = 6, LicensePlate = "M666HC99", Color = "Gray", ModelGenerationId = 6, ModelGeneration = ModelGenerations[5] }, - new() { Id = 7, LicensePlate = "N777CH77", Color = "White", ModelGenerationId = 7, ModelGeneration = ModelGenerations[6] }, - new() { Id = 8, LicensePlate = "O888SH77", Color = "Brown", ModelGenerationId = 8, ModelGeneration = ModelGenerations[7] }, - new() { Id = 9, LicensePlate = "P999SH99", Color = "Yellow", ModelGenerationId = 9, ModelGeneration = ModelGenerations[8] }, - new() { Id = 10, LicensePlate = "R100SE77", Color = "Black", ModelGenerationId = 10, ModelGeneration = ModelGenerations[9] }, - new() { Id = 11, LicensePlate = "S200EY77", Color = "Green", ModelGenerationId = 11, ModelGeneration = ModelGenerations[10] }, - new() { Id = 12, LicensePlate = "T300YA99", Color = "White", ModelGenerationId = 12, ModelGeneration = ModelGenerations[11] }, - new() { Id = 13, LicensePlate = "U400AB77", Color = "Black", ModelGenerationId = 13, ModelGeneration = ModelGenerations[12] }, - new() { Id = 14, LicensePlate = "H500BV99", Color = "Gray", ModelGenerationId = 14, ModelGeneration = ModelGenerations[13] }, - new() { Id = 15, LicensePlate = "SH600VG77", Color = "Beige", ModelGenerationId = 15, ModelGeneration = ModelGenerations[14] }, + 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) }, + 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) }, ]; - // Аренды: Toyota RAV4 (Car Id=4) арендуется 3 раза — лидер по числу аренд Rentals = [ - new() { Id = 1, CarId = 4, ClientId = 1, RentalDate = new DateTime(2025, 3, 4, 10, 0, 0), RentalHours = 48, Car = Cars[3], Client = Clients[0] }, - new() { Id = 2, CarId = 4, ClientId = 3, RentalDate = new DateTime(2025, 2, 25, 14, 30, 0), RentalHours = 72, Car = Cars[3], Client = Clients[2] }, - new() { Id = 3, CarId = 4, ClientId = 5, RentalDate = new DateTime(2025, 2, 20, 9, 15, 0), RentalHours = 24, Car = Cars[3], Client = Clients[4] }, - new() { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2025, 2, 27, 11, 45, 0), RentalHours = 96, Car = Cars[0], Client = Clients[1] }, - new() { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2025, 3, 1, 16, 0, 0), RentalHours = 120, Car = Cars[0], Client = Clients[3] }, - new() { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2025, 2, 23, 13, 20, 0), RentalHours = 72, Car = Cars[1], Client = Clients[5] }, - new() { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2025, 2, 18, 10, 10, 0), RentalHours = 48, Car = Cars[1], Client = Clients[7] }, - new() { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2025, 2, 28, 8, 30, 0), RentalHours = 36, Car = Cars[2], Client = Clients[6] }, - new() { Id = 9, CarId = 5, ClientId = 9, RentalDate = new DateTime(2025, 3, 3, 12, 0, 0), RentalHours = 96, Car = Cars[4], Client = Clients[8] }, - new() { Id = 10, CarId = 6, ClientId = 10, RentalDate = new DateTime(2025, 2, 28, 7, 0, 0), RentalHours = 168, Car = Cars[5], Client = Clients[9] }, - new() { Id = 11, CarId = 7, ClientId = 11, RentalDate = new DateTime(2025, 2, 22, 15, 45, 0), RentalHours = 72, Car = Cars[6], Client = Clients[10] }, - new() { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2025, 2, 26, 9, 20, 0), RentalHours = 48, Car = Cars[7], Client = Clients[11] }, - new() { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2025, 2, 28, 22, 0, 0), RentalHours = 60, Car = Cars[8], Client = Clients[12] }, - new() { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2025, 2, 24, 11, 30, 0), RentalHours = 96, Car = Cars[9], Client = Clients[13] }, - new() { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2025, 2, 10, 14, 15, 0), RentalHours = 120, Car = Cars[10], Client = Clients[14] }, - new() { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2025, 2, 28, 14, 0, 0), RentalHours = 48, Car = Cars[11], Client = Clients[0] }, - new() { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2025, 2, 5, 16, 45, 0), RentalHours = 72, Car = Cars[12], Client = Clients[1] }, - new() { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2025, 2, 12, 10, 10, 0), RentalHours = 36, Car = Cars[13], Client = Clients[2] }, - new() { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2025, 2, 16, 13, 30, 0), RentalHours = 84, Car = Cars[14], Client = Clients[3] }, + 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 }, ]; } } 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.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..8aff07351 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,127 @@ +// +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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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..1a7c58137 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs @@ -0,0 +1,61 @@ +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.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, IQueryable>? include = null) + { + var query = include is null ? _dbSet.AsQueryable() : include(_dbSet.AsQueryable()); + return await query.ToListAsync(); + } + + public IQueryable GetQueryable(Func, IQueryable>? include = null) + { + var query = _dbSet.AsQueryable(); + return include is null ? query : include(query); + } +} diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj new file mode 100644 index 000000000..bac631056 --- /dev/null +++ b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..d115e949e --- /dev/null +++ b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation()) + .WithTracing(tracing => tracing + .AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation()); + + builder.AddOpenTelemetryExporters(); + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"])) + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + return app; + } +} From d3c4889c9af1e9f1ae88f92fabf0ccbb8863ecf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:37:34 +0400 Subject: [PATCH 05/10] Added dtos, mapper profile and services --- .../CarRental.Application.Contracts.csproj | 15 ++++++++++ .../Dto/AnalyticsDto.cs | 15 ++++++++++ .../Dto/CarEditDto.cs | 13 ++++++++ .../Dto/CarGetDto.cs | 17 +++++++++++ .../Dto/CarModelEditDto.cs | 17 +++++++++++ .../Dto/CarModelGetDto.cs | 19 ++++++++++++ .../Dto/ClientAnalyticsDto.cs | 8 +++++ .../Dto/ClientEditDto.cs | 13 ++++++++ .../Dto/ClientGetDto.cs | 15 ++++++++++ .../Dto/ModelGenerationEditDto.cs | 17 +++++++++++ .../Dto/ModelGenerationGetDto.cs | 21 +++++++++++++ .../Dto/RentalEditDto.cs | 16 ++++++++++ .../Dto/RentalGetDto.cs | 21 +++++++++++++ .../MappingProfile.cs | 30 +++++++++++++++++++ 14 files changed, 237 insertions(+) create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs create mode 100644 CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs 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/ClientAnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs new file mode 100644 index 000000000..a58c98cff --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs @@ -0,0 +1,8 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для отображения сумм арендной платы клиентов +/// +/// Информация о клиенте +/// Общая сумма арендной платы для данного клиента +public record ClientRentalAmountDto(ClientGetDto Client, decimal TotalAmount); 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(); + } +} From e0942b8bbf50f2eea4002a03c6d55ff3676a6967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:39:25 +0400 Subject: [PATCH 06/10] Added controllers, app host configuration and migrations --- .../CarRental.API/CarRental.API.csproj | 23 +++ .../Controllers/AnalyticsController.cs | 164 ++++++++++++++++++ .../Controllers/CarModelsController.cs | 70 ++++++++ .../Controllers/CarsController.cs | 75 ++++++++ .../Controllers/ClientsController.cs | 70 ++++++++ .../Controllers/ModelGenerationsController.cs | 71 ++++++++ .../Controllers/RentalsController.cs | 90 ++++++++++ CarRental/CarRental/CarRental.API/Program.cs | 56 ++++++ .../Properties/launchSettings.json | 41 +++++ .../appsettings.Development.json | 8 + .../CarRental/CarRental.API/appsettings.json | 9 + .../CarRental.AppHost.csproj | 18 ++ .../CarRental/CarRental.AppHost/Program.cs | 10 ++ .../Properties/launchSettings.json | 17 ++ .../appsettings.Development.json | 19 ++ .../CarRental.AppHost/appsettings.json | 12 ++ 16 files changed, 753 insertions(+) create mode 100644 CarRental/CarRental/CarRental.API/CarRental.API.csproj create mode 100644 CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Controllers/CarsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs create mode 100644 CarRental/CarRental/CarRental.API/Program.cs create mode 100644 CarRental/CarRental/CarRental.API/Properties/launchSettings.json create mode 100644 CarRental/CarRental/CarRental.API/appsettings.Development.json create mode 100644 CarRental/CarRental/CarRental.API/appsettings.json create mode 100644 CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj create mode 100644 CarRental/CarRental/CarRental.AppHost/Program.cs create mode 100644 CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json create mode 100644 CarRental/CarRental/CarRental.AppHost/appsettings.Development.json create mode 100644 CarRental/CarRental/CarRental.AppHost/appsettings.json diff --git a/CarRental/CarRental/CarRental.API/CarRental.API.csproj b/CarRental/CarRental/CarRental.API/CarRental.API.csproj new file mode 100644 index 000000000..03204aef6 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/CarRental.API.csproj @@ -0,0 +1,23 @@ + + + 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..5757b8ec4 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Program.cs @@ -0,0 +1,56 @@ +using CarRental.Application.Contracts; +using CarRental.Domain.Data; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure.Persistence; +using CarRental.Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddSingleton(); + +builder.Services.AddControllers().AddJsonOptions(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>(); + +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..01314ef24 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + true + 92a4f1d3-7b5e-4c8f-a2d1-e3f456789012 + + + + + + + + + diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs new file mode 100644 index 000000000..d41b552fb --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/Program.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var sqlServer = builder.AddSqlServer("carrental-sql") + .AddDatabase("CarRentalDb"); + +builder.AddProject("carrental-api") + .WithReference(sqlServer, "DefaultConnection") + .WaitFor(sqlServer); + +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..2a499575f --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "profiles": { + "CarRental.AppHost": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:15058", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19141", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20144", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json new file mode 100644 index 000000000..b23b67ac9 --- /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" + } + } + } +} diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.json b/CarRental/CarRental/CarRental.AppHost/appsettings.json new file mode 100644 index 000000000..64f2d8a47 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "" + } +} From a184df87dceba8865455adecab14bb6a7434ae78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:43:27 +0400 Subject: [PATCH 07/10] Added rabbitmq consumer --- .../CarRental.API/CarRental.API.csproj | 2 + CarRental/CarRental/CarRental.API/Program.cs | 5 + .../CarRental.Infrastructure.Messaging.csproj | 16 ++ .../RentalQueueConsumer.cs | 191 ++++++++++++++++++ CarRental/CarRental/CarRental.sln | 62 +++++- 5 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 CarRental/CarRental/CarRental.Infrastructure.Messaging/CarRental.Infrastructure.Messaging.csproj create mode 100644 CarRental/CarRental/CarRental.Infrastructure.Messaging/RentalQueueConsumer.cs diff --git a/CarRental/CarRental/CarRental.API/CarRental.API.csproj b/CarRental/CarRental/CarRental.API/CarRental.API.csproj index 03204aef6..cf7a6f20b 100644 --- a/CarRental/CarRental/CarRental.API/CarRental.API.csproj +++ b/CarRental/CarRental/CarRental.API/CarRental.API.csproj @@ -8,6 +8,7 @@ + all @@ -18,6 +19,7 @@ + diff --git a/CarRental/CarRental/CarRental.API/Program.cs b/CarRental/CarRental/CarRental.API/Program.cs index 5757b8ec4..3d253b1c3 100644 --- a/CarRental/CarRental/CarRental.API/Program.cs +++ b/CarRental/CarRental/CarRental.API/Program.cs @@ -2,6 +2,7 @@ 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; @@ -39,6 +40,10 @@ 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()) 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.sln b/CarRental/CarRental/CarRental.sln index 2c051bbe7..dbb47cba2 100644 --- a/CarRental/CarRental/CarRental.sln +++ b/CarRental/CarRental/CarRental.sln @@ -3,9 +3,23 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Domain", "CarRental.Domain\CarRental.Domain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Domain", "CarRental.Domain\CarRental.Domain.csproj", "{A1111111-1111-1111-1111-111111111111}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Infrastructure", "CarRental.Infrastructure\CarRental.Infrastructure.csproj", "{A2222222-2222-2222-2222-222222222222}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Infrastructure.Messaging", "CarRental.Infrastructure.Messaging\CarRental.Infrastructure.Messaging.csproj", "{A3333333-3333-3333-3333-333333333333}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Application.Contracts", "CarRental.Application.Contracts\CarRental.Application.Contracts.csproj", "{A4444444-4444-4444-4444-444444444444}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.API", "CarRental.API\CarRental.API.csproj", "{A5555555-5555-5555-5555-555555555555}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Generator.Host", "CarRental.Generator.Host\CarRental.Generator.Host.csproj", "{A6666666-6666-6666-6666-666666666666}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.AppHost", "CarRental.AppHost\CarRental.AppHost.csproj", "{A7777777-7777-7777-7777-777777777777}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.ServiceDefaults", "CarRental.ServiceDefaults\CarRental.ServiceDefaults.csproj", "{A8888888-8888-8888-8888-888888888888}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{A9999999-9999-9999-9999-999999999999}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,13 +27,41 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {A1111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU + {A2222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU + {A3333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU + {A4444444-4444-4444-4444-444444444444}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4444444-4444-4444-4444-444444444444}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4444444-4444-4444-4444-444444444444}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4444444-4444-4444-4444-444444444444}.Release|Any CPU.Build.0 = Release|Any CPU + {A5555555-5555-5555-5555-555555555555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5555555-5555-5555-5555-555555555555}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5555555-5555-5555-5555-555555555555}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5555555-5555-5555-5555-555555555555}.Release|Any CPU.Build.0 = Release|Any CPU + {A6666666-6666-6666-6666-666666666666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6666666-6666-6666-6666-666666666666}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6666666-6666-6666-6666-666666666666}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6666666-6666-6666-6666-666666666666}.Release|Any CPU.Build.0 = Release|Any CPU + {A7777777-7777-7777-7777-777777777777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7777777-7777-7777-7777-777777777777}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7777777-7777-7777-7777-777777777777}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7777777-7777-7777-7777-777777777777}.Release|Any CPU.Build.0 = Release|Any CPU + {A8888888-8888-8888-8888-888888888888}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8888888-8888-8888-8888-888888888888}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8888888-8888-8888-8888-888888888888}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8888888-8888-8888-8888-888888888888}.Release|Any CPU.Build.0 = Release|Any CPU + {A9999999-9999-9999-9999-999999999999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9999999-9999-9999-9999-999999999999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9999999-9999-9999-9999-999999999999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9999999-9999-9999-9999-999999999999}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 92f7715bf33cb7ca41f6d7f4c7bfc9159869e09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:45:13 +0400 Subject: [PATCH 08/10] Added generator, worker and producer --- .../CarRental.AppHost.csproj | 8 +- .../CarRental/CarRental.AppHost/Program.cs | 15 +++- .../Properties/launchSettings.json | 4 +- .../appsettings.Development.json | 2 +- .../CarRental.AppHost/appsettings.json | 5 +- .../CarRental.Generator.Host.csproj | 18 +++++ .../Controllers/GeneratorController.cs | 73 +++++++++++++++++++ .../Generator/RentalGenerator.cs | 24 ++++++ .../Messaging/RentalPublisher.cs | 73 +++++++++++++++++++ .../CarRental.Generator.Host/Program.cs | 40 ++++++++++ .../Properties/launchSettings.json | 14 ++++ .../appsettings.Development.json | 8 ++ .../CarRental.Generator.Host/appsettings.json | 12 +++ 13 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 CarRental/CarRental/CarRental.Generator.Host/CarRental.Generator.Host.csproj create mode 100644 CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs create mode 100644 CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs create mode 100644 CarRental/CarRental/CarRental.Generator.Host/Messaging/RentalPublisher.cs create mode 100644 CarRental/CarRental/CarRental.Generator.Host/Program.cs create mode 100644 CarRental/CarRental/CarRental.Generator.Host/Properties/launchSettings.json create mode 100644 CarRental/CarRental/CarRental.Generator.Host/appsettings.Development.json create mode 100644 CarRental/CarRental/CarRental.Generator.Host/appsettings.json diff --git a/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj index 01314ef24..abdc12351 100644 --- a/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj +++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj @@ -6,13 +6,15 @@ enable enable true - 92a4f1d3-7b5e-4c8f-a2d1-e3f456789012 + a7b3c9d1-2e4f-5a6b-8c0d-e1f234567890 - - + + + + diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs index d41b552fb..30286a153 100644 --- a/CarRental/CarRental/CarRental.AppHost/Program.cs +++ b/CarRental/CarRental/CarRental.AppHost/Program.cs @@ -3,8 +3,21 @@ var sqlServer = builder.AddSqlServer("carrental-sql") .AddDatabase("CarRentalDb"); +// RabbitMQ с панелью управления (Management UI на порту 15672) +var rabbitMq = builder.AddRabbitMQ("carrental-rabbitmq") + .WithManagementPlugin(); + builder.AddProject("carrental-api") .WithReference(sqlServer, "DefaultConnection") - .WaitFor(sqlServer); + .WithReference(rabbitMq) + .WithEnvironment("RabbitMQ__ExchangeName", "rental-exchange") + .WithEnvironment("RabbitMQ__QueueName", "rental-queue") + .WaitFor(sqlServer) + .WaitFor(rabbitMq); + +builder.AddProject("carrental-generator") + .WithReference(rabbitMq) + .WithEnvironment("RabbitMQ__ExchangeName", "rental-exchange") + .WaitFor(rabbitMq); builder.Build().Run(); diff --git a/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json index 2a499575f..a4ef35c1d 100644 --- a/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json +++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json @@ -3,7 +3,7 @@ "CarRental.AppHost": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "http://localhost:15058", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", @@ -14,4 +14,4 @@ } } } -} +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json index b23b67ac9..b09d7ac12 100644 --- a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json +++ b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json @@ -16,4 +16,4 @@ } } } -} +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.json b/CarRental/CarRental/CarRental.AppHost/appsettings.json index 64f2d8a47..d87c105e5 100644 --- a/CarRental/CarRental/CarRental.AppHost/appsettings.json +++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json @@ -8,5 +8,8 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "" + }, + "Kafka": { + "RentalTopicName": "rentals" } -} +} \ No newline at end of file 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..7ef16a439 --- /dev/null +++ b/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs @@ -0,0 +1,73 @@ +using CarRental.Application.Contracts.Dto; +using CarRental.Generator.Host.Generator; +using CarRental.Generator.Host.Messaging; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Generator.Host.Controllers; + +/// +/// Контроллер генератора договоров аренды. +/// Генерирует тестовые данные и публикует их в RabbitMQ. +/// +/// Публикатор сообщений RabbitMQ +/// Логгер +[ApiController] +[Route("api/[controller]")] +public class GeneratorController( + RentalPublisher publisher, + ILogger logger) : ControllerBase +{ + /// + /// Сгенерировать договоры и отправить их в RabbitMQ пакетами + /// + /// Общее количество генерируемых 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 items = RentalGenerator.Generate(totalCount); + + 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..5a4c5e38a --- /dev/null +++ b/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs @@ -0,0 +1,24 @@ +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) => + 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.Random.Int(1, 15), + ClientId: f.Random.Int(1, 15))) + .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..1fad063b4 --- /dev/null +++ b/CarRental/CarRental/CarRental.Generator.Host/Program.cs @@ -0,0 +1,40 @@ +using CarRental.Generator.Host.Messaging; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Регистрация RabbitMQ-соединения через Aspire +builder.AddRabbitMQClient("carrental-rabbitmq"); + +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name?.StartsWith("CarRental") == 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" + } +} From 135a246ac187b28e614f75fa776c30dfafe4e824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 04:46:29 +0400 Subject: [PATCH 09/10] Changed README.md --- README.md | 210 +++++++++++++++++++----------------------------------- 1 file changed, 73 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 76afcbfdd..212454320 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,73 @@ -# Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) - -## Задание -### Цель -Реализация проекта сервисно-ориентированного приложения. - -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. - -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. - -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма - -image1 - -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) -[Вопросы к экзамену](https://docs.google.com/document/d/1bjfvtzjyMljJbcu8YCvC8DzDegDUAmDeNtBz9M6FQes/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve -6. Прийти на занятие и защитить работу - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл - -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas). +# Car Rental Service + +Сервис для автоматизации пункта проката автомобилей + +## Стек технологий + +* Platform: .NET 8 (C# 12) +* Database: SQL Server +* ORM: Entity Framework Core 8 +* Messaging: RabbitMQ (RabbitMQ.Client) +* Mapping: AutoMapper +* Testing: xUnit, Bogus (Fake Data) +* Orchestration: .NET Aspire + +## Структура проекта + +### 1. CarRental.Domain + +Доменная модель. Содержит доменные сущности: + +* `CarModel` — модель автомобиля (справочник): тип привода, класс, тип кузова, количество мест +* `ModelGeneration` — поколение модели: год выпуска, объём двигателя, коробка передач, стоимость часа аренды +* `Car` — физический экземпляр автомобиля: госномер, цвет, поколение модели +* `Client` — клиент: номер водительского удостоверения, ФИО, дата рождения +* `Rental` — договор аренды: клиент, автомобиль, время выдачи, длительность в часах + +### 2. CarRental.Application.Contracts + +Контракты бизнес-логики приложения. + +* `Contracts/Dto` — DTO (records) для CRUD-операций и аналитики +* `MappingProfile` — профили AutoMapper для маппинга сущностей в DTO и обратно + +### 3. CarRental.Infrastructure + +Реализация работы с внешними системами. + +* `Persistence/AppDbContext` — конфигурация DbContext и таблиц +* `Migrations` — миграции базы данных EF Core +* `Repositories/DbRepository` — реализация универсального репозитория + +### 4. CarRental.Infrastructure.Messaging + +Реализация работы с брокером сообщений. + +* `RentalQueueConsumer` — фоновый сервис (`BackgroundService`), который подписывается на очередь RabbitMQ, валидирует входящие пакеты договоров аренды и сохраняет их в базу данных + +### 5. CarRental.API + +Точка входа Web API. + +* `Controllers` — REST API контроллеры для всех сущностей (`CarModelsController`, `CarsController`, `ClientsController`, `ModelGenerationsController`, `RentalsController`, `AnalyticsController`) +* `Program.cs` — конфигурация приложения, регистрация зависимостей + +### 6. CarRental.AppHost + +Оркестратор .NET Aspire. + +* Управляет запуском SQL Server, RabbitMQ, CarRental.API и CarRental.Generator.Host +* Автоматически настраивает строки подключения и переменные окружения + +### 7. CarRental.ServiceDefaults + +Общие настройки сервисов (health checks, OpenTelemetry, service discovery). + +### 8. CarRental.Generator.Host + +Сервис генерации нагрузки. + +* `Generator/RentalGenerator` — генерирует фейковые DTO договоров аренды через Bogus +* `Messaging/RentalPublisher` — публикует пакеты в обменник RabbitMQ +* `Controllers/GeneratorController` — REST-контроллер для запуска генерации: `POST /api/Generator/rentals?listSize=100&batchSize=10&delayMs=500` +* Flow: генерирует пачки аренд -> публикует в RabbitMQ -> CarRental.API считывает и сохраняет в БД From dc3c41de1d6b85d6a7b147aa5848c44b349f069b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=9F?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Mon, 23 Feb 2026 15:53:04 +0400 Subject: [PATCH 10/10] resolved EF Core startup errors, added HasData nav props --- .../CarRental/CarRental.AppHost/Program.cs | 18 +- .../Dto/ClientAnalyticsDto.cs | 8 - .../CarRental.Domain/Data/CarRentalFixture.cs | 20 + .../Controllers/GeneratorController.cs | 26 +- .../Generator/RentalGenerator.cs | 10 +- .../CarRental.Generator.Host/Program.cs | 6 + .../Migrations/AppDbContextModelSnapshot.cs | 374 ++++++++++++++---- .../Persistence/AppDbContext.cs | 1 + .../CarRental.Tests/CarRentalTests.cs | 11 +- 9 files changed, 367 insertions(+), 107 deletions(-) delete mode 100644 CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs index 30286a153..170fdd4b2 100644 --- a/CarRental/CarRental/CarRental.AppHost/Program.cs +++ b/CarRental/CarRental/CarRental.AppHost/Program.cs @@ -7,17 +7,19 @@ var rabbitMq = builder.AddRabbitMQ("carrental-rabbitmq") .WithManagementPlugin(); -builder.AddProject("carrental-api") - .WithReference(sqlServer, "DefaultConnection") - .WithReference(rabbitMq) - .WithEnvironment("RabbitMQ__ExchangeName", "rental-exchange") - .WithEnvironment("RabbitMQ__QueueName", "rental-queue") - .WaitFor(sqlServer) - .WaitFor(rabbitMq); +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(rabbitMq) + .WaitFor(api); builder.Build().Run(); diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs deleted file mode 100644 index a58c98cff..000000000 --- a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CarRental.Application.Contracts.Dto; - -/// -/// DTO для отображения сумм арендной платы клиентов -/// -/// Информация о клиенте -/// Общая сумма арендной платы для данного клиента -public record ClientRentalAmountDto(ClientGetDto Client, decimal TotalAmount); diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs index 0bd57bbf4..1f073049f 100644 --- a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs +++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs @@ -114,5 +114,25 @@ public CarRentalFixture() 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.Generator.Host/Controllers/GeneratorController.cs b/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs index 7ef16a439..386c6a25a 100644 --- a/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs +++ b/CarRental/CarRental/CarRental.Generator.Host/Controllers/GeneratorController.cs @@ -2,6 +2,7 @@ using CarRental.Generator.Host.Generator; using CarRental.Generator.Host.Messaging; using Microsoft.AspNetCore.Mvc; +using System.Net.Http.Json; namespace CarRental.Generator.Host.Controllers; @@ -10,15 +11,18 @@ namespace CarRental.Generator.Host.Controllers; /// Генерирует тестовые данные и публикует их в RabbitMQ. /// /// Публикатор сообщений RabbitMQ +/// Фабрика HTTP-клиентов для запросов к API /// Логгер [ApiController] [Route("api/[controller]")] public class GeneratorController( RentalPublisher publisher, + IHttpClientFactory httpClientFactory, ILogger logger) : ControllerBase { /// - /// Сгенерировать договоры и отправить их в RabbitMQ пакетами + /// Сгенерировать договоры и отправить их в RabbitMQ пакетами. + /// Идентификаторы автомобилей и клиентов берутся из базы данных через API. /// /// Общее количество генерируемых DTO /// Размер одного пакета @@ -45,7 +49,25 @@ public async Task>> GenerateRentals( try { - var items = RentalGenerator.Generate(totalCount); + 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)) { diff --git a/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs b/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs index 5a4c5e38a..f87e37804 100644 --- a/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs +++ b/CarRental/CarRental/CarRental.Generator.Host/Generator/RentalGenerator.cs @@ -9,16 +9,18 @@ namespace CarRental.Generator.Host.Generator; public static class RentalGenerator { /// - /// Сгенерировать список DTO договоров аренды + /// Сгенерировать список DTO договоров аренды на основе реальных идентификаторов /// /// Количество записей + /// Список допустимых идентификаторов автомобилей из базы данных + /// Список допустимых идентификаторов клиентов из базы данных /// Список сгенерированных DTO - public static IList Generate(int count) => + 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.Random.Int(1, 15), - ClientId: f.Random.Int(1, 15))) + CarId: f.PickRandom(carIds), + ClientId: f.PickRandom(clientIds))) .Generate(count); } diff --git a/CarRental/CarRental/CarRental.Generator.Host/Program.cs b/CarRental/CarRental/CarRental.Generator.Host/Program.cs index 1fad063b4..3dbf48d7b 100644 --- a/CarRental/CarRental/CarRental.Generator.Host/Program.cs +++ b/CarRental/CarRental/CarRental.Generator.Host/Program.cs @@ -9,6 +9,12 @@ 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 => diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 8aff07351..72391c8cd 100644 --- a/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -23,104 +23,310 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.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.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.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.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.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"); - }); + { + 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"); - }); + { + 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"); - }); + { + 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 index 1a7c58137..39a7cc4dd 100644 --- a/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs +++ b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs @@ -21,6 +21,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) 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) diff --git a/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs index e7db20341..553419122 100644 --- a/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs +++ b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs @@ -1,4 +1,5 @@ using CarRental.Domain.Data; +using Xunit; namespace CarRental.Tests; @@ -6,8 +7,16 @@ namespace CarRental.Tests; /// Юнит-тесты для пункта проката автомобилей. /// Петров Григорий Алексеевич, группа 6413-100503D /// -public class CarRentalTests(CarRentalFixture fixture) : IClassFixture +public class CarRentalTests : IClassFixture { + private readonly CarRentalFixture fixture; + + public CarRentalTests(CarRentalFixture fixture) + { + this.fixture = fixture; + fixture.WireNavigations(); + } + /// /// ТЕСТ 1: Вывести информацию обо всех клиентах, /// которые брали в аренду автомобили указанной модели, упорядочить по ФИО.