From 23d5d07a06c8bc6c29a1ddeafea5acf8751b74d3 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 21 Nov 2025 03:49:09 +0400 Subject: [PATCH 01/37] added a client class with basic fields and configured the structure of the test project --- CarRental.Domain/CarRental.Domain.csproj | 9 +++++ CarRental.Domain/DataModels/Client.cs | 37 ++++++++++++++++++ CarRental.Tests/CarRental.Tests.csproj | 25 ++++++++++++ CarRental.Tests/DomainTests.cs | 10 +++++ CarRental.sln | 48 ++++++++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 CarRental.Domain/CarRental.Domain.csproj create mode 100644 CarRental.Domain/DataModels/Client.cs create mode 100644 CarRental.Tests/CarRental.Tests.csproj create mode 100644 CarRental.Tests/DomainTests.cs create mode 100644 CarRental.sln diff --git a/CarRental.Domain/CarRental.Domain.csproj b/CarRental.Domain/CarRental.Domain.csproj new file mode 100644 index 000000000..125f4c93b --- /dev/null +++ b/CarRental.Domain/CarRental.Domain.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs new file mode 100644 index 000000000..1f788bc6e --- /dev/null +++ b/CarRental.Domain/DataModels/Client.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.DataModels; + +[Table("client")] +public class Client { + + [Key] + [Required(ErrorMessage = "Client's ID is required")] + [Column("id")] + public required uint Id { get; set; } + + [Required(ErrorMessage = "Client's driver license ID is required")] + [StringLength(10, ErrorMessage = "The driver license ID's length should not exceed 50 characters")] + [Column("driver_license")] + public required string DriverLicenseId { get; set; } + + [Required(ErrorMessage = "Client's last name is required")] + [StringLength(50, ErrorMessage = "The last name's length should not exceed 50 characters")] + [Column("last_name")] + public required string LastName { get; set; } + + [Required(ErrorMessage = "Client's first name is required")] + [StringLength(50, ErrorMessage = "The first name's length should not exceed 50 characters")] + [Column("first_name")] + public required string FirstName { get; set; } + + [Required(ErrorMessage = "Client's patronymic is required")] + [StringLength(50, ErrorMessage = "The patronymic's length should not exceed 50 characters")] + [Column("patronymic")] + public required string Patronymic { get; set; } + + [Required(ErrorMessage = "Client's date of birth is required")] + [Column("birth_date")] + public required DateOnly? BirthDate { get; set; } +} \ No newline at end of file diff --git a/CarRental.Tests/CarRental.Tests.csproj b/CarRental.Tests/CarRental.Tests.csproj new file mode 100644 index 000000000..f3152e060 --- /dev/null +++ b/CarRental.Tests/CarRental.Tests.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/CarRental.Tests/DomainTests.cs b/CarRental.Tests/DomainTests.cs new file mode 100644 index 000000000..8f4980f1e --- /dev/null +++ b/CarRental.Tests/DomainTests.cs @@ -0,0 +1,10 @@ +namespace CarRental.Tests; + +public class DomainTests +{ + [Fact] + public void BasicTest_ShouldPass() + { + Assert.Equal(1, 1); + } +} diff --git a/CarRental.sln b/CarRental.sln new file mode 100644 index 000000000..342845641 --- /dev/null +++ b/CarRental.sln @@ -0,0 +1,48 @@ + +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", "{06B41FC1-BC46-473B-8E7E-394749D2A734}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{B253FF47-F3FD-4F60-934B-0A2649ACA810}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|x64.ActiveCfg = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|x64.Build.0 = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|x86.ActiveCfg = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Debug|x86.Build.0 = Debug|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|Any CPU.Build.0 = Release|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|x64.ActiveCfg = Release|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|x64.Build.0 = Release|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|x86.ActiveCfg = Release|Any CPU + {06B41FC1-BC46-473B-8E7E-394749D2A734}.Release|x86.Build.0 = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|x64.ActiveCfg = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|x64.Build.0 = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|x86.ActiveCfg = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Debug|x86.Build.0 = Debug|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|Any CPU.Build.0 = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x64.ActiveCfg = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x64.Build.0 = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.ActiveCfg = Release|Any CPU + {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal From 63be3a90be38df09fac3fd10e6452ce15c4a2a18 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 24 Nov 2025 01:28:02 +0400 Subject: [PATCH 02/37] added an autopark class with basic fields --- CarRental.Domain/DataModels/Autopark.cs | 25 +++++++++++++++++ .../InternalData/ComponentClasses/CarModel.cs | 21 ++++++++++++++ .../ComponentClasses/CarModelGeneration.cs | 17 +++++++++++ .../InternalData/ComponentEnums/BodyType.cs | 28 +++++++++++++++++++ .../InternalData/ComponentEnums/ClassType.cs | 11 ++++++++ .../InternalData/ComponentEnums/DriveType.cs | 8 ++++++ .../ComponentEnums/TransmissionType.cs | 9 ++++++ 7 files changed, 119 insertions(+) create mode 100644 CarRental.Domain/DataModels/Autopark.cs create mode 100644 CarRental.Domain/InternalData/ComponentClasses/CarModel.cs create mode 100644 CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs create mode 100644 CarRental.Domain/InternalData/ComponentEnums/BodyType.cs create mode 100644 CarRental.Domain/InternalData/ComponentEnums/ClassType.cs create mode 100644 CarRental.Domain/InternalData/ComponentEnums/DriveType.cs create mode 100644 CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs diff --git a/CarRental.Domain/DataModels/Autopark.cs b/CarRental.Domain/DataModels/Autopark.cs new file mode 100644 index 000000000..5af2c123c --- /dev/null +++ b/CarRental.Domain/DataModels/Autopark.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using Carrental.Domain.InternalData.ComponentClasses; + +public class Autopark +{ + [Key] + [Required(ErrorMessage = "Car's ID is required")] + [Column("id")] + public required int Id { get; set; } + + [Required] + [Column("model_generation")] + public required CarModelGeneration ModelGeneration { get; set; } + + [Required(ErrorMessage = "Car's number plate is required")] + [Column("number_plate")] + public required string NumberPlate { get; set; } + + [Required(ErrorMessage = "Car's colour is required")] + [Column("colour")] + public required string Colour { get; set; } + +} \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs new file mode 100644 index 000000000..89e4c9cb4 --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -0,0 +1,21 @@ +using CarRental.Domain.InternalData.ComponentEnums; + + +namespace CarRental.Domain.InternalData.ComponentClasses; + + +public class CarModel +{ + public required int Id { get; set; } + + public string Name { get; set; } + + public required DriveType DriveType { get; set; } + + public required uint SeatsNumber { get; set; } + + public required BodyType BodyType { get; set; } + + public required ClassType ClassType { get; set; } +} + diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs new file mode 100644 index 000000000..62bc35d1e --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -0,0 +1,17 @@ +using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Domain.InternalData.ComponentEnums; + +namespace CarRental.Domain.IntenralData.ComponentClasses; + +public class CarModelGeneration +{ + public required int Id { get; set; } + + public required int Year { get; set; } + + public required TransmissionType TransmissionType { get; set; } + + public required CarModel Model { get; set; } + + public required float HourCost { get; set; } +} diff --git a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs new file mode 100644 index 000000000..445d4ff91 --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs @@ -0,0 +1,28 @@ +namespace CarRental.Domain.InternalData.ComponentEnums; + +public enum BodyType +{ + Hatchback, + MicroCompactCar, + StationWagon, + Landau, + PickupTruck, + Van, + Minivan, + SportsCar, + Phaeton, + StationWagon, + Sedan, + Cabriolet, + OffRoadCar, + Convertible, + Roadster, + TwoDoorSedan, + Limousine, + SportUtilityVehicle, + Coupe, + Pickup, + FourDoorCoupe, + Crossover, + FourDoorSedan +} \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs new file mode 100644 index 000000000..6c4687cca --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs @@ -0,0 +1,11 @@ +namespace CarRental.Domain.InternalData.ComponentEnums; + +public enum ClassType +{ + A, + B, + C, + D, + E, + F +} \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs new file mode 100644 index 000000000..ac386e01f --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs @@ -0,0 +1,8 @@ +namespace CarRental.Domain.InternalData.ComponentEnums; + +public enum DriveType +{ + FrontWheel, + RearWheel, + AllWheel +} \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs new file mode 100644 index 000000000..e45d80856 --- /dev/null +++ b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs @@ -0,0 +1,9 @@ +namespace CarRental.Domain.InternalData.ComponentEnums; + +public enum TransmissionType +{ + Manual, + Automatic, + Robotic, + Variable +} \ No newline at end of file From fc0420ed23f16bf5fb066cf6cc831cfb950e2530 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 27 Nov 2025 04:30:19 +0400 Subject: [PATCH 03/37] added a rent class with basic fields --- CarRental.Domain/DataModels/Autopark.cs | 25 ------------------------- CarRental.Domain/DataModels/Car.cs | 18 ++++++++++++++++++ CarRental.Domain/DataModels/Client.cs | 18 ------------------ CarRental.Domain/DataModels/Rent.cs | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 CarRental.Domain/DataModels/Autopark.cs create mode 100644 CarRental.Domain/DataModels/Car.cs create mode 100644 CarRental.Domain/DataModels/Rent.cs diff --git a/CarRental.Domain/DataModels/Autopark.cs b/CarRental.Domain/DataModels/Autopark.cs deleted file mode 100644 index 5af2c123c..000000000 --- a/CarRental.Domain/DataModels/Autopark.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -using Carrental.Domain.InternalData.ComponentClasses; - -public class Autopark -{ - [Key] - [Required(ErrorMessage = "Car's ID is required")] - [Column("id")] - public required int Id { get; set; } - - [Required] - [Column("model_generation")] - public required CarModelGeneration ModelGeneration { get; set; } - - [Required(ErrorMessage = "Car's number plate is required")] - [Column("number_plate")] - public required string NumberPlate { get; set; } - - [Required(ErrorMessage = "Car's colour is required")] - [Column("colour")] - public required string Colour { get; set; } - -} \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs new file mode 100644 index 000000000..57688451c --- /dev/null +++ b/CarRental.Domain/DataModels/Car.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Domain.DataModels; + +public class Car { + + public required int Id { get; set; } + + public required CarModelGeneration ModelGeneration { get; set; } + + public required string NumberPlate { get; set; } + + public required string Colour { get; set; } + +} \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index 1f788bc6e..f99fb651c 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -3,35 +3,17 @@ namespace CarRental.Domain.DataModels; -[Table("client")] public class Client { - [Key] - [Required(ErrorMessage = "Client's ID is required")] - [Column("id")] public required uint Id { get; set; } - [Required(ErrorMessage = "Client's driver license ID is required")] - [StringLength(10, ErrorMessage = "The driver license ID's length should not exceed 50 characters")] - [Column("driver_license")] public required string DriverLicenseId { get; set; } - [Required(ErrorMessage = "Client's last name is required")] - [StringLength(50, ErrorMessage = "The last name's length should not exceed 50 characters")] - [Column("last_name")] public required string LastName { get; set; } - [Required(ErrorMessage = "Client's first name is required")] - [StringLength(50, ErrorMessage = "The first name's length should not exceed 50 characters")] - [Column("first_name")] public required string FirstName { get; set; } - [Required(ErrorMessage = "Client's patronymic is required")] - [StringLength(50, ErrorMessage = "The patronymic's length should not exceed 50 characters")] - [Column("patronymic")] public required string Patronymic { get; set; } - [Required(ErrorMessage = "Client's date of birth is required")] - [Column("birth_date")] public required DateOnly? BirthDate { get; set; } } \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs new file mode 100644 index 000000000..3754955f3 --- /dev/null +++ b/CarRental.Domain/DataModels/Rent.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.DataModels; + +public class Rent { + public int Id { get; set; } + + public required DateTime StartDateTime { get; set; } + + public required int Duration { get; set; } + + public required Autopark Car { get; set; } + + public required Client Client { get; set; } +} From 24f6b523a44c9d942881c0b74cf167d26ef08948 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 28 Nov 2025 02:39:01 +0400 Subject: [PATCH 04/37] added summary comments to domain classes and internal components --- CarRental.Domain/DataModels/Car.cs | 17 ++- CarRental.Domain/DataModels/Client.cs | 44 ++++++-- CarRental.Domain/DataModels/Rent.cs | 20 +++- .../InternalData/ComponentClasses/CarModel.cs | 29 ++++- .../ComponentClasses/CarModelGeneration.cs | 26 ++++- .../InternalData/ComponentEnums/BodyType.cs | 103 ++++++++++++++---- .../InternalData/ComponentEnums/ClassType.cs | 41 +++++-- .../InternalData/ComponentEnums/DriveType.cs | 23 +++- .../ComponentEnums/TransmissionType.cs | 21 +++- 9 files changed, 268 insertions(+), 56 deletions(-) diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs index 57688451c..51aad4ba3 100644 --- a/CarRental.Domain/DataModels/Car.cs +++ b/CarRental.Domain/DataModels/Car.cs @@ -5,14 +5,27 @@ namespace CarRental.Domain.DataModels; +/// +/// Represents a specific physical vehicle available for rental +/// public class Car { - + /// + /// Unique identifier of the car + /// public required int Id { get; set; } + /// + /// The model generation this car belongs to, defining its year, transmission type, and base rental cost + /// public required CarModelGeneration ModelGeneration { get; set; } + /// + /// License plate number of the car + /// public required string NumberPlate { get; set; } + /// + /// Exterior colour of the car + /// public required string Colour { get; set; } - } \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index f99fb651c..27dfe09a1 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -3,17 +3,37 @@ namespace CarRental.Domain.DataModels; +/// +/// Represents a client (rental customer) with personal and identification information +/// public class Client { - - public required uint Id { get; set; } - - public required string DriverLicenseId { get; set; } - - public required string LastName { get; set; } - - public required string FirstName { get; set; } - - public required string Patronymic { get; set; } - - public required DateOnly? BirthDate { get; set; } + /// + /// Unique identifier of the client + /// + public required uint Id { get; set; } + + /// + /// Unique identifier of the client's driver's license + /// + public required string DriverLicenseId { get; set; } + + /// + /// Client's last name (surname) + /// + public required string LastName { get; set; } + + /// + /// Client's first name (given name) + /// + public required string FirstName { get; set; } + + /// + /// Client's patronymic (middle name), if applicable + /// + public string? Patronymic { get; set; } + + /// + /// Client's date of birth + /// + public DateOnly? BirthDate { get; set; } } \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs index 3754955f3..dacd64bfb 100644 --- a/CarRental.Domain/DataModels/Rent.cs +++ b/CarRental.Domain/DataModels/Rent.cs @@ -3,14 +3,32 @@ namespace CarRental.Domain.DataModels; +/// +/// Represents a car rental agreement between a client and the rental company +/// public class Rent { + /// + /// Unique identifier of the rental record + /// public int Id { get; set; } + /// + /// Date and time when the rental period starts + /// public required DateTime StartDateTime { get; set; } + /// + /// Duration of the rental in hours + /// public required int Duration { get; set; } - public required Autopark Car { get; set; } + /// + /// The car that is being rented + /// + public required Car Car { get; set; } + /// + /// The client who is renting the car + /// public required Client Client { get; set; } } diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index 89e4c9cb4..171e26d6e 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -3,19 +3,40 @@ namespace CarRental.Domain.InternalData.ComponentClasses; - +/// +/// Represents a specific car model with its key characteristics +/// such as name, body type, drive type, seating capacity, and vehicle class +/// public class CarModel { + /// + /// Unique identifier of the car model + /// public required int Id { get; set; } + /// + /// Name of the car model (e.g., "Camry", "Golf", "Model 3") + /// public string Name { get; set; } - public required DriveType DriveType { get; set; } + /// + /// Type of drive system used by the car model (front-wheel, rear-wheel or all-wheel drive) + /// + public DriveType? DriveType { get; set; } + /// + /// Number of passenger seats in the vehicle + /// public required uint SeatsNumber { get; set; } - + + /// + /// Body style of the car model (e.g., sedan, SUV, hatchback) + /// public required BodyType BodyType { get; set; } - public required ClassType ClassType { get; set; } + /// + /// Vehicle classification by size and market segment (A, B, C, D, E or F) + /// + public ClassType? ClassType { get; set; } } diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs index 62bc35d1e..221bb6323 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -3,15 +3,37 @@ namespace CarRental.Domain.IntenralData.ComponentClasses; +/// +/// Represents a specific generation of a car model, +/// including its production year, transmission type, +/// and rental cost per hour +/// public class CarModelGeneration { + /// + /// Unique identifier of the car model generation + /// public required int Id { get; set; } + /// + /// Calendar year when this generation of the car model was produced + /// public required int Year { get; set; } - public required TransmissionType TransmissionType { get; set; } + /// + /// Type of transmission used in this car model generation (manual, automatic, robotic or variable) + /// + public TransmissionType? TransmissionType { get; set; } - public required CarModel Model { get; set; } + /// + /// The car model to which this generation belongs (a class that describes + /// the main technical characteristics, such as the model name, + /// drive type, transmission type, body type, and vehicle class) + /// + public CarModel? Model { get; set; } + /// + /// Rental cost per hour for vehicles of this model generation + /// public required float HourCost { get; set; } } diff --git a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs index 445d4ff91..cbc6db945 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs @@ -1,28 +1,91 @@ namespace CarRental.Domain.InternalData.ComponentEnums; -public enum BodyType -{ +/// +/// Type of vehicle body style +/// +public enum BodyType { + /// + /// Ultra-small city car designed for maximum fuel efficiency and maneuverability + /// + CityCar, + + /// + /// Stylish car with a sloping roofline and typically two doors + /// + Coupe, + + /// + /// SUV-like vehicle built on a car platform, offering elevated ride height and versatility + /// + Crossover, + + /// + /// Car with a retractable soft or hard roof, offering open-air driving + /// + Cabriolet, + + /// + /// Traditional sedan with four side doors and a separate trunk compartment + /// + FourDoorSedan, + + /// + /// Sedan styled with a coupe-like roofline but four functional doors + /// + FourDoorCoupe, + + /// + /// Compact car with a rear door that opens upward, including the rear window + /// Hatchback, - MicroCompactCar, - StationWagon, - Landau, - PickupTruck, - Van, + + /// + /// Extended luxury sedan with extra interior space, often chauffeur-driven + /// + Limousine, + + /// + /// Family-oriented van with car-like handling and flexible interior seating + /// Minivan, - SportsCar, - Phaeton, - StationWagon, - Sedan, - Cabriolet, + + /// + /// Rugged vehicle engineered for driving on unpaved and challenging terrain + /// OffRoadCar, - Convertible, + + /// + /// Light-duty truck with an open cargo bed separate from the passenger cabin + /// + PickupTruck, + + /// + /// Lightweight two-seater sports car with a focus on driving dynamics + /// Roadster, - TwoDoorSedan, - Limousine, + + /// + /// Standard passenger car with a three-box configuration: engine, cabin, and trunk + /// + Sedan, + + /// + /// High-riding, versatile vehicle combining passenger and cargo capacity with off-road capability + /// SportUtilityVehicle, - Coupe, - Pickup, - FourDoorCoupe, - Crossover, - FourDoorSedan + + /// + /// Low-slung, high-performance vehicle built for speed and agile handling + /// + SportsCar, + + /// + /// Passenger car with an extended roofline and large cargo area at the rear + /// + StationWagon, + + /// + /// Box-shaped vehicle designed primarily for transporting goods or multiple passengers + /// + Van } \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs index 6c4687cca..229bae8ab 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs @@ -1,11 +1,36 @@ namespace CarRental.Domain.InternalData.ComponentEnums; -public enum ClassType -{ - A, - B, - C, - D, - E, - F +/// +/// Vehicle classification based on size and segment +/// +public enum ClassType { + /// + /// Mini cars, the smallest urban vehicle class + /// + A, + + /// + /// Small cars, compact and economical for city driving + /// + B, + + /// + /// Medium cars, offering balanced space and comfort + /// + C, + + /// + /// Large family cars, with enhanced interior room and features + /// + D, + + /// + /// Executive cars, premium mid-size to large sedans + /// + E, + + /// + /// Luxury cars, high-end flagship vehicles with top-tier features + /// + F } \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs index ac386e01f..0aea4abd7 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs @@ -1,8 +1,21 @@ namespace CarRental.Domain.InternalData.ComponentEnums; -public enum DriveType -{ - FrontWheel, - RearWheel, - AllWheel +/// +/// The type of vehicle drive system +/// +public enum DriveType { + /// + /// Front-wheel drive, where power is delivered to the front wheels + /// + FrontWheel, + + /// + /// Rear-wheel drive, where power is delivered to the rear wheels + /// + RearWheel, + + /// + /// All-wheel drive, where power is distributed to all wheels + /// + AllWheel } \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs index e45d80856..78a7a1f6e 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs @@ -1,9 +1,26 @@ namespace CarRental.Domain.InternalData.ComponentEnums; -public enum TransmissionType -{ +/// +/// The type of vehicle transmission +/// +public enum TransmissionType { + /// + /// Manual gearbox with driver-operated gear shifting + /// Manual, + + /// + /// Automatic gearbox requiring no driver input for shifting + /// Automatic, + + /// + /// Robotic gearbox with automated clutch control + /// Robotic, + + /// + /// Continuously variable transmission (CVT) with stepless gear ratio adjustment + /// Variable } \ No newline at end of file From 3f9d2f4e8ba5fa9405d4995fd37ee2ae55933723 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 28 Nov 2025 04:00:54 +0400 Subject: [PATCH 05/37] added a data seed for completing unit tests and made ome minor changes --- CarRental.Domain/DataModels/Car.cs | 3 - CarRental.Domain/DataModels/Client.cs | 3 - CarRental.Domain/DataModels/Rent.cs | 3 - CarRental.Domain/DataSeed/TestData.cs | 170 ++++++++++++++++++ .../InternalData/ComponentClasses/CarModel.cs | 1 - .../ComponentClasses/CarModelGeneration.cs | 2 +- 6 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 CarRental.Domain/DataSeed/TestData.cs diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs index 51aad4ba3..b023293ba 100644 --- a/CarRental.Domain/DataModels/Car.cs +++ b/CarRental.Domain/DataModels/Car.cs @@ -1,6 +1,3 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - using CarRental.Domain.InternalData.ComponentClasses; namespace CarRental.Domain.DataModels; diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index 27dfe09a1..ed70d0632 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -1,6 +1,3 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - namespace CarRental.Domain.DataModels; /// diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs index dacd64bfb..7d7a488aa 100644 --- a/CarRental.Domain/DataModels/Rent.cs +++ b/CarRental.Domain/DataModels/Rent.cs @@ -1,6 +1,3 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - namespace CarRental.Domain.DataModels; /// diff --git a/CarRental.Domain/DataSeed/TestData.cs b/CarRental.Domain/DataSeed/TestData.cs new file mode 100644 index 000000000..0880f2ad3 --- /dev/null +++ b/CarRental.Domain/DataSeed/TestData.cs @@ -0,0 +1,170 @@ +using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Domain.DataSeed; + +public class DataSeed { + public List Cars { get; } + + public List Clients { get; } + + public List Rents { get; } + + public List Models { get; } + + public List Generation { get; } + + public DataSeed() + { + Models = new List + { + new CarModel { Id = 1, Name = "Fiat 500", DriveType = DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, + new CarModel { Id = 2, Name = "Subaru Outback", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, + new CarModel { Id = 3, Name = "Volkswagen Golf", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 4, Name = "Mazda CX-5", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new CarModel { Id = 5, Name = "Nissan Qashqai", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, + new CarModel { Id = 6, Name = "Volvo XC90", DriveType = DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new CarModel { Id = 7, Name = "Audi A4", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 8, Name = "Honda CR-V", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, + new CarModel { Id = 9, Name = "Hyundai Tucson", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new CarModel { Id = 10, Name = "Volkswagen Transporter", DriveType = DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, + new CarModel { Id = 11, Name = "Mercedes E-Class", DriveType = DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, + new CarModel { Id = 12, Name = "Ford Focus", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 13, Name = "Jaguar F-Type", DriveType = DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, + new CarModel { Id = 14, Name = "Tesla Model 3", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 15, Name = "Toyota Camry", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 16, Name = "Lexus LS", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, + new CarModel { Id = 17, Name = "Porsche 911", DriveType = DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, + new CarModel { Id = 18, Name = "Renault Megane", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 19, Name = "BMW X5", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new CarModel { Id = 20, Name = "Kia Rio", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } + }; + + Generation = new List + { + new CarModelGeneration { Id = 1, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[16], HourCost = 160.0f }, // Porsche 911 + new CarModelGeneration { Id = 2, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[0], HourCost = 35.0f }, // Fiat 500 + new CarModelGeneration { Id = 3, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[11], HourCost = 55.0f }, // Ford Focus + new CarModelGeneration { Id = 4, Year = 2020, TransmissionType = TransmissionType.Variable, Model = Models[4], HourCost = 70.0f }, // Nissan Qashqai + new CarModelGeneration { Id = 5, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[18], HourCost = 120.0f }, // BMW X5 + new CarModelGeneration { Id = 6, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[15], HourCost = 140.0f }, // Lexus LS + new CarModelGeneration { Id = 7, Year = 2018, TransmissionType = TransmissionType.Manual, Model = Models[19], HourCost = 40.0f }, // Kia Rio + new CarModelGeneration { Id = 8, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[7], HourCost = 85.0f }, // Honda CR-V + new CarModelGeneration { Id = 9, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[12], HourCost = 150.0f }, // Jaguar F-Type + new CarModelGeneration { Id = 10, Year = 2020, TransmissionType = TransmissionType.Manual, Model = Models[9], HourCost = 60.0f }, // VW Transporter + new CarModelGeneration { Id = 11, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[1], HourCost = 95.0f }, // Subaru Outback + new CarModelGeneration { Id = 12, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[8], HourCost = 75.0f }, // Hyundai Tucson + new CarModelGeneration { Id = 13, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[2], HourCost = 50.0f }, // VW Golf + new CarModelGeneration { Id = 14, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[13], HourCost = 100.0f }, // Tesla Model 3 + new CarModelGeneration { Id = 15, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[14], HourCost = 80.0f }, // Toyota Camry + new CarModelGeneration { Id = 16, Year = 2020, TransmissionType = TransmissionType.Automatic, Model = Models[6], HourCost = 90.0f }, // Audi A4 + new CarModelGeneration { Id = 17, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[5], HourCost = 105.0f }, // Volvo XC90 + new CarModelGeneration { Id = 18, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[17], HourCost = 55.0f }, // Renault Megane + new CarModelGeneration { Id = 19, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[10], HourCost = 110.0f }, // Mercedes E-Class + new CarModelGeneration { Id = 20, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[3], HourCost = 80.0f } // Mazda CX-5 + }; + + Cars = new List + { + new Car { Id = 1, ModelGeneration = Generation[5], NumberPlate = "T890NO96", Colour = "Gray" }, + new Car { Id = 2, ModelGeneration = Generation[14], NumberPlate = "A123BC77", Colour = "Black" }, + new Car { Id = 3, ModelGeneration = Generation[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, + new Car { Id = 4, ModelGeneration = Generation[19], NumberPlate = "D012HI80", Colour = "Blue" }, + new Car { Id = 5, ModelGeneration = Generation[6], NumberPlate = "E345JK81", Colour = "Red" }, + new Car { Id = 6, ModelGeneration = Generation[16], NumberPlate = "F678LM82", Colour = "Gray" }, + new Car { Id = 7, ModelGeneration = Generation[7], NumberPlate = "G901NO83", Colour = "Green" }, + new Car { Id = 8, ModelGeneration = Generation[13], NumberPlate = "H234PQ84", Colour = "Black" }, + new Car { Id = 9, ModelGeneration = Generation[3], NumberPlate = "I567RS85", Colour = "White" }, + new Car { Id = 10, ModelGeneration = Generation[18], NumberPlate = "J890TU86", Colour = "Silver" }, + new Car { Id = 11, ModelGeneration = Generation[10], NumberPlate = "K123VW87", Colour = "Blue" }, + new Car { Id = 12, ModelGeneration = Generation[11], NumberPlate = "L456XY88", Colour = "Red" }, + new Car { Id = 13, ModelGeneration = Generation[8], NumberPlate = "R234JK94", Colour = "Blue" }, + new Car { Id = 14, ModelGeneration = Generation[9], NumberPlate = "N012BC90", Colour = "White" }, + new Car { Id = 15, ModelGeneration = Generation[1], NumberPlate = "Q901HI93", Colour = "Red" }, + new Car { Id = 16, ModelGeneration = Generation[15], NumberPlate = "P678FG92", Colour = "Silver" }, + new Car { Id = 17, ModelGeneration = Generation[2], NumberPlate = "O345DE91", Colour = "Black" }, + new Car { Id = 18, ModelGeneration = Generation[17], NumberPlate = "S567LM95", Colour = "Green" }, + new Car { Id = 19, ModelGeneration = Generation[4], NumberPlate = "C789FG79", Colour = "Silver" }, + new Car { Id = 20, ModelGeneration = Generation[12], NumberPlate = "B456DE78", Colour = "White" } + }; + + Clients = new List + { + new Client { Id = 1, DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, + new Client { Id = 2, DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, + new Client { Id = 3, DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, + new Client { Id = 4, DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, + new Client { Id = 5, DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, + new Client { Id = 6, DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, + new Client { Id = 7, DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, + new Client { Id = 8, DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, + new Client { Id = 9, DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, + new Client { Id = 10, DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, + new Client { Id = 11, DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, + new Client { Id = 12, DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, + new Client { Id = 13, DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, + new Client { Id = 14, DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, + new Client { Id = 15, DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, + new Client { Id = 16, DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, + new Client { Id = 17, DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, + new Client { Id = 18, DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, + new Client { Id = 19, DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, + new Client { Id = 20, DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } + }; + + var baseTime = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + Rents = new List + { + new Rent { Id = 1, StartDateTime = baseTime.AddDays(-2), Duration = 6, Car = Cars[13], Client = Clients[14] }, + new Rent { Id = 2, StartDateTime = baseTime.AddDays(12), Duration = 6, Car = Cars[19], Client = Clients[19] }, + new Rent { Id = 3, StartDateTime = baseTime.AddDays(-25), Duration = 48, Car = Cars[2], Client = Clients[2] }, + new Rent { Id = 4, StartDateTime = baseTime.AddDays(8), Duration = 24, Car = Cars[17], Client = Clients[17] }, + new Rent { Id = 5, StartDateTime = baseTime.AddDays(-20), Duration = 72, Car = Cars[4], Client = Clients[4] }, + new Rent { Id = 6, StartDateTime = baseTime.AddDays(4), Duration = 72, Car = Cars[15], Client = Clients[15] }, + new Rent { Id = 7, StartDateTime = baseTime.AddDays(-15), Duration = 168, Car = Cars[6], Client = Clients[6] }, + new Rent { Id = 8, StartDateTime = baseTime.AddDays(-4), Duration = 48, Car = Cars[11], Client = Clients[11] }, + new Rent { Id = 9, StartDateTime = baseTime.AddDays(-10), Duration = 36, Car = Cars[8], Client = Clients[8] }, + new Rent { Id = 10, StartDateTime = baseTime, Duration = 24, Car = Cars[1], Client = Clients[0] }, + new Rent { Id = 11, StartDateTime = baseTime.AddDays(2), Duration = 8, Car = Cars[14], Client = Clients[13] }, + new Rent { Id = 12, StartDateTime = baseTime.AddDays(-8), Duration = 24, Car = Cars[9], Client = Clients[9] }, + new Rent { Id = 13, StartDateTime = baseTime.AddDays(6), Duration = 12, Car = Cars[16], Client = Clients[16] }, + new Rent { Id = 14, StartDateTime = baseTime.AddDays(-6), Duration = 12, Car = Cars[10], Client = Clients[10] }, + new Rent { Id = 15, StartDateTime = baseTime.AddDays(10), Duration = 48, Car = Cars[18], Client = Clients[18] }, + new Rent { Id = 16, StartDateTime = baseTime.AddDays(-28), Duration = 12, Car = Cars[12], Client = Clients[12] }, + new Rent { Id = 17, StartDateTime = baseTime.AddDays(-22), Duration = 6, Car = Cars[3], Client = Clients[3] }, + new Rent { Id = 18, StartDateTime = baseTime.AddDays(-18), Duration = 8, Car = Cars[5], Client = Clients[5] }, + new Rent { Id = 19, StartDateTime = baseTime.AddDays(-12), Duration = 4, Car = Cars[7], Client = Clients[7] }, + new Rent { Id = 20, StartDateTime = baseTime.AddDays(-30), Duration = 24, Car = Cars[0], Client = Clients[1] }, + new Rent { Id = 21, StartDateTime = baseTime.AddDays(-25), Duration = 12, Car = Cars[5], Client = Clients[0] }, + new Rent { Id = 22, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[0], Client = Clients[1] }, + new Rent { Id = 23, StartDateTime = baseTime.AddDays(-5), Duration = 8, Car = Cars[10], Client = Clients[1] }, + /// + new Rent { Id = 24, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[3], Client = Clients[2] }, + new Rent { Id = 25, StartDateTime = baseTime.AddDays(-15), Duration = 6, Car = Cars[7], Client = Clients[2] }, + new Rent { Id = 26, StartDateTime = baseTime.AddDays(-8), Duration = 12, Car = Cars[15], Client = Clients[2] }, + /// + new Rent { Id = 27, StartDateTime = baseTime.AddDays(-22), Duration = 24, Car = Cars[4], Client = Clients[3] }, + new Rent { Id = 28, StartDateTime = baseTime.AddDays(-18), Duration = 36, Car = Cars[8], Client = Clients[3] }, + new Rent { Id = 29, StartDateTime = baseTime.AddDays(-12), Duration = 12, Car = Cars[12], Client = Clients[3] }, + new Rent { Id = 30, StartDateTime = baseTime.AddDays(-6), Duration = 6, Car = Cars[17], Client = Clients[3] }, + /// + new Rent { Id = 31, StartDateTime = baseTime.AddDays(-28), Duration = 72, Car = Cars[1], Client = Clients[4] }, + new Rent { Id = 32, StartDateTime = baseTime.AddDays(-24), Duration = 24, Car = Cars[6], Client = Clients[4] }, + new Rent { Id = 33, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[9], Client = Clients[4] }, + new Rent { Id = 34, StartDateTime = baseTime.AddDays(-16), Duration = 12, Car = Cars[13], Client = Clients[4] }, + new Rent { Id = 35, StartDateTime = baseTime.AddDays(-10), Duration = 8, Car = Cars[18], Client = Clients[4] }, + /// + new Rent { Id = 36, StartDateTime = baseTime.AddDays(-30), Duration = 168, Car = Cars[2], Client = Clients[5] }, + new Rent { Id = 37, StartDateTime = baseTime.AddDays(-26), Duration = 24, Car = Cars[7], Client = Clients[5] }, + new Rent { Id = 38, StartDateTime = baseTime.AddDays(-22), Duration = 48, Car = Cars[11], Client = Clients[5] }, + new Rent { Id = 39, StartDateTime = baseTime.AddDays(-18), Duration = 6, Car = Cars[14], Client = Clients[5] }, + new Rent { Id = 40, StartDateTime = baseTime.AddDays(-14), Duration = 12, Car = Cars[16], Client = Clients[5] }, + new Rent { Id = 41, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[19], Client = Clients[5] }, + /// + new Rent { Id = 42, StartDateTime = baseTime.AddDays(-3), Duration = 10, Car = Cars[0], Client = Clients[6] }, + new Rent { Id = 43, StartDateTime = baseTime.AddDays(-1), Duration = 5, Car = Cars[2], Client = Clients[7] }, + new Rent { Id = 44, StartDateTime = baseTime.AddDays(1), Duration = 7, Car = Cars[5], Client = Clients[8] }, + new Rent { Id = 45, StartDateTime = baseTime.AddDays(3), Duration = 9, Car = Cars[10], Client = Clients[9] } + }; + } +} diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index 171e26d6e..40ae3c3e9 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -1,6 +1,5 @@ using CarRental.Domain.InternalData.ComponentEnums; - namespace CarRental.Domain.InternalData.ComponentClasses; /// diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs index 221bb6323..4b967474c 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -1,7 +1,7 @@ using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.InternalData.ComponentEnums; -namespace CarRental.Domain.IntenralData.ComponentClasses; +namespace CarRental.Domain.InternalData.ComponentClasses; /// /// Represents a specific generation of a car model, From d5fd5f08426ab795c788f7b4603431703fd21b2f Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 5 Dec 2025 16:49:39 +0400 Subject: [PATCH 06/37] added summaries for CarRental.Domain.DataSeed.DataSeed.cs and CarRental.Tests.DomainTests.cs and added a file to automate code verification --- .github/workflows/ci.yml | 29 ++++ .../DataSeed/{TestData.cs => DataSeed.cs} | 69 +++++--- .../InternalData/ComponentClasses/CarModel.cs | 1 + CarRental.Tests/DomainTests.cs | 152 +++++++++++++++++- 4 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/ci.yml rename CarRental.Domain/DataSeed/{TestData.cs => DataSeed.cs} (83%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ec3e155b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Run unit tests + run: dotnet test --no-build --configuration Release --verbosity normal \ No newline at end of file diff --git a/CarRental.Domain/DataSeed/TestData.cs b/CarRental.Domain/DataSeed/DataSeed.cs similarity index 83% rename from CarRental.Domain/DataSeed/TestData.cs rename to CarRental.Domain/DataSeed/DataSeed.cs index 0880f2ad3..76dca72c2 100644 --- a/CarRental.Domain/DataSeed/TestData.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -1,43 +1,65 @@ using CarRental.Domain.DataModels; using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Domain.InternalData.ComponentEnums; namespace CarRental.Domain.DataSeed; +/// +/// Provides a fixed set of pre-initialized domain entities for testing and demonstration purposes +/// public class DataSeed { + /// + /// List of physical vehicles available for rental + /// public List Cars { get; } - + + /// + /// List of registered clients + /// public List Clients { get; } + /// + /// List of rental agreements linking clients to specific cars + /// public List Rents { get; } + /// + /// List of car models representing vehicle + /// public List Models { get; } + /// + /// List of car model generations + /// public List Generation { get; } + /// + /// Constructor implementation + /// public DataSeed() { Models = new List { - new CarModel { Id = 1, Name = "Fiat 500", DriveType = DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, - new CarModel { Id = 2, Name = "Subaru Outback", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, - new CarModel { Id = 3, Name = "Volkswagen Golf", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 4, Name = "Mazda CX-5", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new CarModel { Id = 5, Name = "Nissan Qashqai", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, - new CarModel { Id = 6, Name = "Volvo XC90", DriveType = DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new CarModel { Id = 7, Name = "Audi A4", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 8, Name = "Honda CR-V", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, - new CarModel { Id = 9, Name = "Hyundai Tucson", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new CarModel { Id = 10, Name = "Volkswagen Transporter", DriveType = DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, - new CarModel { Id = 11, Name = "Mercedes E-Class", DriveType = DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, - new CarModel { Id = 12, Name = "Ford Focus", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 13, Name = "Jaguar F-Type", DriveType = DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, - new CarModel { Id = 14, Name = "Tesla Model 3", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 15, Name = "Toyota Camry", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 16, Name = "Lexus LS", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, - new CarModel { Id = 17, Name = "Porsche 911", DriveType = DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, - new CarModel { Id = 18, Name = "Renault Megane", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 19, Name = "BMW X5", DriveType = DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new CarModel { Id = 20, Name = "Kia Rio", DriveType = DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } + new CarModel { Id = 1, Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, + new CarModel { Id = 2, Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, + new CarModel { Id = 3, Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 4, Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new CarModel { Id = 5, Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, + new CarModel { Id = 6, Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new CarModel { Id = 7, Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 8, Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, + new CarModel { Id = 9, Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new CarModel { Id = 10, Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, + new CarModel { Id = 11, Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, + new CarModel { Id = 12, Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 13, Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, + new CarModel { Id = 14, Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 15, Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new CarModel { Id = 16, Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, + new CarModel { Id = 17, Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, + new CarModel { Id = 18, Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new CarModel { Id = 19, Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new CarModel { Id = 20, Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } }; Generation = new List @@ -138,29 +160,24 @@ public DataSeed() new Rent { Id = 21, StartDateTime = baseTime.AddDays(-25), Duration = 12, Car = Cars[5], Client = Clients[0] }, new Rent { Id = 22, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[0], Client = Clients[1] }, new Rent { Id = 23, StartDateTime = baseTime.AddDays(-5), Duration = 8, Car = Cars[10], Client = Clients[1] }, - /// new Rent { Id = 24, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[3], Client = Clients[2] }, new Rent { Id = 25, StartDateTime = baseTime.AddDays(-15), Duration = 6, Car = Cars[7], Client = Clients[2] }, new Rent { Id = 26, StartDateTime = baseTime.AddDays(-8), Duration = 12, Car = Cars[15], Client = Clients[2] }, - /// new Rent { Id = 27, StartDateTime = baseTime.AddDays(-22), Duration = 24, Car = Cars[4], Client = Clients[3] }, new Rent { Id = 28, StartDateTime = baseTime.AddDays(-18), Duration = 36, Car = Cars[8], Client = Clients[3] }, new Rent { Id = 29, StartDateTime = baseTime.AddDays(-12), Duration = 12, Car = Cars[12], Client = Clients[3] }, new Rent { Id = 30, StartDateTime = baseTime.AddDays(-6), Duration = 6, Car = Cars[17], Client = Clients[3] }, - /// new Rent { Id = 31, StartDateTime = baseTime.AddDays(-28), Duration = 72, Car = Cars[1], Client = Clients[4] }, new Rent { Id = 32, StartDateTime = baseTime.AddDays(-24), Duration = 24, Car = Cars[6], Client = Clients[4] }, new Rent { Id = 33, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[9], Client = Clients[4] }, new Rent { Id = 34, StartDateTime = baseTime.AddDays(-16), Duration = 12, Car = Cars[13], Client = Clients[4] }, new Rent { Id = 35, StartDateTime = baseTime.AddDays(-10), Duration = 8, Car = Cars[18], Client = Clients[4] }, - /// new Rent { Id = 36, StartDateTime = baseTime.AddDays(-30), Duration = 168, Car = Cars[2], Client = Clients[5] }, new Rent { Id = 37, StartDateTime = baseTime.AddDays(-26), Duration = 24, Car = Cars[7], Client = Clients[5] }, new Rent { Id = 38, StartDateTime = baseTime.AddDays(-22), Duration = 48, Car = Cars[11], Client = Clients[5] }, new Rent { Id = 39, StartDateTime = baseTime.AddDays(-18), Duration = 6, Car = Cars[14], Client = Clients[5] }, new Rent { Id = 40, StartDateTime = baseTime.AddDays(-14), Duration = 12, Car = Cars[16], Client = Clients[5] }, new Rent { Id = 41, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[19], Client = Clients[5] }, - /// new Rent { Id = 42, StartDateTime = baseTime.AddDays(-3), Duration = 10, Car = Cars[0], Client = Clients[6] }, new Rent { Id = 43, StartDateTime = baseTime.AddDays(-1), Duration = 5, Car = Cars[2], Client = Clients[7] }, new Rent { Id = 44, StartDateTime = baseTime.AddDays(1), Duration = 7, Car = Cars[5], Client = Clients[8] }, diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index 40ae3c3e9..b9d8d0a88 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -1,4 +1,5 @@ using CarRental.Domain.InternalData.ComponentEnums; +using DriveType = CarRental.Domain.InternalData.ComponentEnums.DriveType; namespace CarRental.Domain.InternalData.ComponentClasses; diff --git a/CarRental.Tests/DomainTests.cs b/CarRental.Tests/DomainTests.cs index 8f4980f1e..d1ffdc1eb 100644 --- a/CarRental.Tests/DomainTests.cs +++ b/CarRental.Tests/DomainTests.cs @@ -1,10 +1,154 @@ -namespace CarRental.Tests; +using CarRental.Domain.DataSeed; +using Xunit.Abstractions; -public class DomainTests +namespace CarRental.Tests; + +/// +/// A class that contains unit tests for checking various scenarios of using the main classes +/// +public class DomainTests : IClassFixture { + /// + /// Shared test data fixture providing pre-initialized domain entities + /// (clients, cars, models, rentals, etc.) for all test methods in this class + /// + private readonly DataSeed _fixture; + + /// + /// Helper for writing diagnostic output during test execution; + /// messages are visible in test logs (e.g., in Test Explorer or CI reports) + /// + private readonly ITestOutputHelper _output; + + /// + /// Initializes a new instance of the test class with shared test data and output helper + /// + public DomainTests(DataSeed fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + + /// + /// 1. Output of clients who rented vehicles of a specified model, + /// ordered by last name, first name, and patronymic + /// + [Fact] + public void Should_Return_Clients_Sorted_By_FullName_For_Given_Car_Model() + { + var target = _fixture.Models[9]; // Volkswagen Transporter + + var targetClients = _fixture.Rents + .Where(r => r.Car.ModelGeneration.Model.Name == target.Name) + .Select(r => r.Client) + .Distinct() + .OrderBy(c => c.LastName) + .ThenBy(c => c.FirstName) + .ThenBy(c => c.Patronymic) + .ToList(); + foreach (var client in targetClients) + { + _output.WriteLine($"{client.Id} {client.LastName} {client.FirstName} {client.Patronymic ?? ""} {client.BirthDate?.ToString() ?? ""}"); + } + + var correctId = new uint[] { 15, 5 }; + Assert.Equal(correctId, targetClients.Select(c => c.Id).ToArray()); + + } + + /// + /// 2. Output of vehicles currently in rental as of January 1, 2025, 10:00 + /// + [Fact] + public void CarsInRentAtBaseTime_AreListedCorrectly() + { + var now = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); + + var carsInRent = _fixture.Rents + .Where(r => r.StartDateTime <= now && now < r.StartDateTime.AddHours(r.Duration)) + .Select(r => r.Car) + .Distinct() + .OrderBy(c => c.Id) + .ToList(); + + foreach (var car in carsInRent) + { + _output.WriteLine($"{car.Id} {car.ModelGeneration.Model?.Name ?? ""} {car.NumberPlate} {car.Colour}"); + } + + var correctCount = 1; + Assert.Equal(carsInRent.Count, correctCount); + } + + /// + /// 3. Output of the top 5 most frequently rented vehicles, + /// sorted in descending order by rental count + /// [Fact] - public void BasicTest_ShouldPass() + public void Top5MostRentedCars_AreReturnedInDescendingOrder() { - Assert.Equal(1, 1); + var topCars = _fixture.Rents + .GroupBy(r => r.Car) + .Select(g => new { Car = g.Key, RentCount = g.Count() }) + .OrderByDescending(x => x.RentCount) + .ThenBy(x => x.Car.Id) + .Take(5) + .ToList(); + + foreach (var item in topCars) + { + _output.WriteLine($"{item.Car.Id} {item.Car.ModelGeneration.Model?.Name ?? ""} {item.RentCount}"); + } + + Assert.Equal(5, topCars.Count); + + } + + /// + /// 4. Output of rental counts for every vehicle in the fleet, + /// including vehicles with zero rentals + /// + [Fact] + public void AllCars_IncludeRentalCount_EvenIfZero() + { + foreach (var car in _fixture.Cars.OrderBy(c => c.Id)) + { + _output.WriteLine( + $"{car.Id} {car.ModelGeneration.Model?.Name ?? "Unknown"} {car.NumberPlate} " + + $"{car.Colour} {_fixture.Rents.Count(r => r.Car.Id == car.Id)}" + ); + } + + Assert.Equal(20, _fixture.Cars.Count); + } + + /// + /// 5. Output of the top 5 clients with the highest total rental amount, + /// calculated as the sum of (duration × hourly cost) for all their rentals + /// + [Fact] + public void Top5ClientsByTotalRentalAmount_AreReturnedCorrectly() + { + var clientTotals = _fixture.Rents + .GroupBy(r => r.Client) + .Select(g => new + { + Client = g.Key, + TotalAmount = g.Sum(r => r.Duration * r.Car.ModelGeneration.HourCost) + }) + .OrderByDescending(x => x.TotalAmount) + .ThenBy(x => x.Client.Id) + .Take(5) + .ToList(); + + foreach (var item in clientTotals) + { + _output.WriteLine( + $"{item.Client.LastName} {item.Client.FirstName} " + + $"{item.Client.Id} {item.TotalAmount:F2}" + ); + } + + Assert.True(clientTotals.Count == 5); } } From 09fa87487855688de3269978da6ad09dfc9f1431 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 11 Dec 2025 01:04:56 +0400 Subject: [PATCH 07/37] the .net version has been fixed from 9.0 to 8.0 --- CarRental.Domain/CarRental.Domain.csproj | 2 +- CarRental.Tests/CarRental.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CarRental.Domain/CarRental.Domain.csproj b/CarRental.Domain/CarRental.Domain.csproj index 125f4c93b..fa71b7ae6 100644 --- a/CarRental.Domain/CarRental.Domain.csproj +++ b/CarRental.Domain/CarRental.Domain.csproj @@ -1,7 +1,7 @@  - net9.0 + net8.0 enable enable diff --git a/CarRental.Tests/CarRental.Tests.csproj b/CarRental.Tests/CarRental.Tests.csproj index f3152e060..dd21883c9 100644 --- a/CarRental.Tests/CarRental.Tests.csproj +++ b/CarRental.Tests/CarRental.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net8.0 enable enable false From 6dab5dbf692f16b9de2a71da792677356d1896b9 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 11 Dec 2025 20:57:30 +0400 Subject: [PATCH 08/37] was added the first repository for storing cars --- CarRental.Domain/IRepository.cs | 16 ++++++++++++++++ .../CarRental.Infrastructure.csproj | 13 +++++++++++++ .../Repository/CarRepository.cs | 9 +++++++++ 3 files changed, 38 insertions(+) create mode 100644 CarRental.Domain/IRepository.cs create mode 100644 CarRental.Infrastructure/CarRental.Infrastructure.csproj create mode 100644 CarRental.Infrastructure/Repository/CarRepository.cs diff --git a/CarRental.Domain/IRepository.cs b/CarRental.Domain/IRepository.cs new file mode 100644 index 000000000..636cfa0ba --- /dev/null +++ b/CarRental.Domain/IRepository.cs @@ -0,0 +1,16 @@ +namespace CarRental.Domain; + +public interface IRepository + where TEntity : class +{ + public TKey Create(TEntity entity); + + public TEntity? Read(TKey id); + + public List ReadAll(); + + public void Update(TEntity entity); + + public bool Delete(TKey id); + +} \ No newline at end of file diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj new file mode 100644 index 000000000..7450f9583 --- /dev/null +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + + + + + + net8.0 + enable + enable + + + diff --git a/CarRental.Infrastructure/Repository/CarRepository.cs b/CarRental.Infrastructure/Repository/CarRepository.cs new file mode 100644 index 000000000..bd218ec9c --- /dev/null +++ b/CarRental.Infrastructure/Repository/CarRepository.cs @@ -0,0 +1,9 @@ +using CarRental.Domain.DataModels; +using CarRental.Domain; + +namespace CarRental.Infrastructure.Repository; + +public class CarRepository : IRepository +{ + +} \ No newline at end of file From 1c8cc849f393084d83245489b11c83bc5cfb578b Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 11 Dec 2025 22:35:33 +0400 Subject: [PATCH 09/37] minor changes: have the brackets in the files been changed, the type of Duration (double), HourCost (decimal) has been changed, the Id (uint) has been unified, the name field for the model has become mandatory, the class constructor in the tests has been redesigned for primary, the names of the tests have been changed in accordance with the conventions --- CarRental.Domain/DataModels/Car.cs | 5 +- CarRental.Domain/DataModels/Client.cs | 3 +- CarRental.Domain/DataModels/Rent.cs | 7 +- CarRental.Domain/DataSeed/DataSeed.cs | 253 +++++++++--------- .../InternalData/ComponentClasses/CarModel.cs | 4 +- .../ComponentClasses/CarModelGeneration.cs | 4 +- .../InternalData/ComponentEnums/BodyType.cs | 3 +- .../InternalData/ComponentEnums/ClassType.cs | 3 +- .../InternalData/ComponentEnums/DriveType.cs | 3 +- .../ComponentEnums/TransmissionType.cs | 3 +- CarRental.Tests/DomainTests.cs | 72 ++--- 11 files changed, 176 insertions(+), 184 deletions(-) diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs index b023293ba..be8ce4cf9 100644 --- a/CarRental.Domain/DataModels/Car.cs +++ b/CarRental.Domain/DataModels/Car.cs @@ -5,11 +5,12 @@ namespace CarRental.Domain.DataModels; /// /// Represents a specific physical vehicle available for rental /// -public class Car { +public class Car +{ /// /// Unique identifier of the car /// - public required int Id { get; set; } + public required uint Id { get; set; } /// /// The model generation this car belongs to, defining its year, transmission type, and base rental cost diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index ed70d0632..f3a5a1928 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -3,7 +3,8 @@ namespace CarRental.Domain.DataModels; /// /// Represents a client (rental customer) with personal and identification information /// -public class Client { +public class Client +{ /// /// Unique identifier of the client /// diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs index 7d7a488aa..15d688238 100644 --- a/CarRental.Domain/DataModels/Rent.cs +++ b/CarRental.Domain/DataModels/Rent.cs @@ -3,11 +3,12 @@ namespace CarRental.Domain.DataModels; /// /// Represents a car rental agreement between a client and the rental company /// -public class Rent { +public class Rent +{ /// /// Unique identifier of the rental record /// - public int Id { get; set; } + public uint Id { get; set; } /// /// Date and time when the rental period starts @@ -17,7 +18,7 @@ public class Rent { /// /// Duration of the rental in hours /// - public required int Duration { get; set; } + public required double Duration { get; set; } /// /// The car that is being rented diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs index 76dca72c2..b594dd437 100644 --- a/CarRental.Domain/DataSeed/DataSeed.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -7,7 +7,8 @@ namespace CarRental.Domain.DataSeed; /// /// Provides a fixed set of pre-initialized domain entities for testing and demonstration purposes /// -public class DataSeed { +public class DataSeed +{ /// /// List of physical vehicles available for rental /// @@ -40,148 +41,148 @@ public DataSeed() { Models = new List { - new CarModel { Id = 1, Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, - new CarModel { Id = 2, Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, - new CarModel { Id = 3, Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 4, Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new CarModel { Id = 5, Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, - new CarModel { Id = 6, Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new CarModel { Id = 7, Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 8, Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, - new CarModel { Id = 9, Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new CarModel { Id = 10, Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, - new CarModel { Id = 11, Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, - new CarModel { Id = 12, Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 13, Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, - new CarModel { Id = 14, Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 15, Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new CarModel { Id = 16, Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, - new CarModel { Id = 17, Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, - new CarModel { Id = 18, Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new CarModel { Id = 19, Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new CarModel { Id = 20, Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } + new() { Id = 1, Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, + new() { Id = 2, Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, + new() { Id = 3, Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = 4, Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new() { Id = 5, Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, + new() { Id = 6, Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new() { Id = 7, Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = 8, Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, + new() { Id = 9, Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new() { Id = 10, Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, + new() { Id = 11, Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, + new() { Id = 12, Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = 13, Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, + new() { Id = 14, Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = 15, Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = 16, Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, + new() { Id = 17, Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, + new() { Id = 18, Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = 19, Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new() { Id = 20, Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } }; Generation = new List { - new CarModelGeneration { Id = 1, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[16], HourCost = 160.0f }, // Porsche 911 - new CarModelGeneration { Id = 2, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[0], HourCost = 35.0f }, // Fiat 500 - new CarModelGeneration { Id = 3, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[11], HourCost = 55.0f }, // Ford Focus - new CarModelGeneration { Id = 4, Year = 2020, TransmissionType = TransmissionType.Variable, Model = Models[4], HourCost = 70.0f }, // Nissan Qashqai - new CarModelGeneration { Id = 5, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[18], HourCost = 120.0f }, // BMW X5 - new CarModelGeneration { Id = 6, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[15], HourCost = 140.0f }, // Lexus LS - new CarModelGeneration { Id = 7, Year = 2018, TransmissionType = TransmissionType.Manual, Model = Models[19], HourCost = 40.0f }, // Kia Rio - new CarModelGeneration { Id = 8, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[7], HourCost = 85.0f }, // Honda CR-V - new CarModelGeneration { Id = 9, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[12], HourCost = 150.0f }, // Jaguar F-Type - new CarModelGeneration { Id = 10, Year = 2020, TransmissionType = TransmissionType.Manual, Model = Models[9], HourCost = 60.0f }, // VW Transporter - new CarModelGeneration { Id = 11, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[1], HourCost = 95.0f }, // Subaru Outback - new CarModelGeneration { Id = 12, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[8], HourCost = 75.0f }, // Hyundai Tucson - new CarModelGeneration { Id = 13, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[2], HourCost = 50.0f }, // VW Golf - new CarModelGeneration { Id = 14, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[13], HourCost = 100.0f }, // Tesla Model 3 - new CarModelGeneration { Id = 15, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[14], HourCost = 80.0f }, // Toyota Camry - new CarModelGeneration { Id = 16, Year = 2020, TransmissionType = TransmissionType.Automatic, Model = Models[6], HourCost = 90.0f }, // Audi A4 - new CarModelGeneration { Id = 17, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[5], HourCost = 105.0f }, // Volvo XC90 - new CarModelGeneration { Id = 18, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[17], HourCost = 55.0f }, // Renault Megane - new CarModelGeneration { Id = 19, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[10], HourCost = 110.0f }, // Mercedes E-Class - new CarModelGeneration { Id = 20, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[3], HourCost = 80.0f } // Mazda CX-5 + new() { Id = 1, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[16], HourCost = 160.00m }, // Porsche 911 + new() { Id = 2, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[0], HourCost = 35.00m }, // Fiat 500 + new() { Id = 3, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[11], HourCost = 55.00m }, // Ford Focus + new() { Id = 4, Year = 2020, TransmissionType = TransmissionType.Variable, Model = Models[4], HourCost = 70.00m }, // Nissan Qashqai + new() { Id = 5, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[18], HourCost = 120.00m }, // BMW X5 + new() { Id = 6, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[15], HourCost = 140.00m }, // Lexus LS + new() { Id = 7, Year = 2018, TransmissionType = TransmissionType.Manual, Model = Models[19], HourCost = 40.00m }, // Kia Rio + new() { Id = 8, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[7], HourCost = 85.00m }, // Honda CR-V + new() { Id = 9, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[12], HourCost = 150.00m }, // Jaguar F-Type + new() { Id = 10, Year = 2020, TransmissionType = TransmissionType.Manual, Model = Models[9], HourCost = 60.00m }, // VW Transporter + new() { Id = 11, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[1], HourCost = 95.00m }, // Subaru Outback + new() { Id = 12, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[8], HourCost = 75.00m }, // Hyundai Tucson + new() { Id = 13, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[2], HourCost = 50.00m }, // VW Golf + new() { Id = 14, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[13], HourCost = 100.00m }, // Tesla Model 3 + new() { Id = 15, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[14], HourCost = 80.00m }, // Toyota Camry + new() { Id = 16, Year = 2020, TransmissionType = TransmissionType.Automatic, Model = Models[6], HourCost = 90.00m }, // Audi A4 + new() { Id = 17, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[5], HourCost = 105.00m }, // Volvo XC90 + new() { Id = 18, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[17], HourCost = 55.00m }, // Renault Megane + new() { Id = 19, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[10], HourCost = 110.00m }, // Mercedes E-Class + new() { Id = 20, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[3], HourCost = 80.00m } // Mazda CX-5 }; Cars = new List { - new Car { Id = 1, ModelGeneration = Generation[5], NumberPlate = "T890NO96", Colour = "Gray" }, - new Car { Id = 2, ModelGeneration = Generation[14], NumberPlate = "A123BC77", Colour = "Black" }, - new Car { Id = 3, ModelGeneration = Generation[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, - new Car { Id = 4, ModelGeneration = Generation[19], NumberPlate = "D012HI80", Colour = "Blue" }, - new Car { Id = 5, ModelGeneration = Generation[6], NumberPlate = "E345JK81", Colour = "Red" }, - new Car { Id = 6, ModelGeneration = Generation[16], NumberPlate = "F678LM82", Colour = "Gray" }, - new Car { Id = 7, ModelGeneration = Generation[7], NumberPlate = "G901NO83", Colour = "Green" }, - new Car { Id = 8, ModelGeneration = Generation[13], NumberPlate = "H234PQ84", Colour = "Black" }, - new Car { Id = 9, ModelGeneration = Generation[3], NumberPlate = "I567RS85", Colour = "White" }, - new Car { Id = 10, ModelGeneration = Generation[18], NumberPlate = "J890TU86", Colour = "Silver" }, - new Car { Id = 11, ModelGeneration = Generation[10], NumberPlate = "K123VW87", Colour = "Blue" }, - new Car { Id = 12, ModelGeneration = Generation[11], NumberPlate = "L456XY88", Colour = "Red" }, - new Car { Id = 13, ModelGeneration = Generation[8], NumberPlate = "R234JK94", Colour = "Blue" }, - new Car { Id = 14, ModelGeneration = Generation[9], NumberPlate = "N012BC90", Colour = "White" }, - new Car { Id = 15, ModelGeneration = Generation[1], NumberPlate = "Q901HI93", Colour = "Red" }, - new Car { Id = 16, ModelGeneration = Generation[15], NumberPlate = "P678FG92", Colour = "Silver" }, - new Car { Id = 17, ModelGeneration = Generation[2], NumberPlate = "O345DE91", Colour = "Black" }, - new Car { Id = 18, ModelGeneration = Generation[17], NumberPlate = "S567LM95", Colour = "Green" }, - new Car { Id = 19, ModelGeneration = Generation[4], NumberPlate = "C789FG79", Colour = "Silver" }, - new Car { Id = 20, ModelGeneration = Generation[12], NumberPlate = "B456DE78", Colour = "White" } + new() { Id = 1, ModelGeneration = Generation[5], NumberPlate = "T890NO96", Colour = "Gray" }, + new() { Id = 2, ModelGeneration = Generation[14], NumberPlate = "A123BC77", Colour = "Black" }, + new() { Id = 3, ModelGeneration = Generation[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, + new() { Id = 4, ModelGeneration = Generation[19], NumberPlate = "D012HI80", Colour = "Blue" }, + new() { Id = 5, ModelGeneration = Generation[6], NumberPlate = "E345JK81", Colour = "Red" }, + new() { Id = 6, ModelGeneration = Generation[16], NumberPlate = "F678LM82", Colour = "Gray" }, + new() { Id = 7, ModelGeneration = Generation[7], NumberPlate = "G901NO83", Colour = "Green" }, + new() { Id = 8, ModelGeneration = Generation[13], NumberPlate = "H234PQ84", Colour = "Black" }, + new() { Id = 9, ModelGeneration = Generation[3], NumberPlate = "I567RS85", Colour = "White" }, + new() { Id = 10, ModelGeneration = Generation[18], NumberPlate = "J890TU86", Colour = "Silver" }, + new() { Id = 11, ModelGeneration = Generation[10], NumberPlate = "K123VW87", Colour = "Blue" }, + new() { Id = 12, ModelGeneration = Generation[11], NumberPlate = "L456XY88", Colour = "Red" }, + new() { Id = 13, ModelGeneration = Generation[8], NumberPlate = "R234JK94", Colour = "Blue" }, + new() { Id = 14, ModelGeneration = Generation[9], NumberPlate = "N012BC90", Colour = "White" }, + new() { Id = 15, ModelGeneration = Generation[1], NumberPlate = "Q901HI93", Colour = "Red" }, + new() { Id = 16, ModelGeneration = Generation[15], NumberPlate = "P678FG92", Colour = "Silver" }, + new() { Id = 17, ModelGeneration = Generation[2], NumberPlate = "O345DE91", Colour = "Black" }, + new() { Id = 18, ModelGeneration = Generation[17], NumberPlate = "S567LM95", Colour = "Green" }, + new() { Id = 19, ModelGeneration = Generation[4], NumberPlate = "C789FG79", Colour = "Silver" }, + new() { Id = 20, ModelGeneration = Generation[12], NumberPlate = "B456DE78", Colour = "White" } }; Clients = new List { - new Client { Id = 1, DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, - new Client { Id = 2, DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, - new Client { Id = 3, DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, - new Client { Id = 4, DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, - new Client { Id = 5, DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, - new Client { Id = 6, DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, - new Client { Id = 7, DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, - new Client { Id = 8, DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, - new Client { Id = 9, DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, - new Client { Id = 10, DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, - new Client { Id = 11, DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, - new Client { Id = 12, DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, - new Client { Id = 13, DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, - new Client { Id = 14, DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, - new Client { Id = 15, DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, - new Client { Id = 16, DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, - new Client { Id = 17, DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, - new Client { Id = 18, DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, - new Client { Id = 19, DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, - new Client { Id = 20, DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } + new() { Id = 1, DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, + new() { Id = 2, DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, + new() { Id = 3, DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, + new() { Id = 4, DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, + new() { Id = 5, DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, + new() { Id = 6, DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, + new() { Id = 7, DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, + new() { Id = 8, DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, + new() { Id = 9, DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, + new() { Id = 10, DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, + new() { Id = 11, DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, + new() { Id = 12, DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, + new() { Id = 13, DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, + new() { Id = 14, DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, + new() { Id = 15, DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, + new() { Id = 16, DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, + new() { Id = 17, DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, + new() { Id = 18, DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, + new() { Id = 19, DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, + new() { Id = 20, DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } }; var baseTime = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); Rents = new List { - new Rent { Id = 1, StartDateTime = baseTime.AddDays(-2), Duration = 6, Car = Cars[13], Client = Clients[14] }, - new Rent { Id = 2, StartDateTime = baseTime.AddDays(12), Duration = 6, Car = Cars[19], Client = Clients[19] }, - new Rent { Id = 3, StartDateTime = baseTime.AddDays(-25), Duration = 48, Car = Cars[2], Client = Clients[2] }, - new Rent { Id = 4, StartDateTime = baseTime.AddDays(8), Duration = 24, Car = Cars[17], Client = Clients[17] }, - new Rent { Id = 5, StartDateTime = baseTime.AddDays(-20), Duration = 72, Car = Cars[4], Client = Clients[4] }, - new Rent { Id = 6, StartDateTime = baseTime.AddDays(4), Duration = 72, Car = Cars[15], Client = Clients[15] }, - new Rent { Id = 7, StartDateTime = baseTime.AddDays(-15), Duration = 168, Car = Cars[6], Client = Clients[6] }, - new Rent { Id = 8, StartDateTime = baseTime.AddDays(-4), Duration = 48, Car = Cars[11], Client = Clients[11] }, - new Rent { Id = 9, StartDateTime = baseTime.AddDays(-10), Duration = 36, Car = Cars[8], Client = Clients[8] }, - new Rent { Id = 10, StartDateTime = baseTime, Duration = 24, Car = Cars[1], Client = Clients[0] }, - new Rent { Id = 11, StartDateTime = baseTime.AddDays(2), Duration = 8, Car = Cars[14], Client = Clients[13] }, - new Rent { Id = 12, StartDateTime = baseTime.AddDays(-8), Duration = 24, Car = Cars[9], Client = Clients[9] }, - new Rent { Id = 13, StartDateTime = baseTime.AddDays(6), Duration = 12, Car = Cars[16], Client = Clients[16] }, - new Rent { Id = 14, StartDateTime = baseTime.AddDays(-6), Duration = 12, Car = Cars[10], Client = Clients[10] }, - new Rent { Id = 15, StartDateTime = baseTime.AddDays(10), Duration = 48, Car = Cars[18], Client = Clients[18] }, - new Rent { Id = 16, StartDateTime = baseTime.AddDays(-28), Duration = 12, Car = Cars[12], Client = Clients[12] }, - new Rent { Id = 17, StartDateTime = baseTime.AddDays(-22), Duration = 6, Car = Cars[3], Client = Clients[3] }, - new Rent { Id = 18, StartDateTime = baseTime.AddDays(-18), Duration = 8, Car = Cars[5], Client = Clients[5] }, - new Rent { Id = 19, StartDateTime = baseTime.AddDays(-12), Duration = 4, Car = Cars[7], Client = Clients[7] }, - new Rent { Id = 20, StartDateTime = baseTime.AddDays(-30), Duration = 24, Car = Cars[0], Client = Clients[1] }, - new Rent { Id = 21, StartDateTime = baseTime.AddDays(-25), Duration = 12, Car = Cars[5], Client = Clients[0] }, - new Rent { Id = 22, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[0], Client = Clients[1] }, - new Rent { Id = 23, StartDateTime = baseTime.AddDays(-5), Duration = 8, Car = Cars[10], Client = Clients[1] }, - new Rent { Id = 24, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[3], Client = Clients[2] }, - new Rent { Id = 25, StartDateTime = baseTime.AddDays(-15), Duration = 6, Car = Cars[7], Client = Clients[2] }, - new Rent { Id = 26, StartDateTime = baseTime.AddDays(-8), Duration = 12, Car = Cars[15], Client = Clients[2] }, - new Rent { Id = 27, StartDateTime = baseTime.AddDays(-22), Duration = 24, Car = Cars[4], Client = Clients[3] }, - new Rent { Id = 28, StartDateTime = baseTime.AddDays(-18), Duration = 36, Car = Cars[8], Client = Clients[3] }, - new Rent { Id = 29, StartDateTime = baseTime.AddDays(-12), Duration = 12, Car = Cars[12], Client = Clients[3] }, - new Rent { Id = 30, StartDateTime = baseTime.AddDays(-6), Duration = 6, Car = Cars[17], Client = Clients[3] }, - new Rent { Id = 31, StartDateTime = baseTime.AddDays(-28), Duration = 72, Car = Cars[1], Client = Clients[4] }, - new Rent { Id = 32, StartDateTime = baseTime.AddDays(-24), Duration = 24, Car = Cars[6], Client = Clients[4] }, - new Rent { Id = 33, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[9], Client = Clients[4] }, - new Rent { Id = 34, StartDateTime = baseTime.AddDays(-16), Duration = 12, Car = Cars[13], Client = Clients[4] }, - new Rent { Id = 35, StartDateTime = baseTime.AddDays(-10), Duration = 8, Car = Cars[18], Client = Clients[4] }, - new Rent { Id = 36, StartDateTime = baseTime.AddDays(-30), Duration = 168, Car = Cars[2], Client = Clients[5] }, - new Rent { Id = 37, StartDateTime = baseTime.AddDays(-26), Duration = 24, Car = Cars[7], Client = Clients[5] }, - new Rent { Id = 38, StartDateTime = baseTime.AddDays(-22), Duration = 48, Car = Cars[11], Client = Clients[5] }, - new Rent { Id = 39, StartDateTime = baseTime.AddDays(-18), Duration = 6, Car = Cars[14], Client = Clients[5] }, - new Rent { Id = 40, StartDateTime = baseTime.AddDays(-14), Duration = 12, Car = Cars[16], Client = Clients[5] }, - new Rent { Id = 41, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[19], Client = Clients[5] }, - new Rent { Id = 42, StartDateTime = baseTime.AddDays(-3), Duration = 10, Car = Cars[0], Client = Clients[6] }, - new Rent { Id = 43, StartDateTime = baseTime.AddDays(-1), Duration = 5, Car = Cars[2], Client = Clients[7] }, - new Rent { Id = 44, StartDateTime = baseTime.AddDays(1), Duration = 7, Car = Cars[5], Client = Clients[8] }, - new Rent { Id = 45, StartDateTime = baseTime.AddDays(3), Duration = 9, Car = Cars[10], Client = Clients[9] } + new() { Id = 1, StartDateTime = baseTime.AddDays(-2), Duration = 6, Car = Cars[13], Client = Clients[14] }, + new() { Id = 2, StartDateTime = baseTime.AddDays(12), Duration = 6, Car = Cars[19], Client = Clients[19] }, + new() { Id = 3, StartDateTime = baseTime.AddDays(-25), Duration = 48, Car = Cars[2], Client = Clients[2] }, + new() { Id = 4, StartDateTime = baseTime.AddDays(8), Duration = 24, Car = Cars[17], Client = Clients[17] }, + new() { Id = 5, StartDateTime = baseTime.AddDays(-20), Duration = 72, Car = Cars[4], Client = Clients[4] }, + new() { Id = 6, StartDateTime = baseTime.AddDays(4), Duration = 72, Car = Cars[15], Client = Clients[15] }, + new() { Id = 7, StartDateTime = baseTime.AddDays(-15), Duration = 168, Car = Cars[6], Client = Clients[6] }, + new() { Id = 8, StartDateTime = baseTime.AddDays(-4), Duration = 48, Car = Cars[11], Client = Clients[11] }, + new() { Id = 9, StartDateTime = baseTime.AddDays(-10), Duration = 36, Car = Cars[8], Client = Clients[8] }, + new() { Id = 10, StartDateTime = baseTime, Duration = 24, Car = Cars[1], Client = Clients[0] }, + new() { Id = 11, StartDateTime = baseTime.AddDays(2), Duration = 8, Car = Cars[14], Client = Clients[13] }, + new() { Id = 12, StartDateTime = baseTime.AddDays(-8), Duration = 24, Car = Cars[9], Client = Clients[9] }, + new() { Id = 13, StartDateTime = baseTime.AddDays(6), Duration = 12, Car = Cars[16], Client = Clients[16] }, + new() { Id = 14, StartDateTime = baseTime.AddDays(-6), Duration = 12, Car = Cars[10], Client = Clients[10] }, + new() { Id = 15, StartDateTime = baseTime.AddDays(10), Duration = 48, Car = Cars[18], Client = Clients[18] }, + new() { Id = 16, StartDateTime = baseTime.AddDays(-28), Duration = 12, Car = Cars[12], Client = Clients[12] }, + new() { Id = 17, StartDateTime = baseTime.AddDays(-22), Duration = 6, Car = Cars[3], Client = Clients[3] }, + new() { Id = 18, StartDateTime = baseTime.AddDays(-18), Duration = 8, Car = Cars[5], Client = Clients[5] }, + new() { Id = 19, StartDateTime = baseTime.AddDays(-12), Duration = 4, Car = Cars[7], Client = Clients[7] }, + new() { Id = 20, StartDateTime = baseTime.AddDays(-30), Duration = 24, Car = Cars[0], Client = Clients[1] }, + new() { Id = 21, StartDateTime = baseTime.AddDays(-25), Duration = 12, Car = Cars[5], Client = Clients[0] }, + new() { Id = 22, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[0], Client = Clients[1] }, + new() { Id = 23, StartDateTime = baseTime.AddDays(-5), Duration = 8, Car = Cars[10], Client = Clients[1] }, + new() { Id = 24, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[3], Client = Clients[2] }, + new() { Id = 25, StartDateTime = baseTime.AddDays(-15), Duration = 6, Car = Cars[7], Client = Clients[2] }, + new() { Id = 26, StartDateTime = baseTime.AddDays(-8), Duration = 12, Car = Cars[15], Client = Clients[2] }, + new() { Id = 27, StartDateTime = baseTime.AddDays(-22), Duration = 24, Car = Cars[4], Client = Clients[3] }, + new() { Id = 28, StartDateTime = baseTime.AddDays(-18), Duration = 36, Car = Cars[8], Client = Clients[3] }, + new() { Id = 29, StartDateTime = baseTime.AddDays(-12), Duration = 12, Car = Cars[12], Client = Clients[3] }, + new() { Id = 30, StartDateTime = baseTime.AddDays(-6), Duration = 6, Car = Cars[17], Client = Clients[3] }, + new() { Id = 31, StartDateTime = baseTime.AddDays(-28), Duration = 72, Car = Cars[1], Client = Clients[4] }, + new() { Id = 32, StartDateTime = baseTime.AddDays(-24), Duration = 24, Car = Cars[6], Client = Clients[4] }, + new() { Id = 33, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[9], Client = Clients[4] }, + new() { Id = 34, StartDateTime = baseTime.AddDays(-16), Duration = 12, Car = Cars[13], Client = Clients[4] }, + new() { Id = 35, StartDateTime = baseTime.AddDays(-10), Duration = 8, Car = Cars[18], Client = Clients[4] }, + new() { Id = 36, StartDateTime = baseTime.AddDays(-30), Duration = 168, Car = Cars[2], Client = Clients[5] }, + new() { Id = 37, StartDateTime = baseTime.AddDays(-26), Duration = 24, Car = Cars[7], Client = Clients[5] }, + new() { Id = 38, StartDateTime = baseTime.AddDays(-22), Duration = 48, Car = Cars[11], Client = Clients[5] }, + new() { Id = 39, StartDateTime = baseTime.AddDays(-18), Duration = 6, Car = Cars[14], Client = Clients[5] }, + new() { Id = 40, StartDateTime = baseTime.AddDays(-14), Duration = 12, Car = Cars[16], Client = Clients[5] }, + new() { Id = 41, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[19], Client = Clients[5] }, + new() { Id = 42, StartDateTime = baseTime.AddDays(-3), Duration = 10, Car = Cars[0], Client = Clients[6] }, + new() { Id = 43, StartDateTime = baseTime.AddDays(-1), Duration = 5, Car = Cars[2], Client = Clients[7] }, + new() { Id = 44, StartDateTime = baseTime.AddDays(1), Duration = 7, Car = Cars[5], Client = Clients[8] }, + new() { Id = 45, StartDateTime = baseTime.AddDays(3), Duration = 9, Car = Cars[10], Client = Clients[9] } }; } } diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index b9d8d0a88..542f2f817 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -12,12 +12,12 @@ public class CarModel /// /// Unique identifier of the car model /// - public required int Id { get; set; } + public required uint Id { get; set; } /// /// Name of the car model (e.g., "Camry", "Golf", "Model 3") /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Type of drive system used by the car model (front-wheel, rear-wheel or all-wheel drive) diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs index 4b967474c..7b9a3bd83 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -13,7 +13,7 @@ public class CarModelGeneration /// /// Unique identifier of the car model generation /// - public required int Id { get; set; } + public required uint Id { get; set; } /// /// Calendar year when this generation of the car model was produced @@ -35,5 +35,5 @@ public class CarModelGeneration /// /// Rental cost per hour for vehicles of this model generation /// - public required float HourCost { get; set; } + public required decimal HourCost { get; set; } } diff --git a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs index cbc6db945..16587ce1f 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/BodyType.cs @@ -3,7 +3,8 @@ namespace CarRental.Domain.InternalData.ComponentEnums; /// /// Type of vehicle body style /// -public enum BodyType { +public enum BodyType +{ /// /// Ultra-small city car designed for maximum fuel efficiency and maneuverability /// diff --git a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs index 229bae8ab..5c415b308 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/ClassType.cs @@ -3,7 +3,8 @@ namespace CarRental.Domain.InternalData.ComponentEnums; /// /// Vehicle classification based on size and segment /// -public enum ClassType { +public enum ClassType +{ /// /// Mini cars, the smallest urban vehicle class /// diff --git a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs index 0aea4abd7..d30a9f9ad 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/DriveType.cs @@ -3,7 +3,8 @@ namespace CarRental.Domain.InternalData.ComponentEnums; /// /// The type of vehicle drive system /// -public enum DriveType { +public enum DriveType +{ /// /// Front-wheel drive, where power is delivered to the front wheels /// diff --git a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs index 78a7a1f6e..50170c726 100644 --- a/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs +++ b/CarRental.Domain/InternalData/ComponentEnums/TransmissionType.cs @@ -3,7 +3,8 @@ namespace CarRental.Domain.InternalData.ComponentEnums; /// /// The type of vehicle transmission /// -public enum TransmissionType { +public enum TransmissionType +{ /// /// Manual gearbox with driver-operated gear shifting /// diff --git a/CarRental.Tests/DomainTests.cs b/CarRental.Tests/DomainTests.cs index d1ffdc1eb..7f41f0a2d 100644 --- a/CarRental.Tests/DomainTests.cs +++ b/CarRental.Tests/DomainTests.cs @@ -4,51 +4,35 @@ namespace CarRental.Tests; /// -/// A class that contains unit tests for checking various scenarios of using the main classes +/// Unit tests for rental domain analytics, initialized with shared test data and output helper. +/// The primary constructor accepts: +/// - : Pre-filled domain entities (clients, cars, models, rentals). +/// - : xUnit helper for diagnostic logging in test results. /// -public class DomainTests : IClassFixture +public class DomainTests( + DataSeed fixture, + ITestOutputHelper output) : IClassFixture { - /// - /// Shared test data fixture providing pre-initialized domain entities - /// (clients, cars, models, rentals, etc.) for all test methods in this class - /// - private readonly DataSeed _fixture; - - /// - /// Helper for writing diagnostic output during test execution; - /// messages are visible in test logs (e.g., in Test Explorer or CI reports) - /// - private readonly ITestOutputHelper _output; - - /// - /// Initializes a new instance of the test class with shared test data and output helper - /// - public DomainTests(DataSeed fixture, ITestOutputHelper output) - { - _fixture = fixture; - _output = output; - } - /// /// 1. Output of clients who rented vehicles of a specified model, /// ordered by last name, first name, and patronymic /// [Fact] - public void Should_Return_Clients_Sorted_By_FullName_For_Given_Car_Model() + public void GetClientsByModelName_WhenModelHasRentals_ReturnsClientsSortedByFullName() { - var target = _fixture.Models[9]; // Volkswagen Transporter + var target = fixture.Models[9]; // Volkswagen Transporter - var targetClients = _fixture.Rents - .Where(r => r.Car.ModelGeneration.Model.Name == target.Name) + var targetClients = fixture.Rents + .Where(r => r.Car?.ModelGeneration.Model?.Name == target.Name) .Select(r => r.Client) .Distinct() .OrderBy(c => c.LastName) .ThenBy(c => c.FirstName) - .ThenBy(c => c.Patronymic) + .ThenBy(c => c.Patronymic ?? string.Empty) .ToList(); foreach (var client in targetClients) { - _output.WriteLine($"{client.Id} {client.LastName} {client.FirstName} {client.Patronymic ?? ""} {client.BirthDate?.ToString() ?? ""}"); + output.WriteLine($"{client.Id} {client.LastName} {client.FirstName} {client.Patronymic ?? ""} {client.BirthDate?.ToString() ?? ""}"); } var correctId = new uint[] { 15, 5 }; @@ -60,11 +44,11 @@ public void Should_Return_Clients_Sorted_By_FullName_For_Given_Car_Model() /// 2. Output of vehicles currently in rental as of January 1, 2025, 10:00 /// [Fact] - public void CarsInRentAtBaseTime_AreListedCorrectly() + public void GetCarsInRent_WhenCheckedAtBaseTime_ReturnsActiveRentalCars() { var now = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); - var carsInRent = _fixture.Rents + var carsInRent = fixture.Rents .Where(r => r.StartDateTime <= now && now < r.StartDateTime.AddHours(r.Duration)) .Select(r => r.Car) .Distinct() @@ -73,7 +57,7 @@ public void CarsInRentAtBaseTime_AreListedCorrectly() foreach (var car in carsInRent) { - _output.WriteLine($"{car.Id} {car.ModelGeneration.Model?.Name ?? ""} {car.NumberPlate} {car.Colour}"); + output.WriteLine($"{car.Id} {car.ModelGeneration.Model?.Name ?? ""} {car.NumberPlate} {car.Colour}"); } var correctCount = 1; @@ -85,9 +69,9 @@ public void CarsInRentAtBaseTime_AreListedCorrectly() /// sorted in descending order by rental count /// [Fact] - public void Top5MostRentedCars_AreReturnedInDescendingOrder() + public void GetTopRentedCars_WhenAllRentalsExist_ReturnsTop5CarsOrderedByRentalCountDescending() { - var topCars = _fixture.Rents + var topCars = fixture.Rents .GroupBy(r => r.Car) .Select(g => new { Car = g.Key, RentCount = g.Count() }) .OrderByDescending(x => x.RentCount) @@ -97,7 +81,7 @@ public void Top5MostRentedCars_AreReturnedInDescendingOrder() foreach (var item in topCars) { - _output.WriteLine($"{item.Car.Id} {item.Car.ModelGeneration.Model?.Name ?? ""} {item.RentCount}"); + output.WriteLine($"{item.Car.Id} {item.Car.ModelGeneration.Model?.Name ?? ""} {item.RentCount}"); } Assert.Equal(5, topCars.Count); @@ -109,17 +93,17 @@ public void Top5MostRentedCars_AreReturnedInDescendingOrder() /// including vehicles with zero rentals /// [Fact] - public void AllCars_IncludeRentalCount_EvenIfZero() + public void GetAllCars_WhenFleetIsInitialized_ReturnsAllCarsWithRentalCountIncludingZero() { - foreach (var car in _fixture.Cars.OrderBy(c => c.Id)) + foreach (var car in fixture.Cars.OrderBy(c => c.Id)) { - _output.WriteLine( + output.WriteLine( $"{car.Id} {car.ModelGeneration.Model?.Name ?? "Unknown"} {car.NumberPlate} " + - $"{car.Colour} {_fixture.Rents.Count(r => r.Car.Id == car.Id)}" + $"{car.Colour} {fixture.Rents.Count(r => r.Car.Id == car.Id)}" ); } - Assert.Equal(20, _fixture.Cars.Count); + Assert.Equal(20, fixture.Cars.Count); } /// @@ -127,14 +111,14 @@ public void AllCars_IncludeRentalCount_EvenIfZero() /// calculated as the sum of (duration × hourly cost) for all their rentals /// [Fact] - public void Top5ClientsByTotalRentalAmount_AreReturnedCorrectly() + public void GetTopClientsByTotalRentalAmount_WhenRentalsHaveDurationAndCost_ReturnsTop5ClientsOrderedByAmountDescending() { - var clientTotals = _fixture.Rents + var clientTotals = fixture.Rents .GroupBy(r => r.Client) .Select(g => new { Client = g.Key, - TotalAmount = g.Sum(r => r.Duration * r.Car.ModelGeneration.HourCost) + TotalAmount = g.Sum(r => Convert.ToDecimal(r.Duration) * r.Car.ModelGeneration.HourCost) }) .OrderByDescending(x => x.TotalAmount) .ThenBy(x => x.Client.Id) @@ -143,7 +127,7 @@ public void Top5ClientsByTotalRentalAmount_AreReturnedCorrectly() foreach (var item in clientTotals) { - _output.WriteLine( + output.WriteLine( $"{item.Client.LastName} {item.Client.FirstName} " + $"{item.Client.Id} {item.TotalAmount:F2}" ); From 842d9edd80eb479f0a9a6535064b98fec2416830 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 15 Dec 2025 22:50:32 +0400 Subject: [PATCH 10/37] made minor change in data seed field name, added abstract class for repository and repositories for classes from domain space --- CarRental.Domain/BaseRepository.cs | 70 +++++++++++++++++++ CarRental.Domain/DataSeed/DataSeed.cs | 4 +- CarRental.Domain/IRepository.cs | 16 ----- .../CarModelGenerationRepository.cs | 12 ++++ .../InMemoryRepository/CarModelRepository.cs | 12 ++++ .../InMemoryRepository/CarRepository.cs | 12 ++++ .../InMemoryRepository/ClientRepository.cs | 12 ++++ .../InMemoryRepository/RentRepository.cs | 12 ++++ .../Repository/CarRepository.cs | 9 --- CarRental.sln | 17 +++++ 10 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 CarRental.Domain/BaseRepository.cs delete mode 100644 CarRental.Domain/IRepository.cs create mode 100644 CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs create mode 100644 CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs create mode 100644 CarRental.Infrastructure/InMemoryRepository/CarRepository.cs create mode 100644 CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs create mode 100644 CarRental.Infrastructure/InMemoryRepository/RentRepository.cs delete mode 100644 CarRental.Infrastructure/Repository/CarRepository.cs diff --git a/CarRental.Domain/BaseRepository.cs b/CarRental.Domain/BaseRepository.cs new file mode 100644 index 000000000..e8e27bc88 --- /dev/null +++ b/CarRental.Domain/BaseRepository.cs @@ -0,0 +1,70 @@ +namespace CarRental.Domain; + +public abstract class BaseRepository + where TEntity : class + where TKey: struct +{ + private uint _nextId; + + protected abstract TKey GetEntityId(TEntity entity); + + protected abstract void SetEntityId(TEntity entity, TKey id); + + private readonly List _entities; + + protected Repository(List? entities = null) + { + if (entities != null) + { + _entities = entities; + _nextId = _entities.Count + 1; + } + else + { + _entities = new List(); + _nextId = 1; + } + } + + public virtual uint Create(TEntity entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + uint currentId = _nextId; + SetEntityId(entity, currentId); + _entities.Add(entity); + _nextId++; + return currentId; + } + + public virtual TEntity? Read(uint id) + { + return _entities.FirstOrDefault(c => c.Id == id); + } + + public virtual List ReadAll() + { + List copy = _entities; + return copy; + } + + public virtual void Update(TEntity entity) + { + Delete(entity.Id); + _entities.Add(entity); + } + + public virtual bool Delete(uint id) + { + if (_entities[id] != null) + { + _entities.RemoveAt(id); + return true; + } + return false; + } + + +} \ No newline at end of file diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs index b594dd437..46dbd178d 100644 --- a/CarRental.Domain/DataSeed/DataSeed.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -32,7 +32,7 @@ public class DataSeed /// /// List of car model generations /// - public List Generation { get; } + public List Generations { get; } /// /// Constructor implementation @@ -63,7 +63,7 @@ public DataSeed() new() { Id = 20, Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } }; - Generation = new List + Generations = new List { new() { Id = 1, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[16], HourCost = 160.00m }, // Porsche 911 new() { Id = 2, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[0], HourCost = 35.00m }, // Fiat 500 diff --git a/CarRental.Domain/IRepository.cs b/CarRental.Domain/IRepository.cs deleted file mode 100644 index 636cfa0ba..000000000 --- a/CarRental.Domain/IRepository.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace CarRental.Domain; - -public interface IRepository - where TEntity : class -{ - public TKey Create(TEntity entity); - - public TEntity? Read(TKey id); - - public List ReadAll(); - - public void Update(TEntity entity); - - public bool Delete(TKey id); - -} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs new file mode 100644 index 000000000..a4ec9701f --- /dev/null +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -0,0 +1,12 @@ +using CarRental.Domain; +using CarRental.Domain.DataModels; +using CarRental.Domain.DataSeed; + +namespace CarRental.Infrastructure.InMemoryRepository; + +public class RentRepository(DataSeed data) : BaseRepository(data.Generations) +{ + protected override uint GetEntityId(CarModelGeneration generation) => generation.Id; + + protected override void SetEntityId(CarModelGeneration generation, uint id) => generation.Id = id; +} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs new file mode 100644 index 000000000..ea70ca41d --- /dev/null +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -0,0 +1,12 @@ +using CarRental.Domain; +using CarRental.Domain.DataModels; +using CarRental.Domain.DataSeed; + +namespace CarRental.Infrastructure.InMemoryRepository; + +public class RentRepository(DataSeed data) : BaseRepository(data.Models) +{ + protected override uint GetEntityId(CarModel model) => model.Id; + + protected override void SetEntityId(CarModel model, uint id) => model.Id = id; +} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs new file mode 100644 index 000000000..3ea21eb59 --- /dev/null +++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs @@ -0,0 +1,12 @@ +using CarRental.Domain; +using CarRental.Domain.DataModels; +using CarRental.Domain.DataSeed; + +namespace CarRental.Infrastructure.InMemoryRepository; + +public class CarRepository(DataSeed data) : BaseRepository(data.Cars) +{ + protected override uint GetEntityId(Car car) => car.Id; + + protected override void SetEntityId(Car car, uint id) => car.Id = id; +} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs new file mode 100644 index 000000000..bdbf4b2ea --- /dev/null +++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs @@ -0,0 +1,12 @@ +using CarRental.Domain; +using CarRental.Domain.DataModels; +using CarRental.Domain.DataSeed; + +namespace CarRental.Infrastructure.InMemoryRepository; + +public class ClientRepository(DataSeed data) : BaseRepository(data.Clients) +{ + protected override uint GetEntityId(Client client) => client.Id; + + protected override void SetEntityId(Client client, uint id) => client.Id = id; +} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs new file mode 100644 index 000000000..f6e9e02c2 --- /dev/null +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -0,0 +1,12 @@ +using CarRental.Domain; +using CarRental.Domain.DataModels; +using CarRental.Domain.DataSeed; + +namespace CarRental.Infrastructure.InMemoryRepository; + +public class RentRepository(DataSeed data) : BaseRepository(data.Rents) +{ + protected override uint GetEntityId(Rent rent) => rent.Id; + + protected override void SetEntityId(Rent rent, uint id) => rent.Id = id; +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/CarRepository.cs b/CarRental.Infrastructure/Repository/CarRepository.cs deleted file mode 100644 index bd218ec9c..000000000 --- a/CarRental.Infrastructure/Repository/CarRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CarRental.Domain.DataModels; -using CarRental.Domain; - -namespace CarRental.Infrastructure.Repository; - -public class CarRepository : IRepository -{ - -} \ No newline at end of file diff --git a/CarRental.sln b/CarRental.sln index 342845641..39c86166d 100644 --- a/CarRental.sln +++ b/CarRental.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Domain", "CarRent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{B253FF47-F3FD-4F60-934B-0A2649ACA810}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Api", "CarRental.Api\CarRental.Api.csproj", "{5E76316F-B8B5-4F6A-B49E-BB8C5333332D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,8 +43,23 @@ Global {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x64.Build.0 = Release|Any CPU {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.ActiveCfg = Release|Any CPU {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.Build.0 = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x64.Build.0 = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x86.Build.0 = Debug|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|Any CPU.Build.0 = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x64.ActiveCfg = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x64.Build.0 = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x86.ActiveCfg = Release|Any CPU + {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DDDF992B-860F-484F-9B9D-CE109A6F3417} + EndGlobalSection EndGlobal From f5f343d1d52390d372254abdba6cb94915d086b1 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 19 Dec 2025 18:48:50 +0400 Subject: [PATCH 11/37] added an interface of base repository for better abstraction, fixed some methods in BaseRepository to complete incapsulation and correctness work with ID's, fixed names of classes in repositories, replaced TKey generic to uint --- .../CarRental.Application.Contracts.csproj | 9 +++++ CarRental.Application.Contracts/Class1.cs | 6 ++++ .../Interfaces/IAnalyticsService.cs | 14 ++++++++ .../Interfaces/IApplicationService.cs | 17 +++++++++ .../CarRental.Application.csproj | 14 ++++++++ CarRental.Application/Class1.cs | 6 ++++ .../{ => Interfaces}/BaseRepository.cs | 36 ++++++++++--------- .../Interfaces/IBaseRepository.cs | 11 ++++++ .../CarModelGenerationRepository.cs | 4 +-- .../InMemoryRepository/CarModelRepository.cs | 4 +-- .../InMemoryRepository/CarRepository.cs | 4 +-- .../InMemoryRepository/ClientRepository.cs | 4 +-- .../InMemoryRepository/RentRepository.cs | 4 +-- 13 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 CarRental.Application.Contracts/CarRental.Application.Contracts.csproj create mode 100644 CarRental.Application.Contracts/Class1.cs create mode 100644 CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs create mode 100644 CarRental.Application.Contracts/Interfaces/IApplicationService.cs create mode 100644 CarRental.Application/CarRental.Application.csproj create mode 100644 CarRental.Application/Class1.cs rename CarRental.Domain/{ => Interfaces}/BaseRepository.cs (51%) create mode 100644 CarRental.Domain/Interfaces/IBaseRepository.cs diff --git a/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/CarRental.Application.Contracts/Class1.cs b/CarRental.Application.Contracts/Class1.cs new file mode 100644 index 000000000..750d91393 --- /dev/null +++ b/CarRental.Application.Contracts/Class1.cs @@ -0,0 +1,6 @@ +namespace CarRental.Application.Contracts; + +public class Class1 +{ + +} diff --git a/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs new file mode 100644 index 000000000..06b80a4c4 --- /dev/null +++ b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs @@ -0,0 +1,14 @@ +namespace CarRental.Application.Contracts; + +public interface IAnalyticsService +{ + List ReadClientsByModelName(string modelName); + + List ReadCarsInRent(DateTime atTime); + + List ReadTop5MostRentedCars(); + + List ReadAllCarsWithRentalCount(); + + List ReadTop5ClientsByTotalAmount(); +} \ No newline at end of file diff --git a/CarRental.Application.Contracts/Interfaces/IApplicationService.cs b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs new file mode 100644 index 000000000..dae44e5d7 --- /dev/null +++ b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs @@ -0,0 +1,17 @@ +namespace CarRental.Application.Contracts.Interfaces; + +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + public TDto Create(TCreateUpdateDto dto); + + public TDto? Read(TKey id); + + public List ReadAll(); + + public TDto Update(TCreateUpdateDto dto, TKey id); + + public bool Delete(TKey id); +} \ No newline at end of file diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj new file mode 100644 index 000000000..74d4d05a8 --- /dev/null +++ b/CarRental.Application/CarRental.Application.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net8.0 + enable + enable + + + diff --git a/CarRental.Application/Class1.cs b/CarRental.Application/Class1.cs new file mode 100644 index 000000000..14b4f1ba3 --- /dev/null +++ b/CarRental.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace CarRental.Application; + +public class Class1 +{ + +} diff --git a/CarRental.Domain/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs similarity index 51% rename from CarRental.Domain/BaseRepository.cs rename to CarRental.Domain/Interfaces/BaseRepository.cs index e8e27bc88..f3df82d77 100644 --- a/CarRental.Domain/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -1,27 +1,25 @@ -namespace CarRental.Domain; +namespace CarRental.Domain.Interfaces; -public abstract class BaseRepository +public abstract class BaseRepository : IBaseRepository where TEntity : class - where TKey: struct { private uint _nextId; - protected abstract TKey GetEntityId(TEntity entity); + protected abstract uint GetEntityId(TEntity entity); - protected abstract void SetEntityId(TEntity entity, TKey id); + protected abstract void SetEntityId(TEntity entity, uint id); private readonly List _entities; - protected Repository(List? entities = null) + protected BaseRepository(List? entities = null) { - if (entities != null) + _entities = entities ?? new List(); + if (_entities.Count > 0) { - _entities = entities; - _nextId = _entities.Count + 1; + _nextId = _entities.Max(e => GetEntityId(e)) + 1; } else { - _entities = new List(); _nextId = 1; } } @@ -46,22 +44,26 @@ public virtual uint Create(TEntity entity) public virtual List ReadAll() { - List copy = _entities; - return copy; + return _entities.ToList(); } public virtual void Update(TEntity entity) { - Delete(entity.Id); - _entities.Add(entity); + var existing = Read(id); + if (existing != null) + { + var index = _entities.IndexOf(existing); + SetEntityId(entity, id); + _entities[index] = entity; + } } public virtual bool Delete(uint id) { - if (_entities[id] != null) + var entity = Read(id); + if (entity != null) { - _entities.RemoveAt(id); - return true; + return _entities.Remove(entity); } return false; } diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs new file mode 100644 index 000000000..62cce7523 --- /dev/null +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -0,0 +1,11 @@ +namespace CarRental.Domain.Interfaces; + +public interface IBaseRepository + where TEntity : class +{ + uint Create(TEntity entity); + TEntity? Read(uint id); + List ReadAll(); + void Update(TEntity entity, uint id); + bool Delete(uint id); +} \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs index a4ec9701f..d2232d4b5 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -1,10 +1,10 @@ -using CarRental.Domain; +using CarRental.Domain.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; -public class RentRepository(DataSeed data) : BaseRepository(data.Generations) +public class CarModelGenerationRepository(DataSeed data) : BaseRepository(data.Generations) { protected override uint GetEntityId(CarModelGeneration generation) => generation.Id; diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs index ea70ca41d..336f7917a 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -1,10 +1,10 @@ -using CarRental.Domain; +using CarRental.Domain.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; -public class RentRepository(DataSeed data) : BaseRepository(data.Models) +public class CarModelRepository(DataSeed data) : BaseRepository(data.Models) { protected override uint GetEntityId(CarModel model) => model.Id; diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs index 3ea21eb59..782e7eee6 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs @@ -1,10 +1,10 @@ -using CarRental.Domain; +using CarRental.Domain.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; -public class CarRepository(DataSeed data) : BaseRepository(data.Cars) +public class CarRepository(DataSeed data) : BaseRepository(data.Cars) { protected override uint GetEntityId(Car car) => car.Id; diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs index bdbf4b2ea..0a3218eda 100644 --- a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs @@ -1,10 +1,10 @@ -using CarRental.Domain; +using CarRental.Domain.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; -public class ClientRepository(DataSeed data) : BaseRepository(data.Clients) +public class ClientRepository(DataSeed data) : BaseRepository(data.Clients) { protected override uint GetEntityId(Client client) => client.Id; diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs index f6e9e02c2..d92d2246c 100644 --- a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -1,10 +1,10 @@ -using CarRental.Domain; +using CarRental.Domain.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; -public class RentRepository(DataSeed data) : BaseRepository(data.Rents) +public class RentRepository(DataSeed data) : BaseRepository(data.Rents) { protected override uint GetEntityId(Rent rent) => rent.Id; From 758e4b4b7cd6daef4bc1721402000d925df50b7e Mon Sep 17 00:00:00 2001 From: Amitroki Date: Sat, 20 Dec 2025 00:01:07 +0400 Subject: [PATCH 12/37] modified the structure of solution the interfaces have been moved to the CarRental.Application.Interfaces folder, and folders have been created to contain DTO entities in CarRental.Application.Contracts --- .../CarRental.Application.Contracts.csproj | 9 --------- CarRental.Application.Contracts/Class1.cs | 6 ------ .../Interfaces/IAnalyticsService.cs | 2 +- .../Interfaces/IApplicationService.cs | 2 +- 4 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 CarRental.Application.Contracts/CarRental.Application.Contracts.csproj delete mode 100644 CarRental.Application.Contracts/Class1.cs rename {CarRental.Application.Contracts => CarRental.Application}/Interfaces/IAnalyticsService.cs (88%) rename {CarRental.Application.Contracts => CarRental.Application}/Interfaces/IApplicationService.cs (86%) diff --git a/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj deleted file mode 100644 index fa71b7ae6..000000000 --- a/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/CarRental.Application.Contracts/Class1.cs b/CarRental.Application.Contracts/Class1.cs deleted file mode 100644 index 750d91393..000000000 --- a/CarRental.Application.Contracts/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CarRental.Application.Contracts; - -public class Class1 -{ - -} diff --git a/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs similarity index 88% rename from CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs rename to CarRental.Application/Interfaces/IAnalyticsService.cs index 06b80a4c4..291613b7e 100644 --- a/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -1,4 +1,4 @@ -namespace CarRental.Application.Contracts; +namespace CarRental.Application.Interfaces; public interface IAnalyticsService { diff --git a/CarRental.Application.Contracts/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs similarity index 86% rename from CarRental.Application.Contracts/Interfaces/IApplicationService.cs rename to CarRental.Application/Interfaces/IApplicationService.cs index dae44e5d7..870ef7bc7 100644 --- a/CarRental.Application.Contracts/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -1,4 +1,4 @@ -namespace CarRental.Application.Contracts.Interfaces; +namespace CarRental.Application.Interfaces; public interface IApplicationService where TDto : class From 7f28c55485dea71f77469e503acb8bd556fb64b7 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Sun, 21 Dec 2025 22:14:56 +0400 Subject: [PATCH 13/37] added DTO's for exchange information about cars, class for correct mapping and service class for car --- .../CarRental.Application.csproj | 13 +++-- .../Contracts/Car/CarCreateUpdate.cs | 3 + CarRental.Application/Contracts/Car/CarDto.cs | 3 + .../Mapping/MappingConfig.cs | 17 ++++++ .../Services/CarService/CarService.cs | 58 +++++++++++++++++++ 5 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 CarRental.Application/Contracts/Car/CarCreateUpdate.cs create mode 100644 CarRental.Application/Contracts/Car/CarDto.cs create mode 100644 CarRental.Application/Mapping/MappingConfig.cs create mode 100644 CarRental.Application/Services/CarService/CarService.cs diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index 74d4d05a8..36331926b 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -1,14 +1,19 @@  - + - + - + + + + + + net8.0 enable enable - + diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdate.cs b/CarRental.Application/Contracts/Car/CarCreateUpdate.cs new file mode 100644 index 000000000..bc08c0c6f --- /dev/null +++ b/CarRental.Application/Contracts/Car/CarCreateUpdate.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts; + +public record CarCreateUpdateDto(string NumberPlate, string Colour, uint ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs new file mode 100644 index 000000000..155161edc --- /dev/null +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts; + +public record CarDto(uint Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs new file mode 100644 index 000000000..72e090d14 --- /dev/null +++ b/CarRental.Application/Mapping/MappingConfig.cs @@ -0,0 +1,17 @@ +using Mapster; +using CarRental.Application.Contracts; +using CarRental.Domain.DataModels; + +namespace CarRental.Application.Mapping; + +public static class MappingConfig +{ + public static void Configure() + { + TypeAdapterConfig.NewConfig() + .Map(dest => dest.ModelName, src => src.ModelGeneration.Model.Name); + + TypeAdapterConfig.NewConfig() + .Ignore(dest => dest.ModelGeneration); + } +} \ No newline at end of file diff --git a/CarRental.Application/Services/CarService/CarService.cs b/CarRental.Application/Services/CarService/CarService.cs new file mode 100644 index 000000000..5bd93d7fb --- /dev/null +++ b/CarRental.Application/Services/CarService/CarService.cs @@ -0,0 +1,58 @@ +using MapsterMapper; +using CarRental.Application.Contracts; +using CarRental.Application.Contracts.Interfaces; +using CarRental.Domain.DataModels; +using CarRental.Infrastructure.InMemoryRepository; + +namespace CarRental.Application.Services; + +public class CarService : IApplicationService +{ + private readonly CarRepository _carRepo; + private readonly CarModelGenerationRepository _modelRepo; + private readonly IMapper _mapper; + + public CarService(CarRepository carRepo, CarModelGenerationRepository modelRepo, IMapper mapper) + { + _carRepo = carRepo; + _modelRepo = modelRepo; + _mapper = mapper; + } + + public CarDto Create(CarCreateUpdateDto dto) + { + var car = _mapper.Map(dto); + + car.ModelGeneration = _modelRepo.Read(dto.ModelGenerationId) + ?? throw new Exception("Model not found"); + + var newId = _carRepo.Create(car); + + return _mapper.Map(_carRepo.Read(newId)); + } + + public List ReadAll() + { + var cars = _carRepo.ReadAll(); + return _mapper.Map>(cars); + } + + public CarDto? Read(uint id) + { + var car = _carRepo.Read(id); + return car == null ? null : _mapper.Map(car); + } + + public CarDto Update(CarCreateUpdateDto dto, uint id) + { + var car = _mapper.Map(dto); + car.Id = id; + car.ModelGeneration = _modelRepo.Read(dto.ModelGenerationId) + ?? throw new Exception("Model not found"); + + _carRepo.Update(car, id); + return _mapper.Map(car); + } + + public bool Delete(uint id) => _carRepo.Delete(id); +} \ No newline at end of file From d02573cfcf72b6639a0e78f0f11fa908b1c97d89 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 22 Dec 2025 00:25:31 +0400 Subject: [PATCH 14/37] fixed links between projects, fixed names of imports, fixed type parameters for BaseRepository, IBaseRepository and related files, fixed name of generation variable in DataSeed file, fixed imports in repository classes, added project for impementation Web API --- CarRental.Api/CarRental.Api.csproj | 19 +++++ CarRental.Api/Controllers/CarControllers.cs | 74 +++++++++++++++++++ CarRental.Api/Program.cs | 42 +++++++++++ CarRental.Api/Properties/launchSettings.json | 41 ++++++++++ CarRental.Api/appsettings.Development.json | 8 ++ CarRental.Api/appsettings.json | 9 +++ .../CarRental.Application.csproj | 2 +- CarRental.Application/Class1.cs | 6 -- .../Contracts/Car/CarCreateUpdate.cs | 2 +- CarRental.Application/Contracts/Car/CarDto.cs | 2 +- .../Interfaces/IAnalyticsService.cs | 12 +-- .../Interfaces/IApplicationService.cs | 9 +-- .../Mapping/MappingConfig.cs | 2 +- .../Services/CarService/CarService.cs | 8 +- CarRental.Domain/DataSeed/DataSeed.cs | 40 +++++----- CarRental.Domain/Interfaces/BaseRepository.cs | 4 +- .../Interfaces/IBaseRepository.cs | 2 +- .../CarModelGenerationRepository.cs | 3 +- .../InMemoryRepository/CarModelRepository.cs | 2 +- .../InMemoryRepository/RentRepository.cs | 1 + CarRental.sln | 54 ++++++++++---- 21 files changed, 280 insertions(+), 62 deletions(-) create mode 100644 CarRental.Api/CarRental.Api.csproj create mode 100644 CarRental.Api/Controllers/CarControllers.cs create mode 100644 CarRental.Api/Program.cs create mode 100644 CarRental.Api/Properties/launchSettings.json create mode 100644 CarRental.Api/appsettings.Development.json create mode 100644 CarRental.Api/appsettings.json delete mode 100644 CarRental.Application/Class1.cs diff --git a/CarRental.Api/CarRental.Api.csproj b/CarRental.Api/CarRental.Api.csproj new file mode 100644 index 000000000..b561f917d --- /dev/null +++ b/CarRental.Api/CarRental.Api.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs new file mode 100644 index 000000000..aa149b85d --- /dev/null +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -0,0 +1,74 @@ +using CarRental.Application.Contracts.Car; +using CarRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CarsController : ControllerBase +{ + private readonly IApplicationService _carService; + + public CarsController(IApplicationService carService) + { + _carService = carService; + } + + [HttpGet] + public ActionResult> GetAll() + { + var cars = _carService.ReadAll(); + return Ok(cars); + } + + [HttpGet("{id}")] + public ActionResult GetById(uint id) + { + var car = _carService.Read(id); + if (car == null) + { + return NotFound($" ID {id} ."); + } + return Ok(car); + } + + [HttpPost] + public ActionResult Create([FromBody] CarCreateUpdateDto dto) + { + try + { + var createdCar = _carService.Create(dto); + return CreatedAtAction(nameof(GetById), new { id = createdCar.Id }, createdCar); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPut("{id}")] + public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) + { + try + { + var updatedCar = _carService.Update(dto, id); + return Ok(updatedCar); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpDelete("{id}")] + public ActionResult Delete(uint id) + { + var result = _carService.Delete(id); + if (!result) + { + return NotFound($" ID {id}."); + } + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs new file mode 100644 index 000000000..8b8fb7745 --- /dev/null +++ b/CarRental.Api/Program.cs @@ -0,0 +1,42 @@ +using CarRental.Domain.DataSeed; +using CarRental.Infrastructure.InMemoryRepository; +using CarRental.Application.Services.CarService; +using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Mapping; +using Mapster; +using MapsterMapper; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var config = new TypeAdapterConfig(); +MappingConfig.Configure(); +builder.Services.AddSingleton(config); +builder.Services.AddScoped(); + +builder.Services.AddScoped, CarService>(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseSwagger(); +app.UseSwaggerUI(); + + +//app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/CarRental.Api/Properties/launchSettings.json b/CarRental.Api/Properties/launchSettings.json new file mode 100644 index 000000000..6e8b0f447 --- /dev/null +++ b/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:39741", + "sslPort": 44397 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5175", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7277;http://localhost:5175", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CarRental.Api/appsettings.Development.json b/CarRental.Api/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/CarRental.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CarRental.Api/appsettings.json b/CarRental.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/CarRental.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index 36331926b..05b2b5a42 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -1,8 +1,8 @@  - + diff --git a/CarRental.Application/Class1.cs b/CarRental.Application/Class1.cs deleted file mode 100644 index 14b4f1ba3..000000000 --- a/CarRental.Application/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CarRental.Application; - -public class Class1 -{ - -} diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdate.cs b/CarRental.Application/Contracts/Car/CarCreateUpdate.cs index bc08c0c6f..18fa6c19b 100644 --- a/CarRental.Application/Contracts/Car/CarCreateUpdate.cs +++ b/CarRental.Application/Contracts/Car/CarCreateUpdate.cs @@ -1,3 +1,3 @@ -namespace CarRental.Application.Contracts; +namespace CarRental.Application.Contracts.Car; public record CarCreateUpdateDto(string NumberPlate, string Colour, uint ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs index 155161edc..979c01f1e 100644 --- a/CarRental.Application/Contracts/Car/CarDto.cs +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -1,3 +1,3 @@ -namespace CarRental.Application.Contracts; +namespace CarRental.Application.Contracts.Car; public record CarDto(uint Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs index 291613b7e..ec5a87403 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -1,14 +1,16 @@ +using CarRental.Application.Contracts.Car; + namespace CarRental.Application.Interfaces; public interface IAnalyticsService { - List ReadClientsByModelName(string modelName); + //public List ReadClientsByModelName(string modelName); - List ReadCarsInRent(DateTime atTime); + public List ReadCarsInRent(DateTime atTime); - List ReadTop5MostRentedCars(); + public List ReadTop5MostRentedCars(); - List ReadAllCarsWithRentalCount(); + //public List ReadAllCarsWithRentalCount(); - List ReadTop5ClientsByTotalAmount(); + //public List ReadTop5ClientsByTotalAmount(); } \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index 870ef7bc7..969710a77 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -1,17 +1,16 @@ namespace CarRental.Application.Interfaces; -public interface IApplicationService +public interface IApplicationService where TDto : class where TCreateUpdateDto : class - where TKey : struct { public TDto Create(TCreateUpdateDto dto); - public TDto? Read(TKey id); + public TDto? Read(uint id); public List ReadAll(); - public TDto Update(TCreateUpdateDto dto, TKey id); + public TDto Update(TCreateUpdateDto dto, uint id); - public bool Delete(TKey id); + public bool Delete(uint id); } \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs index 72e090d14..b005362b7 100644 --- a/CarRental.Application/Mapping/MappingConfig.cs +++ b/CarRental.Application/Mapping/MappingConfig.cs @@ -1,5 +1,5 @@ using Mapster; -using CarRental.Application.Contracts; +using CarRental.Application.Contracts.Car; using CarRental.Domain.DataModels; namespace CarRental.Application.Mapping; diff --git a/CarRental.Application/Services/CarService/CarService.cs b/CarRental.Application/Services/CarService/CarService.cs index 5bd93d7fb..1d9cf4b90 100644 --- a/CarRental.Application/Services/CarService/CarService.cs +++ b/CarRental.Application/Services/CarService/CarService.cs @@ -1,12 +1,12 @@ using MapsterMapper; -using CarRental.Application.Contracts; -using CarRental.Application.Contracts.Interfaces; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Infrastructure.InMemoryRepository; -namespace CarRental.Application.Services; +namespace CarRental.Application.Services.CarService; -public class CarService : IApplicationService +public class CarService : IApplicationService { private readonly CarRepository _carRepo; private readonly CarModelGenerationRepository _modelRepo; diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs index 46dbd178d..7c1478132 100644 --- a/CarRental.Domain/DataSeed/DataSeed.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -89,26 +89,26 @@ public DataSeed() Cars = new List { - new() { Id = 1, ModelGeneration = Generation[5], NumberPlate = "T890NO96", Colour = "Gray" }, - new() { Id = 2, ModelGeneration = Generation[14], NumberPlate = "A123BC77", Colour = "Black" }, - new() { Id = 3, ModelGeneration = Generation[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, - new() { Id = 4, ModelGeneration = Generation[19], NumberPlate = "D012HI80", Colour = "Blue" }, - new() { Id = 5, ModelGeneration = Generation[6], NumberPlate = "E345JK81", Colour = "Red" }, - new() { Id = 6, ModelGeneration = Generation[16], NumberPlate = "F678LM82", Colour = "Gray" }, - new() { Id = 7, ModelGeneration = Generation[7], NumberPlate = "G901NO83", Colour = "Green" }, - new() { Id = 8, ModelGeneration = Generation[13], NumberPlate = "H234PQ84", Colour = "Black" }, - new() { Id = 9, ModelGeneration = Generation[3], NumberPlate = "I567RS85", Colour = "White" }, - new() { Id = 10, ModelGeneration = Generation[18], NumberPlate = "J890TU86", Colour = "Silver" }, - new() { Id = 11, ModelGeneration = Generation[10], NumberPlate = "K123VW87", Colour = "Blue" }, - new() { Id = 12, ModelGeneration = Generation[11], NumberPlate = "L456XY88", Colour = "Red" }, - new() { Id = 13, ModelGeneration = Generation[8], NumberPlate = "R234JK94", Colour = "Blue" }, - new() { Id = 14, ModelGeneration = Generation[9], NumberPlate = "N012BC90", Colour = "White" }, - new() { Id = 15, ModelGeneration = Generation[1], NumberPlate = "Q901HI93", Colour = "Red" }, - new() { Id = 16, ModelGeneration = Generation[15], NumberPlate = "P678FG92", Colour = "Silver" }, - new() { Id = 17, ModelGeneration = Generation[2], NumberPlate = "O345DE91", Colour = "Black" }, - new() { Id = 18, ModelGeneration = Generation[17], NumberPlate = "S567LM95", Colour = "Green" }, - new() { Id = 19, ModelGeneration = Generation[4], NumberPlate = "C789FG79", Colour = "Silver" }, - new() { Id = 20, ModelGeneration = Generation[12], NumberPlate = "B456DE78", Colour = "White" } + new() { Id = 1, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" }, + new() { Id = 2, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" }, + new() { Id = 3, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, + new() { Id = 4, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" }, + new() { Id = 5, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" }, + new() { Id = 6, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" }, + new() { Id = 7, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" }, + new() { Id = 8, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" }, + new() { Id = 9, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" }, + new() { Id = 10, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" }, + new() { Id = 11, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" }, + new() { Id = 12, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" }, + new() { Id = 13, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" }, + new() { Id = 14, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" }, + new() { Id = 15, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" }, + new() { Id = 16, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" }, + new() { Id = 17, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" }, + new() { Id = 18, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" }, + new() { Id = 19, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" }, + new() { Id = 20, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" } }; Clients = new List diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index f3df82d77..3c451a82c 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -39,7 +39,7 @@ public virtual uint Create(TEntity entity) public virtual TEntity? Read(uint id) { - return _entities.FirstOrDefault(c => c.Id == id); + return _entities.FirstOrDefault(e => GetEntityId(e) == id); } public virtual List ReadAll() @@ -47,7 +47,7 @@ public virtual List ReadAll() return _entities.ToList(); } - public virtual void Update(TEntity entity) + public virtual void Update(TEntity entity, uint id) { var existing = Read(id); if (existing != null) diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs index 62cce7523..6e647b364 100644 --- a/CarRental.Domain/Interfaces/IBaseRepository.cs +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -1,6 +1,6 @@ namespace CarRental.Domain.Interfaces; -public interface IBaseRepository +public interface IBaseRepository where TEntity : class { uint Create(TEntity entity); diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs index d2232d4b5..d2fb5daa9 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -1,7 +1,8 @@ using CarRental.Domain.Interfaces; -using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.DataSeed; + namespace CarRental.Infrastructure.InMemoryRepository; public class CarModelGenerationRepository(DataSeed data) : BaseRepository(data.Generations) diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs index 336f7917a..efc5df1bd 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -1,5 +1,5 @@ using CarRental.Domain.Interfaces; -using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.DataSeed; namespace CarRental.Infrastructure.InMemoryRepository; diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs index d92d2246c..b31abc415 100644 --- a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -9,4 +9,5 @@ public class RentRepository(DataSeed data) : BaseRepository(data.Rents) protected override uint GetEntityId(Rent rent) => rent.Id; protected override void SetEntityId(Rent rent, uint id) => rent.Id = id; + } \ No newline at end of file diff --git a/CarRental.sln b/CarRental.sln index 39c86166d..ae77556f4 100644 --- a/CarRental.sln +++ b/CarRental.sln @@ -7,7 +7,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Domain", "CarRent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Tests", "CarRental.Tests\CarRental.Tests.csproj", "{B253FF47-F3FD-4F60-934B-0A2649ACA810}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Api", "CarRental.Api\CarRental.Api.csproj", "{5E76316F-B8B5-4F6A-B49E-BB8C5333332D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Api", "CarRental.Api\CarRental.Api.csproj", "{7B16F96C-0A36-4D10-A10A-5AD14619865F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Application", "CarRental.Application\CarRental.Application.csproj", "{D1CFC635-6F68-727F-E951-9D087D3A3DBF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Infrastructure", "CarRental.Infrastructure\CarRental.Infrastructure.csproj", "{BC450137-ECDC-D0A6-9C70-887579DD4AD0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,18 +47,42 @@ Global {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x64.Build.0 = Release|Any CPU {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.ActiveCfg = Release|Any CPU {B253FF47-F3FD-4F60-934B-0A2649ACA810}.Release|x86.Build.0 = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x64.ActiveCfg = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x64.Build.0 = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x86.ActiveCfg = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Debug|x86.Build.0 = Debug|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|Any CPU.Build.0 = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x64.ActiveCfg = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x64.Build.0 = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x86.ActiveCfg = Release|Any CPU - {5E76316F-B8B5-4F6A-B49E-BB8C5333332D}.Release|x86.Build.0 = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|x64.Build.0 = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Debug|x86.Build.0 = Debug|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|Any CPU.Build.0 = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|x64.ActiveCfg = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|x64.Build.0 = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|x86.ActiveCfg = Release|Any CPU + {7B16F96C-0A36-4D10-A10A-5AD14619865F}.Release|x86.Build.0 = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|x64.Build.0 = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Debug|x86.Build.0 = Debug|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|Any CPU.Build.0 = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|x64.ActiveCfg = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|x64.Build.0 = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|x86.ActiveCfg = Release|Any CPU + {D1CFC635-6F68-727F-E951-9D087D3A3DBF}.Release|x86.Build.0 = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|x64.Build.0 = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Debug|x86.Build.0 = Debug|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|Any CPU.Build.0 = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x64.ActiveCfg = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x64.Build.0 = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x86.ActiveCfg = Release|Any CPU + {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 28990aab2cda8c825a4e4b3277168043d321e881 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 22 Dec 2025 17:29:11 +0400 Subject: [PATCH 15/37] added correct working services for client, rent, car model, car model generation classes, fixed some problems with returning types in several methods, added correct working controllers, added new mapping settings, now server works correct with car, client and rent --- CarRental.Api/Controllers/ClientController.cs | 39 +++++++++++++ CarRental.Api/Controllers/RentController.cs | 43 ++++++++++++++ CarRental.Api/Program.cs | 48 ++++++++------- ...rCreateUpdate.cs => CarCreateUpdateDto.cs} | 0 .../CarModel/CarModelCreateUpdateDto.cs | 3 + .../Contracts/CarModel/CarModelDto.cs | 3 + .../CarModelGenerationCreateUpdateDto.cs | 3 + .../CarModelGenerationDto.cs | 3 + .../Contracts/Client/ClientCreateUpdateDto.cs | 3 + .../Contracts/Client/ClientDto.cs | 3 + .../Contracts/Rent/RentCreateUpdateDto.cs | 3 + .../Contracts/Rent/RentDto.cs | 3 + .../Interfaces/IApplicationService.cs | 2 +- .../Mapping/MappingConfig.cs | 25 ++++++-- CarRental.Application/Services/CarService.cs | 51 ++++++++++++++++ .../Services/CarService/CarService.cs | 58 ------------------- .../Services/ClientService.cs | 34 +++++++++++ CarRental.Application/Services/RentService.cs | 58 +++++++++++++++++++ CarRental.Domain/Interfaces/BaseRepository.cs | 6 +- .../Interfaces/IBaseRepository.cs | 10 ++-- 20 files changed, 308 insertions(+), 90 deletions(-) create mode 100644 CarRental.Api/Controllers/ClientController.cs create mode 100644 CarRental.Api/Controllers/RentController.cs rename CarRental.Application/Contracts/Car/{CarCreateUpdate.cs => CarCreateUpdateDto.cs} (100%) create mode 100644 CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs create mode 100644 CarRental.Application/Contracts/CarModel/CarModelDto.cs create mode 100644 CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs create mode 100644 CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs create mode 100644 CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs create mode 100644 CarRental.Application/Contracts/Client/ClientDto.cs create mode 100644 CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs create mode 100644 CarRental.Application/Contracts/Rent/RentDto.cs create mode 100644 CarRental.Application/Services/CarService.cs delete mode 100644 CarRental.Application/Services/CarService/CarService.cs create mode 100644 CarRental.Application/Services/ClientService.cs create mode 100644 CarRental.Application/Services/RentService.cs diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs new file mode 100644 index 000000000..a2bff23b7 --- /dev/null +++ b/CarRental.Api/Controllers/ClientController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Client; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ClientController(IApplicationService clientService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() => Ok(clientService.ReadAll()); + + [HttpGet("{id}")] + public ActionResult Get(uint id) + { + var client = clientService.Read(id); + return client != null ? Ok(client) : NotFound(); + } + + [HttpPost] + public ActionResult Create(ClientCreateUpdateDto dto) + { + var result = clientService.Create(dto); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + [HttpPut("{id}")] + public ActionResult Update(uint id, ClientCreateUpdateDto dto) + { + return clientService.Update(dto, id) ? NoContent() : NotFound(); + } + + [HttpDelete("{id}")] + public ActionResult Delete(uint id) + { + return clientService.Delete(id) ? NoContent() : NotFound(); + } +} \ No newline at end of file diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs new file mode 100644 index 000000000..b0812b0d6 --- /dev/null +++ b/CarRental.Api/Controllers/RentController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Rent; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RentController(IApplicationService rentService) : ControllerBase +{ + [HttpGet] + public ActionResult> GetAll() => Ok(rentService.ReadAll()); + + [HttpGet("{id}")] + public ActionResult Get(uint id) + { + var rent = rentService.Read(id); + return rent != null ? Ok(rent) : NotFound(); + } + + [HttpPost] + public ActionResult Create(RentCreateUpdateDto dto) + { + var result = rentService.Create(dto); + if (result == null) + { + return BadRequest("Client or car is not exist"); + } + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + [HttpPut("{id}")] + public ActionResult Update(uint id, RentCreateUpdateDto dto) + { + return rentService.Update(dto, id) ? NoContent() : NotFound(); + } + + [HttpDelete("{id}")] + public ActionResult Delete(uint id) + { + return rentService.Delete(id) ? NoContent() : NotFound(); + } +} \ No newline at end of file diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 8b8fb7745..3edbfc47a 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -1,42 +1,50 @@ -using CarRental.Domain.DataSeed; -using CarRental.Infrastructure.InMemoryRepository; -using CarRental.Application.Services.CarService; -using CarRental.Application.Interfaces; using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Interfaces; using CarRental.Application.Mapping; +using CarRental.Application.Services; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Infrastructure.InMemoryRepository; using Mapster; using MapsterMapper; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +builder.Services.AddSingleton(TypeAdapterConfig.GlobalSettings); +builder.Services.AddScoped(); -builder.Services.AddControllers(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton, CarModelRepository>(); +builder.Services.AddSingleton, CarModelGenerationRepository>(); +builder.Services.AddSingleton, CarRepository>(); +builder.Services.AddSingleton, ClientRepository>(); +builder.Services.AddSingleton, RentRepository>(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped, CarService>(); +builder.Services.AddScoped, ClientService>(); +builder.Services.AddScoped, RentService>(); -var config = new TypeAdapterConfig(); MappingConfig.Configure(); -builder.Services.AddSingleton(config); -builder.Services.AddScoped(); -builder.Services.AddScoped, CarService>(); +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; + }); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); var app = builder.Build(); -// Configure the HTTP request pipeline. app.UseSwagger(); app.UseSwaggerUI(); - -//app.UseHttpsRedirection(); +app.UseHttpsRedirection(); // , app.UseAuthorization(); - app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdate.cs b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/Car/CarCreateUpdate.cs rename to CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs new file mode 100644 index 000000000..7089b2df5 --- /dev/null +++ b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.CarModel; + +public record CarModelCreateUpdateDto(string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelDto.cs b/CarRental.Application/Contracts/CarModel/CarModelDto.cs new file mode 100644 index 000000000..e93cba5b0 --- /dev/null +++ b/CarRental.Application/Contracts/CarModel/CarModelDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.CarModel; + +public record CarModelDto(uint Id, string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs new file mode 100644 index 000000000..575f69385 --- /dev/null +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.CarModelGeneration; + +public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs new file mode 100644 index 000000000..9e5bf08dc --- /dev/null +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.CarModelGeneration; + +public record CarModelGenerationDto(uint Id, int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs new file mode 100644 index 000000000..25342cd4d --- /dev/null +++ b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.Client; + +public record ClientCreateUpdateDto(string FirstName, string LastName, string PhoneNumber, string DriverLicense, DateOnly BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientDto.cs b/CarRental.Application/Contracts/Client/ClientDto.cs new file mode 100644 index 000000000..1b8c360d2 --- /dev/null +++ b/CarRental.Application/Contracts/Client/ClientDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.Client; + +public record ClientDto(uint Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs new file mode 100644 index 000000000..4b4e00f18 --- /dev/null +++ b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.Rent; + +public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, uint CarId, uint ClientId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application/Contracts/Rent/RentDto.cs new file mode 100644 index 000000000..3b9cf912f --- /dev/null +++ b/CarRental.Application/Contracts/Rent/RentDto.cs @@ -0,0 +1,3 @@ +namespace CarRental.Application.Contracts.Rent; + +public record RentDto(uint Id, DateTime StartDateTime, double Duration, uint CarId, string CarLicensePlate, uint ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index 969710a77..4bcd2572a 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -10,7 +10,7 @@ public interface IApplicationService public List ReadAll(); - public TDto Update(TCreateUpdateDto dto, uint id); + public bool Update(TCreateUpdateDto dto, uint id); public bool Delete(uint id); } \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs index b005362b7..7e55f5371 100644 --- a/CarRental.Application/Mapping/MappingConfig.cs +++ b/CarRental.Application/Mapping/MappingConfig.cs @@ -1,6 +1,11 @@ using Mapster; -using CarRental.Application.Contracts.Car; using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Contracts.CarModelGeneration; namespace CarRental.Application.Mapping; @@ -8,10 +13,22 @@ public static class MappingConfig { public static void Configure() { + TypeAdapterConfig.GlobalSettings.Default.PreserveReference(false); + + TypeAdapterConfig.NewConfig() + .MapToConstructor(true); + TypeAdapterConfig.NewConfig() - .Map(dest => dest.ModelName, src => src.ModelGeneration.Model.Name); - TypeAdapterConfig.NewConfig() - .Ignore(dest => dest.ModelGeneration); + .MapToConstructor(true) + .Map(dest => dest.ModelName, src => src.ModelGeneration!.Model!.Name); + + TypeAdapterConfig.NewConfig() + .MapToConstructor(true) + .Map(dest => dest.CarId, src => src.Car.Id) + .Map(dest => dest.ClientId, src => src.Client.Id) + .Map(dest => dest.ClientLastName, src => src.Client != null? src.Client.LastName: "Client is deleted") + .Map(dest => dest.CarLicensePlate, src => src.Car != null? src.Car.NumberPlate: "Car is deleted") + .Map(dest => dest.TotalCost, src => src.Car != null && src.Car.ModelGeneration != null? (decimal)src.Duration * src.Car.ModelGeneration.HourCost: 0); } } \ No newline at end of file diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs new file mode 100644 index 000000000..7b1318c37 --- /dev/null +++ b/CarRental.Application/Services/CarService.cs @@ -0,0 +1,51 @@ +using Mapster; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Interfaces; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Application.Services; +public class CarService( + IBaseRepository repository, + IBaseRepository generationRepository) + : IApplicationService +{ + public List ReadAll() => + repository.ReadAll().Select(e => e.Adapt()).ToList(); + + public CarDto? Read(uint id) => + repository.Read(id)?.Adapt(); + + public CarDto Create(CarCreateUpdateDto dto) + { + var entity = dto.Adapt(); + var fullGeneration = generationRepository.Read(dto.ModelGenerationId); + + if (fullGeneration == null) + throw new Exception("Generation not found"); + entity.ModelGeneration = fullGeneration; + + var id = repository.Create(entity); + var savedEntity = repository.Read(id); + + return savedEntity!.Adapt(); + } + + public bool Update(CarCreateUpdateDto dto, uint id) + { + var existing = repository.Read(id); + if (existing is null) return false; + + dto.Adapt(existing); + var fullGeneration = generationRepository.Read(dto.ModelGenerationId); + if (fullGeneration != null) + { + existing.ModelGeneration = fullGeneration; + } + + return repository.Update(existing, id); + } + + public bool Delete(uint id) => repository.Delete(id); +} \ No newline at end of file diff --git a/CarRental.Application/Services/CarService/CarService.cs b/CarRental.Application/Services/CarService/CarService.cs deleted file mode 100644 index 1d9cf4b90..000000000 --- a/CarRental.Application/Services/CarService/CarService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using MapsterMapper; -using CarRental.Application.Contracts.Car; -using CarRental.Application.Interfaces; -using CarRental.Domain.DataModels; -using CarRental.Infrastructure.InMemoryRepository; - -namespace CarRental.Application.Services.CarService; - -public class CarService : IApplicationService -{ - private readonly CarRepository _carRepo; - private readonly CarModelGenerationRepository _modelRepo; - private readonly IMapper _mapper; - - public CarService(CarRepository carRepo, CarModelGenerationRepository modelRepo, IMapper mapper) - { - _carRepo = carRepo; - _modelRepo = modelRepo; - _mapper = mapper; - } - - public CarDto Create(CarCreateUpdateDto dto) - { - var car = _mapper.Map(dto); - - car.ModelGeneration = _modelRepo.Read(dto.ModelGenerationId) - ?? throw new Exception("Model not found"); - - var newId = _carRepo.Create(car); - - return _mapper.Map(_carRepo.Read(newId)); - } - - public List ReadAll() - { - var cars = _carRepo.ReadAll(); - return _mapper.Map>(cars); - } - - public CarDto? Read(uint id) - { - var car = _carRepo.Read(id); - return car == null ? null : _mapper.Map(car); - } - - public CarDto Update(CarCreateUpdateDto dto, uint id) - { - var car = _mapper.Map(dto); - car.Id = id; - car.ModelGeneration = _modelRepo.Read(dto.ModelGenerationId) - ?? throw new Exception("Model not found"); - - _carRepo.Update(car, id); - return _mapper.Map(car); - } - - public bool Delete(uint id) => _carRepo.Delete(id); -} \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs new file mode 100644 index 000000000..74d4ef09a --- /dev/null +++ b/CarRental.Application/Services/ClientService.cs @@ -0,0 +1,34 @@ +using Mapster; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Interfaces; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; + +namespace CarRental.Application.Services; + +public class ClientService(IBaseRepository repository) : IApplicationService +{ + public List ReadAll() => + repository.ReadAll().Select(e => e.Adapt()).ToList(); + + public ClientDto? Read(uint id) => + repository.Read(id)?.Adapt(); + + public ClientDto Create(ClientCreateUpdateDto dto) + { + var entity = dto.Adapt(); + var id = repository.Create(entity); + var savedEntity = repository.Read(id); + return savedEntity!.Adapt(); + } + + public bool Update(ClientCreateUpdateDto dto, uint id) + { + var existing = repository.Read(id); + if (existing is null) return false; + dto.Adapt(existing); + return repository.Update(existing, id); + } + + public bool Delete(uint id) => repository.Delete(id); +} \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs new file mode 100644 index 000000000..d97521d14 --- /dev/null +++ b/CarRental.Application/Services/RentService.cs @@ -0,0 +1,58 @@ +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Interfaces; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure.InMemoryRepository; +using Mapster; + +namespace CarRental.Application.Services; +public class RentService( + IBaseRepository repository, + IBaseRepository carRepository, + IBaseRepository clientRepository) + : IApplicationService +{ + public List ReadAll() + { + var rents = repository.ReadAll(); + foreach (var rent in rents) + { + if (clientRepository.Read(rent.Client.Id) == null) + { + rent.Client = null; + } + } + return rents.Select(r => r.Adapt()).ToList(); + } + + public RentDto? Read(uint id) => + repository.Read(id)?.Adapt(); + + public RentDto? Create(RentCreateUpdateDto dto) + { + var car = carRepository.Read(dto.CarId); + var client = clientRepository.Read(dto.ClientId); + if (car == null || client == null) + { + return null; + } + var entity = dto.Adapt(); + entity.Car = car; + entity.Client = client; + + var id = repository.Create(entity); + var savedEntity = repository.Read(id); + + return savedEntity!.Adapt(); + } + + public bool Update(RentCreateUpdateDto dto, uint id) + { + var existing = repository.Read(id); + if (existing is null) return false; + dto.Adapt(existing); + return repository.Update(existing, id); + } + + public bool Delete(uint id) => repository.Delete(id); +} \ No newline at end of file diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index 3c451a82c..1333a2087 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -30,7 +30,7 @@ public virtual uint Create(TEntity entity) { throw new ArgumentNullException(nameof(entity)); } - uint currentId = _nextId; + var currentId = _nextId; SetEntityId(entity, currentId); _entities.Add(entity); _nextId++; @@ -47,7 +47,7 @@ public virtual List ReadAll() return _entities.ToList(); } - public virtual void Update(TEntity entity, uint id) + public virtual bool Update(TEntity entity, uint id) { var existing = Read(id); if (existing != null) @@ -55,7 +55,9 @@ public virtual void Update(TEntity entity, uint id) var index = _entities.IndexOf(existing); SetEntityId(entity, id); _entities[index] = entity; + return true; } + return false; } public virtual bool Delete(uint id) diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs index 6e647b364..ccf5c60ed 100644 --- a/CarRental.Domain/Interfaces/IBaseRepository.cs +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -3,9 +3,9 @@ namespace CarRental.Domain.Interfaces; public interface IBaseRepository where TEntity : class { - uint Create(TEntity entity); - TEntity? Read(uint id); - List ReadAll(); - void Update(TEntity entity, uint id); - bool Delete(uint id); + public uint Create(TEntity entity); + public TEntity? Read(uint id); + public List ReadAll(); + public bool Update(TEntity entity, uint id); + public bool Delete(uint id); } \ No newline at end of file From fb254012e85221a496f660b4ea81c51d20395714 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 22 Dec 2025 18:18:23 +0400 Subject: [PATCH 16/37] added all essential files for analytics methods (DTOs, Service, Controller) and some minor changes in rent service --- .../Controllers/AnalyticsController.cs | 44 ++++++++++ CarRental.Api/Program.cs | 2 + .../Contracts/AnalyticsDtos.cs | 23 ++++++ .../Interfaces/IAnalyticsService.cs | 12 +-- .../Services/AnalyticsService.cs | 82 +++++++++++++++++++ CarRental.Application/Services/RentService.cs | 5 +- 6 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 CarRental.Api/Controllers/AnalyticsController.cs create mode 100644 CarRental.Application/Contracts/AnalyticsDtos.cs create mode 100644 CarRental.Application/Services/AnalyticsService.cs diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..72e3514f6 --- /dev/null +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -0,0 +1,44 @@ +using CarRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase +{ + [HttpGet("clients-by-model")] + public IActionResult GetClientsByModel([FromQuery] string modelName) + { + var result = analyticsService.ReadClientsByModelName(modelName); + return Ok(result); + } + + [HttpGet("cars-in-rent")] + public IActionResult GetCarsInRent([FromQuery] DateTime atTime) + { + var result = analyticsService.ReadCarsInRent(atTime); + return Ok(result); + } + + [HttpGet("top-5-rented-cars")] + public IActionResult GetTop5Cars() + { + var result = analyticsService.ReadTop5MostRentedCars(); + return Ok(result); + } + + [HttpGet("all-cars-with-rental-count")] + public IActionResult GetAllCarsWithCount() + { + var result = analyticsService.ReadAllCarsWithRentalCount(); + return Ok(result); + } + + [HttpGet("top-5-clients-by-money")] + public IActionResult GetTop5Clients() + { + var result = analyticsService.ReadTop5ClientsByTotalAmount(); + return Ok(result); + } +} \ No newline at end of file diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 3edbfc47a..54bb42da7 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -27,6 +27,8 @@ builder.Services.AddScoped, ClientService>(); builder.Services.AddScoped, RentService>(); +builder.Services.AddScoped(); + MappingConfig.Configure(); builder.Services.AddControllers() diff --git a/CarRental.Application/Contracts/AnalyticsDtos.cs b/CarRental.Application/Contracts/AnalyticsDtos.cs new file mode 100644 index 000000000..e46262eb4 --- /dev/null +++ b/CarRental.Application/Contracts/AnalyticsDtos.cs @@ -0,0 +1,23 @@ +namespace CarRental.Application.Contracts; + +public record CarWithRentalCountDto( + uint Id, + string ModelName, + string NumberPlate, + int RentalCount +); + +public record ClientWithTotalAmountDto( + uint Id, + string FullName, + decimal TotalSpentAmount, + int TotalRentsCount +); + +public record CarInRentDto( + uint CarId, + string ModelName, + string NumberPlate, + DateTime RentStartDate, + int DurationHours +); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs index ec5a87403..e5c4ba353 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -1,16 +1,18 @@ using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts; namespace CarRental.Application.Interfaces; public interface IAnalyticsService { - //public List ReadClientsByModelName(string modelName); + public List ReadClientsByModelName(string modelName); - public List ReadCarsInRent(DateTime atTime); + public List ReadCarsInRent(DateTime atTime); - public List ReadTop5MostRentedCars(); + public List ReadTop5MostRentedCars(); - //public List ReadAllCarsWithRentalCount(); + public List ReadAllCarsWithRentalCount(); - //public List ReadTop5ClientsByTotalAmount(); + public List ReadTop5ClientsByTotalAmount(); } \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs new file mode 100644 index 000000000..e670afca2 --- /dev/null +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -0,0 +1,82 @@ +using CarRental.Application.Contracts; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Interfaces; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using Mapster; + +namespace CarRental.Application.Services; + +public class AnalyticsService( + IBaseRepository rentRepository, + IBaseRepository carRepository) : IAnalyticsService +{ + public List ReadClientsByModelName(string modelName) + { + return rentRepository.ReadAll() + .Where(r => r.Car.ModelGeneration!.Model!.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + .Select(r => r.Client.Adapt()) + .DistinctBy(c => c.Id) + .ToList(); + } + public List ReadTop5MostRentedCars() + { + return rentRepository.ReadAll() + .GroupBy(r => r.Car.Id) + .Select(g => new CarWithRentalCountDto( + g.First().Car.Id, + g.First().Car.ModelGeneration?.Model!.Name ?? "Unknown", + g.First().Car.NumberPlate, + g.Count() + )) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToList(); + } + public List ReadCarsInRent(DateTime atTime) + { + return rentRepository.ReadAll() + .Where(r => r.StartDateTime <= atTime && r.StartDateTime.AddHours(r.Duration) >= atTime) + .Select(r => new CarInRentDto( + r.Car.Id, + r.Car.ModelGeneration?.Model!.Name ?? "Unknown", + r.Car.NumberPlate, + r.StartDateTime, + (int)r.Duration + )) + .ToList(); + } + public List ReadAllCarsWithRentalCount() + { + var allRents = rentRepository.ReadAll(); + + return carRepository.ReadAll() + .Select(car => new CarWithRentalCountDto( + car.Id, + car.ModelGeneration?.Model!.Name ?? "Unknown", + car.NumberPlate, + allRents.Count(r => r.Car.Id == car.Id) + )) + .ToList(); + } + public List ReadTop5ClientsByTotalAmount() + { + return rentRepository.ReadAll() + .GroupBy(r => r.Client.Id) + .Select(g => { + var client = g.First().Client; + var totalAmount = g.Sum(r => (decimal)r.Duration * (r.Car.ModelGeneration?.HourCost ?? 0)); + return new ClientWithTotalAmountDto( + client.Id, + $"{client.LastName} {client.FirstName}", + totalAmount, + g.Count() + ); + }) + .OrderByDescending(x => x.TotalSpentAmount) + .Take(5) + .ToList(); + } +} \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index d97521d14..76cd9e3a8 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -2,7 +2,6 @@ using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure.InMemoryRepository; using Mapster; namespace CarRental.Application.Services; @@ -17,9 +16,9 @@ public List ReadAll() var rents = repository.ReadAll(); foreach (var rent in rents) { - if (clientRepository.Read(rent.Client.Id) == null) + if (clientRepository.Read(rent.Client!.Id) == null) { - rent.Client = null; + rent.Client = null!; } } return rents.Select(r => r.Adapt()).ToList(); From 440ae7ff25366baaacd082f4ceefb68dbe80ee0a Mon Sep 17 00:00:00 2001 From: Amitroki Date: Mon, 22 Dec 2025 21:08:03 +0400 Subject: [PATCH 17/37] added summaries for all existing essential files and changed constructor for cars in controllers to primary --- .../Controllers/AnalyticsController.cs | 20 +++++++++ CarRental.Api/Controllers/CarControllers.cs | 42 +++++++++++++------ CarRental.Api/Controllers/ClientController.cs | 23 ++++++++++ CarRental.Api/Controllers/RentController.cs | 23 ++++++++++ CarRental.Api/Program.cs | 9 ++-- .../Contracts/AnalyticsDtos.cs | 22 ++++++++++ .../Contracts/Car/CarCreateUpdateDto.cs | 6 +++ CarRental.Application/Contracts/Car/CarDto.cs | 7 ++++ .../CarModel/CarModelCreateUpdateDto.cs | 8 ++++ .../Contracts/CarModel/CarModelDto.cs | 9 ++++ .../CarModelGenerationCreateUpdateDto.cs | 7 ++++ .../CarModelGenerationDto.cs | 8 ++++ .../Contracts/Client/ClientCreateUpdateDto.cs | 8 ++++ .../Contracts/Client/ClientDto.cs | 9 ++++ .../Contracts/Rent/RentCreateUpdateDto.cs | 7 ++++ .../Contracts/Rent/RentDto.cs | 11 +++++ .../Interfaces/IAnalyticsService.cs | 18 ++++++++ .../Interfaces/IApplicationService.cs | 20 +++++++++ .../Mapping/MappingConfig.cs | 11 ++++- .../Services/AnalyticsService.cs | 22 ++++++++++ CarRental.Application/Services/CarService.cs | 20 +++++++++ .../Services/ClientService.cs | 18 ++++++++ CarRental.Application/Services/RentService.cs | 20 +++++++++ CarRental.Domain/Interfaces/BaseRepository.cs | 36 +++++++++++++++- .../Interfaces/IBaseRepository.cs | 23 ++++++++++ .../CarModelGenerationRepository.cs | 11 ++++- .../InMemoryRepository/CarModelRepository.cs | 10 +++++ .../InMemoryRepository/CarRepository.cs | 10 +++++ .../InMemoryRepository/ClientRepository.cs | 10 +++++ .../InMemoryRepository/RentRepository.cs | 10 +++++ 30 files changed, 438 insertions(+), 20 deletions(-) diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index 72e3514f6..2d317c477 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -3,10 +3,17 @@ namespace CarRental.Api.Controllers; +/// +/// Provides specialized API endpoints for data analytics and business reporting +/// [ApiController] [Route("api/[controller]")] public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase { + /// + /// Retrieves a list of clients who have rented cars associated with a specific model name + /// + /// The name of the car model to filter by [HttpGet("clients-by-model")] public IActionResult GetClientsByModel([FromQuery] string modelName) { @@ -14,6 +21,10 @@ public IActionResult GetClientsByModel([FromQuery] string modelName) return Ok(result); } + /// + /// Returns details of cars that are currently on lease at the specified date and time + /// + /// The point in time to check for active rentals [HttpGet("cars-in-rent")] public IActionResult GetCarsInRent([FromQuery] DateTime atTime) { @@ -21,6 +32,9 @@ public IActionResult GetCarsInRent([FromQuery] DateTime atTime) return Ok(result); } + /// + /// Returns the top 5 most popular cars based on total rental frequency + /// [HttpGet("top-5-rented-cars")] public IActionResult GetTop5Cars() { @@ -28,6 +42,9 @@ public IActionResult GetTop5Cars() return Ok(result); } + /// + /// Provides a comprehensive list of all cars and how many times each has been rented + /// [HttpGet("all-cars-with-rental-count")] public IActionResult GetAllCarsWithCount() { @@ -35,6 +52,9 @@ public IActionResult GetAllCarsWithCount() return Ok(result); } + /// + /// Returns the top 5 clients who have contributed the most to total revenue + /// [HttpGet("top-5-clients-by-money")] public IActionResult GetTop5Clients() { diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index aa149b85d..b5dedb774 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -4,28 +4,31 @@ namespace CarRental.Api.Controllers; +/// +/// API controller for managing the car fleet (CRUD operations) +/// [ApiController] [Route("api/[controller]")] -public class CarsController : ControllerBase +public class CarsController(IApplicationService carService) : ControllerBase { - private readonly IApplicationService _carService; - - public CarsController(IApplicationService carService) - { - _carService = carService; - } - + /// + /// Retrieves a list of all cars available in the system + /// [HttpGet] public ActionResult> GetAll() { - var cars = _carService.ReadAll(); + var cars = carService.ReadAll(); return Ok(cars); } + /// + /// Retrieves details of a specific car by its identifier + /// + /// The unique identifier of the car [HttpGet("{id}")] public ActionResult GetById(uint id) { - var car = _carService.Read(id); + var car = carService.Read(id); if (car == null) { return NotFound($" ID {id} ."); @@ -33,12 +36,16 @@ public ActionResult GetById(uint id) return Ok(car); } + /// + /// Registers a new car in the fleet + /// + /// The data for the new car record [HttpPost] public ActionResult Create([FromBody] CarCreateUpdateDto dto) { try { - var createdCar = _carService.Create(dto); + var createdCar = carService.Create(dto); return CreatedAtAction(nameof(GetById), new { id = createdCar.Id }, createdCar); } catch (Exception ex) @@ -47,12 +54,17 @@ public ActionResult Create([FromBody] CarCreateUpdateDto dto) } } + /// + /// Updates an existing car's information + /// + /// The unique identifier of the car to update + /// The updated data [HttpPut("{id}")] public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) { try { - var updatedCar = _carService.Update(dto, id); + var updatedCar = carService.Update(dto, id); return Ok(updatedCar); } catch (Exception ex) @@ -61,10 +73,14 @@ public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) } } + /// + /// Removes a car from the system + /// + /// The unique identifier of the car to delete [HttpDelete("{id}")] public ActionResult Delete(uint id) { - var result = _carService.Delete(id); + var result = carService.Delete(id); if (!result) { return NotFound($" ID {id}."); diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index a2bff23b7..ee7da67ba 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -4,13 +4,23 @@ namespace CarRental.Api.Controllers; +/// +/// API controller for managing client records and personal data (CRUD operations) +/// [ApiController] [Route("api/[controller]")] public class ClientController(IApplicationService clientService) : ControllerBase { + /// + /// Retrieves a list of all registered clients + /// [HttpGet] public ActionResult> GetAll() => Ok(clientService.ReadAll()); + /// + /// Retrieves a specific client by their unique identifier + /// + /// The unique identifier of the client [HttpGet("{id}")] public ActionResult Get(uint id) { @@ -18,6 +28,10 @@ public ActionResult Get(uint id) return client != null ? Ok(client) : NotFound(); } + /// + /// Registers a new client and returns the created record + /// + /// The client information to create [HttpPost] public ActionResult Create(ClientCreateUpdateDto dto) { @@ -25,12 +39,21 @@ public ActionResult Create(ClientCreateUpdateDto dto) return CreatedAtAction(nameof(Get), new { id = result.Id }, result); } + /// + /// Updates an existing client's information + /// + /// The ID of the client to update + /// The updated client data [HttpPut("{id}")] public ActionResult Update(uint id, ClientCreateUpdateDto dto) { return clientService.Update(dto, id) ? NoContent() : NotFound(); } + /// + /// Removes a client from the system by their ID + /// + /// The unique identifier of the client to delete [HttpDelete("{id}")] public ActionResult Delete(uint id) { diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index b0812b0d6..f78aa553d 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -4,13 +4,23 @@ namespace CarRental.Api.Controllers; +/// +/// API controller for managing car rental agreements and lease transactions +/// [ApiController] [Route("api/[controller]")] public class RentController(IApplicationService rentService) : ControllerBase { + /// + /// Retrieves a list of all rental records, including calculated costs and linked entity names + /// [HttpGet] public ActionResult> GetAll() => Ok(rentService.ReadAll()); + /// + /// Retrieves a specific rental agreement by its identifier. + /// + /// The unique identifier of the rental record. [HttpGet("{id}")] public ActionResult Get(uint id) { @@ -18,6 +28,10 @@ public ActionResult Get(uint id) return rent != null ? Ok(rent) : NotFound(); } + /// + /// Creates a new rental agreement after verifying the existence of the client and car. + /// + /// The rental details, including CarId and ClientId. [HttpPost] public ActionResult Create(RentCreateUpdateDto dto) { @@ -29,12 +43,21 @@ public ActionResult Create(RentCreateUpdateDto dto) return CreatedAtAction(nameof(Get), new { id = result.Id }, result); } + /// + /// Updates the details of an existing rental agreement. + /// + /// The ID of the rental to update. + /// The updated rental data. [HttpPut("{id}")] public ActionResult Update(uint id, RentCreateUpdateDto dto) { return rentService.Update(dto, id) ? NoContent() : NotFound(); } + /// + /// Deletes a rental record from the system. + /// + /// The unique identifier of the rental to remove. [HttpDelete("{id}")] public ActionResult Delete(uint id) { diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 54bb42da7..37fc56ccd 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -41,10 +41,13 @@ var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI(); +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} -app.UseHttpsRedirection(); // , +app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); diff --git a/CarRental.Application/Contracts/AnalyticsDtos.cs b/CarRental.Application/Contracts/AnalyticsDtos.cs index e46262eb4..d328a8b81 100644 --- a/CarRental.Application/Contracts/AnalyticsDtos.cs +++ b/CarRental.Application/Contracts/AnalyticsDtos.cs @@ -1,5 +1,12 @@ namespace CarRental.Application.Contracts; +/// +/// Data transfer object for car statistics, including the total number of times it was rented. +/// +/// The unique identifier of the car. +/// The descriptive name of the car model. +/// The vehicle's license plate number. +/// Total number of rental agreements associated with this car. public record CarWithRentalCountDto( uint Id, string ModelName, @@ -7,6 +14,13 @@ public record CarWithRentalCountDto( int RentalCount ); +/// +/// Data transfer object for client financial statistics. +/// +/// The unique identifier of the client. +/// The concatenated full name of the client. +/// The sum of all rental costs paid by the client. +/// Total number of times the client has rented vehicles. public record ClientWithTotalAmountDto( uint Id, string FullName, @@ -14,6 +28,14 @@ public record ClientWithTotalAmountDto( int TotalRentsCount ); +/// +/// Data transfer object representing a car that is currently or was previously in an active rental state. +/// +/// The unique identifier of the car. +/// The descriptive name of the car model. +/// The vehicle's license plate number. +/// The exact start time of the rental period. +/// The length of the rental in hours. public record CarInRentDto( uint CarId, string ModelName, diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs index 18fa6c19b..ff9535b02 100644 --- a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs @@ -1,3 +1,9 @@ namespace CarRental.Application.Contracts.Car; +/// +/// Data transfer object for creating or updating a car record. +/// +/// The vehicle's license plate number. +/// The color of the car. +/// The unique identifier of the associated car model generation. public record CarCreateUpdateDto(string NumberPlate, string Colour, uint ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs index 979c01f1e..7725a9d1d 100644 --- a/CarRental.Application/Contracts/Car/CarDto.cs +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -1,3 +1,10 @@ namespace CarRental.Application.Contracts.Car; +/// +/// Data transfer object representing a car with its basic details for display. +/// +/// The unique identifier of the car. +/// The vehicle's license plate number. +/// The color of the car. +/// The descriptive name of the car model. public record CarDto(uint Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs index 7089b2df5..6c8c0137b 100644 --- a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs @@ -1,3 +1,11 @@ namespace CarRental.Application.Contracts.CarModel; +/// +/// Data transfer object for creating or updating a car model definition. +/// +/// The brand or specific model name. +/// The type of drivetrain (e.g., AWD). +/// The total passenger capacity. +/// The style of the vehicle body (e.g., Sedan, SUV). +/// The market segment or luxury class of the vehicle. public record CarModelCreateUpdateDto(string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelDto.cs b/CarRental.Application/Contracts/CarModel/CarModelDto.cs index e93cba5b0..9d2f8da93 100644 --- a/CarRental.Application/Contracts/CarModel/CarModelDto.cs +++ b/CarRental.Application/Contracts/CarModel/CarModelDto.cs @@ -1,3 +1,12 @@ namespace CarRental.Application.Contracts.CarModel; +/// +/// Data transfer object representing a car model with its technical specifications. +/// +/// The unique identifier of the car model. +/// The brand or specific model name. +/// The type of drivetrain (e.g., AWD, FWD, RWD). +/// The total passenger capacity. +/// The style of the vehicle body (e.g., Sedan, SUV). +/// The market segment or luxury class of the vehicle. public record CarModelDto(uint Id, string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs index 575f69385..7334a2add 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs @@ -1,3 +1,10 @@ namespace CarRental.Application.Contracts.CarModelGeneration; +/// +/// Data transfer object for creating or updating a specific car model generation. +/// +/// The manufacturing year of the generation. +/// The type of transmission (e.g., Manual, Automatic). +/// The rental cost per hour for this generation. +/// The unique identifier of the parent car model. public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs index 9e5bf08dc..83e5a99b5 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs @@ -1,3 +1,11 @@ namespace CarRental.Application.Contracts.CarModelGeneration; +/// +/// Data transfer object representing a specific car model generation with pricing and details. +/// +/// The unique identifier of the car model generation. +/// The manufacturing year of the generation. +/// The type of transmission used in this generation. +/// The rental cost per hour. +/// The identifier of the parent car model. public record CarModelGenerationDto(uint Id, int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs index 25342cd4d..48189a06c 100644 --- a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs @@ -1,3 +1,11 @@ namespace CarRental.Application.Contracts.Client; +/// +/// Data transfer object for creating or updating client information. +/// +/// The client's given name. +/// The client's family name. +/// The client's contact phone number. +/// The unique identifier of the client's driving license. +/// The client's date of birth. public record ClientCreateUpdateDto(string FirstName, string LastName, string PhoneNumber, string DriverLicense, DateOnly BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientDto.cs b/CarRental.Application/Contracts/Client/ClientDto.cs index 1b8c360d2..b2802925a 100644 --- a/CarRental.Application/Contracts/Client/ClientDto.cs +++ b/CarRental.Application/Contracts/Client/ClientDto.cs @@ -1,3 +1,12 @@ namespace CarRental.Application.Contracts.Client; +/// +/// Data transfer object representing client details for display and identification. +/// +/// The unique identifier of the client record. +/// The identification number of the client's driver license. +/// The client's family name. +/// The client's first name. +/// The client's middle name (optional). +/// The client's date of birth (optional). public record ClientDto(uint Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs index 4b4e00f18..bd2a6b5a7 100644 --- a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs @@ -1,3 +1,10 @@ namespace CarRental.Application.Contracts.Rent; +/// +/// Data transfer object for creating or updating a car rental agreement. +/// +/// The scheduled date and time for the rental to begin. +/// The length of the rental period in hours. +/// The unique identifier of the car to be rented. +/// The unique identifier of the client renting the car. public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, uint CarId, uint ClientId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application/Contracts/Rent/RentDto.cs index 3b9cf912f..36441d76c 100644 --- a/CarRental.Application/Contracts/Rent/RentDto.cs +++ b/CarRental.Application/Contracts/Rent/RentDto.cs @@ -1,3 +1,14 @@ namespace CarRental.Application.Contracts.Rent; +/// +/// Data transfer object representing a rental agreement with calculated details and linked entity info. +/// +/// The unique identifier of the rental record. +/// The date and time when the rental period starts. +/// The length of the rental in hours. +/// The unique identifier of the rented car. +/// The license plate of the rented car. +/// The unique identifier of the client. +/// The last name of the client. +/// The total calculated cost for the rental duration. public record RentDto(uint Id, DateTime StartDateTime, double Duration, uint CarId, string CarLicensePlate, uint ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs index e5c4ba353..14567b77f 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -4,15 +4,33 @@ namespace CarRental.Application.Interfaces; +/// +/// Defines methods for business intelligence and data analysis across cars, clients, and rentals. +/// public interface IAnalyticsService { + /// + /// Retrieves all clients who have rented a specific car model. + /// public List ReadClientsByModelName(string modelName); + /// + /// Lists all cars that are currently occupied at a specific point in time. + /// public List ReadCarsInRent(DateTime atTime); + /// + /// Returns the top 5 cars with the highest number of rental agreements. + /// public List ReadTop5MostRentedCars(); + /// + /// Returns a list of all cars along with their total rental frequency. + /// public List ReadAllCarsWithRentalCount(); + /// + /// Returns the top 5 clients who have spent the most money on rentals. + /// public List ReadTop5ClientsByTotalAmount(); } \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index 4bcd2572a..9893feb1a 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -1,16 +1,36 @@ namespace CarRental.Application.Interfaces; +/// +/// Defines a generic contract for application services handling mapping between entities and DTOs. +/// +/// The data transfer object used for output. +/// The data transfer object used for input operations. public interface IApplicationService where TDto : class where TCreateUpdateDto : class { + /// + /// Creates a new record from the provided input DTO and returns the resulting output DTO. + /// public TDto Create(TCreateUpdateDto dto); + /// + /// Retrieves a single record by its unique identifier, mapped to an output DTO. + /// public TDto? Read(uint id); + /// + /// Retrieves all records mapped to a list of output DTOs. + /// public List ReadAll(); + /// + /// Updates an existing record identified by the given ID using the input DTO data. + /// public bool Update(TCreateUpdateDto dto, uint id); + /// + /// Removes a record from the system by its unique identifier. + /// public bool Delete(uint id); } \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs index 7e55f5371..d6e803e84 100644 --- a/CarRental.Application/Mapping/MappingConfig.cs +++ b/CarRental.Application/Mapping/MappingConfig.cs @@ -9,20 +9,29 @@ namespace CarRental.Application.Mapping; +/// +/// Provides global configuration for object-to-object mapping using Mapster. +/// Defines rules for converting domain entities into data transfer objects. +/// public static class MappingConfig { + /// + /// Initializes and registers mapping configurations between domain models and DTOs. + /// public static void Configure() { TypeAdapterConfig.GlobalSettings.Default.PreserveReference(false); + // Client mapping TypeAdapterConfig.NewConfig() .MapToConstructor(true); + // Car mapping with flattened ModelName TypeAdapterConfig.NewConfig() - .MapToConstructor(true) .Map(dest => dest.ModelName, src => src.ModelGeneration!.Model!.Name); + // Rent mapping with complex logic for associated entities and costs TypeAdapterConfig.NewConfig() .MapToConstructor(true) .Map(dest => dest.CarId, src => src.Car.Id) diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index e670afca2..43e018cef 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -9,10 +9,16 @@ namespace CarRental.Application.Services; +/// +/// Implements business logic for data aggregation and rental statistics. +/// public class AnalyticsService( IBaseRepository rentRepository, IBaseRepository carRepository) : IAnalyticsService { + /// + /// Finds unique clients who rented cars of a specific model name. + /// public List ReadClientsByModelName(string modelName) { return rentRepository.ReadAll() @@ -21,6 +27,10 @@ public List ReadClientsByModelName(string modelName) .DistinctBy(c => c.Id) .ToList(); } + + /// + /// Identifies the top 5 most frequently rented cars. + /// public List ReadTop5MostRentedCars() { return rentRepository.ReadAll() @@ -35,6 +45,10 @@ public List ReadTop5MostRentedCars() .Take(5) .ToList(); } + + /// + /// Retrieves cars that were actively rented at a specific point in time. + /// public List ReadCarsInRent(DateTime atTime) { return rentRepository.ReadAll() @@ -48,6 +62,10 @@ public List ReadCarsInRent(DateTime atTime) )) .ToList(); } + + /// + /// Lists all cars and their total rental frequency. + /// public List ReadAllCarsWithRentalCount() { var allRents = rentRepository.ReadAll(); @@ -61,6 +79,10 @@ public List ReadAllCarsWithRentalCount() )) .ToList(); } + + /// + /// Identifies the top 5 clients by total revenue generated. + /// public List ReadTop5ClientsByTotalAmount() { return rentRepository.ReadAll() diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index 7b1318c37..ccb5813f1 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -6,17 +6,31 @@ using CarRental.Domain.InternalData.ComponentClasses; namespace CarRental.Application.Services; + +/// +/// Provides CRUD operations for car entities, including relationship management with car model generations. +/// public class CarService( IBaseRepository repository, IBaseRepository generationRepository) : IApplicationService { + /// + /// Retrieves all car records and maps them to DTOs. + /// public List ReadAll() => repository.ReadAll().Select(e => e.Adapt()).ToList(); + /// + /// Retrieves a specific car by its identifier. + /// public CarDto? Read(uint id) => repository.Read(id)?.Adapt(); + /// + /// Creates a new car record after validating the associated model generation. + /// + /// Thrown when the specified ModelGenerationId does not exist. public CarDto Create(CarCreateUpdateDto dto) { var entity = dto.Adapt(); @@ -32,6 +46,9 @@ public CarDto Create(CarCreateUpdateDto dto) return savedEntity!.Adapt(); } + /// + /// Updates an existing car's information and its relationship with a model generation. + /// public bool Update(CarCreateUpdateDto dto, uint id) { var existing = repository.Read(id); @@ -47,5 +64,8 @@ public bool Update(CarCreateUpdateDto dto, uint id) return repository.Update(existing, id); } + /// + /// Deletes a car record by its identifier. + /// public bool Delete(uint id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 74d4ef09a..66e14d6d4 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -6,14 +6,26 @@ namespace CarRental.Application.Services; +/// +/// Manages client-related operations, including registration and profile management. +/// public class ClientService(IBaseRepository repository) : IApplicationService { + /// + /// Retrieves a complete list of registered clients. + /// public List ReadAll() => repository.ReadAll().Select(e => e.Adapt()).ToList(); + /// + /// Finds a specific client by their unique identifier. + /// public ClientDto? Read(uint id) => repository.Read(id)?.Adapt(); + /// + /// Registers a new client in the system. + /// public ClientDto Create(ClientCreateUpdateDto dto) { var entity = dto.Adapt(); @@ -22,6 +34,9 @@ public ClientDto Create(ClientCreateUpdateDto dto) return savedEntity!.Adapt(); } + /// + /// Updates an existing client's personal and contact information. + /// public bool Update(ClientCreateUpdateDto dto, uint id) { var existing = repository.Read(id); @@ -30,5 +45,8 @@ public bool Update(ClientCreateUpdateDto dto, uint id) return repository.Update(existing, id); } + /// + /// Removes a client record from the database. + /// public bool Delete(uint id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 76cd9e3a8..23c3bce01 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -5,12 +5,19 @@ using Mapster; namespace CarRental.Application.Services; + +/// +/// Managing associations between cars, clients, and rental periods. +/// public class RentService( IBaseRepository repository, IBaseRepository carRepository, IBaseRepository clientRepository) : IApplicationService { + /// + /// Retrieves all rental records, performing safety checks for deleted clients to ensure data integrity during mapping. + /// public List ReadAll() { var rents = repository.ReadAll(); @@ -24,9 +31,16 @@ public List ReadAll() return rents.Select(r => r.Adapt()).ToList(); } + /// + /// Retrieves a specific rental agreement by its identifier. + /// public RentDto? Read(uint id) => repository.Read(id)?.Adapt(); + /// + /// Creates a new rental agreement after validating that both the requested car and client exist. + /// + /// The created rental DTO, or null if validation fails. public RentDto? Create(RentCreateUpdateDto dto) { var car = carRepository.Read(dto.CarId); @@ -45,6 +59,9 @@ public List ReadAll() return savedEntity!.Adapt(); } + /// + /// Updates an existing rental agreement's details. + /// public bool Update(RentCreateUpdateDto dto, uint id) { var existing = repository.Read(id); @@ -53,5 +70,8 @@ public bool Update(RentCreateUpdateDto dto, uint id) return repository.Update(existing, id); } + /// + /// Permanently removes a rental record from the system. + /// public bool Delete(uint id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index 1333a2087..84b1427b6 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -1,16 +1,33 @@ namespace CarRental.Domain.Interfaces; +/// +/// Provides a base implementation for in-memory CRUD operations. +/// +/// The type of the entity managed by the repository. public abstract class BaseRepository : IBaseRepository where TEntity : class { + /// + /// Private field for obtaining a unique identifier + /// to assign it to the next entity in the repository + /// + private uint _nextId; + private readonly List _entities; + /// + /// Gets the unique identifier from the entity. + /// protected abstract uint GetEntityId(TEntity entity); + /// + /// Sets the unique identifier for the entity + /// protected abstract void SetEntityId(TEntity entity, uint id); - private readonly List _entities; - + /// + /// Initializes the repository and determines the starting ID based on existing data. + /// protected BaseRepository(List? entities = null) { _entities = entities ?? new List(); @@ -24,6 +41,9 @@ protected BaseRepository(List? entities = null) } } + /// + /// Adds a new entity to the collection and assigns a unique ID. + /// public virtual uint Create(TEntity entity) { if (entity == null) @@ -37,16 +57,25 @@ public virtual uint Create(TEntity entity) return currentId; } + /// + /// Retrieves an entity by its unique identifier. + /// public virtual TEntity? Read(uint id) { return _entities.FirstOrDefault(e => GetEntityId(e) == id); } + /// + /// Returns all entities in the collection. + /// public virtual List ReadAll() { return _entities.ToList(); } + /// + /// Replaces an existing entity at the specified ID. + /// public virtual bool Update(TEntity entity, uint id) { var existing = Read(id); @@ -60,6 +89,9 @@ public virtual bool Update(TEntity entity, uint id) return false; } + /// + /// Removes an entity from the collection by its ID. + /// public virtual bool Delete(uint id) { var entity = Read(id); diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs index ccf5c60ed..c16764d24 100644 --- a/CarRental.Domain/Interfaces/IBaseRepository.cs +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -1,11 +1,34 @@ namespace CarRental.Domain.Interfaces; +/// +/// Defines the standard contract for a generic repository supporting CRUD operations. +/// +/// The type of the entity object. public interface IBaseRepository where TEntity : class { + /// + /// Adds a new entity to the collection and returns a unique ID. + /// public uint Create(TEntity entity); + + /// + /// Retrieves an entity by its unique identifier. + /// public TEntity? Read(uint id); + + /// + /// Returns all entities in the collection. + /// public List ReadAll(); + + /// + /// Replaces an existing entity at the specified ID. + /// public bool Update(TEntity entity, uint id); + + /// + /// Removes an entity from the collection by its ID. + /// public bool Delete(uint id); } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs index d2fb5daa9..6de588976 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -2,12 +2,21 @@ using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.DataSeed; - namespace CarRental.Infrastructure.InMemoryRepository; +/// +/// Repository for managing CarModelGeneration entities +/// Provides data access for vehicle model's generation (e.g., year) using the BaseRepository +/// public class CarModelGenerationRepository(DataSeed data) : BaseRepository(data.Generations) { + /// + /// Gets the unique identifier from the specified CarModelGeneration entity + /// protected override uint GetEntityId(CarModelGeneration generation) => generation.Id; + /// + /// Sets the unique identifier for the specified CarModelGeneration entity + /// protected override void SetEntityId(CarModelGeneration generation, uint id) => generation.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs index efc5df1bd..6c4f0274f 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -4,9 +4,19 @@ namespace CarRental.Infrastructure.InMemoryRepository; +/// +/// Repository for managing CarModel entities +/// Provides data access for vehicle models (e.g., model name) using the BaseRepository +/// public class CarModelRepository(DataSeed data) : BaseRepository(data.Models) { + /// + /// Gets the unique identifier from the specified CarModel entity + /// protected override uint GetEntityId(CarModel model) => model.Id; + /// + /// Sets the unique identifier for the specified CarModel entity + /// protected override void SetEntityId(CarModel model, uint id) => model.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs index 782e7eee6..3ffb1000c 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs @@ -4,9 +4,19 @@ namespace CarRental.Infrastructure.InMemoryRepository; +/// +/// Repository for the Car entity +/// Inherits BaseRepository for in-memory CRUD operations +/// public class CarRepository(DataSeed data) : BaseRepository(data.Cars) { + /// + /// Gets the unique identifier from the specified Car entity + /// protected override uint GetEntityId(Car car) => car.Id; + /// + /// Sets the unique identifier for the specified Car entity + /// protected override void SetEntityId(Car car, uint id) => car.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs index 0a3218eda..0092844d5 100644 --- a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs @@ -4,9 +4,19 @@ namespace CarRental.Infrastructure.InMemoryRepository; +/// +/// Repository for the Client entity +/// Inherits BaseRepository for in-memory CRUD operations +/// public class ClientRepository(DataSeed data) : BaseRepository(data.Clients) { + /// + /// Gets the unique identifier from the specified Client entity + /// protected override uint GetEntityId(Client client) => client.Id; + /// + /// Sets the unique identifier for the specified Client entity + /// protected override void SetEntityId(Client client, uint id) => client.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs index b31abc415..518698725 100644 --- a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -4,10 +4,20 @@ namespace CarRental.Infrastructure.InMemoryRepository; +/// +/// Repository for the Rent entity +/// Inherits BaseRepository for in-memory CRUD operations +/// public class RentRepository(DataSeed data) : BaseRepository(data.Rents) { + /// + /// Gets the unique identifier from the specified Rent entity + /// protected override uint GetEntityId(Rent rent) => rent.Id; + /// + /// Sets the unique identifier for the specified Rent entity + /// protected override void SetEntityId(Rent rent, uint id) => rent.Id = id; } \ No newline at end of file From 3505b49d7c87472193662cc1256d104255dab9ed Mon Sep 17 00:00:00 2001 From: Amitroki Date: Tue, 23 Dec 2025 01:55:16 +0400 Subject: [PATCH 18/37] added CarRental.ServiceDefaults and CarRental.AppHost projects, added essential links between projects --- CarRental.Api/CarRental.Api.csproj | 2 + CarRental.Api/Program.cs | 15 ++- CarRental.AppHost/CarRental.AppHost.csproj | 21 ++++ CarRental.AppHost/Program.cs | 10 ++ .../Properties/launchSettings.json | 29 +++++ .../appsettings.Development.json | 8 ++ CarRental.AppHost/appsettings.json | 9 ++ .../CarRental.Infrastructure.csproj | 4 + .../CarRental.ServiceDefaults.csproj | 22 ++++ CarRental.ServiceDefaults/Extensions.cs | 118 ++++++++++++++++++ CarRental.sln | 28 +++++ 11 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 CarRental.AppHost/CarRental.AppHost.csproj create mode 100644 CarRental.AppHost/Program.cs create mode 100644 CarRental.AppHost/Properties/launchSettings.json create mode 100644 CarRental.AppHost/appsettings.Development.json create mode 100644 CarRental.AppHost/appsettings.json create mode 100644 CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj create mode 100644 CarRental.ServiceDefaults/Extensions.cs diff --git a/CarRental.Api/CarRental.Api.csproj b/CarRental.Api/CarRental.Api.csproj index b561f917d..812ebe8bf 100644 --- a/CarRental.Api/CarRental.Api.csproj +++ b/CarRental.Api/CarRental.Api.csproj @@ -7,6 +7,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 37fc56ccd..6ef541f17 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -8,20 +8,23 @@ using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Infrastructure.InMemoryRepository; +using CarRental.ServiceDefaults; using Mapster; using MapsterMapper; var builder = WebApplication.CreateBuilder(args); +builder.AddMongoDBClient("CarRentalDb"); +builder.AddServiceDefaults(); builder.Services.AddSingleton(TypeAdapterConfig.GlobalSettings); builder.Services.AddScoped(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton, CarModelRepository>(); -builder.Services.AddSingleton, CarModelGenerationRepository>(); -builder.Services.AddSingleton, CarRepository>(); -builder.Services.AddSingleton, ClientRepository>(); -builder.Services.AddSingleton, RentRepository>(); +builder.Services.AddScoped(); +builder.Services.AddScoped, CarModelRepository>(); +builder.Services.AddScoped, CarModelGenerationRepository>(); +builder.Services.AddScoped, CarRepository>(); +builder.Services.AddScoped, ClientRepository>(); +builder.Services.AddScoped, RentRepository>(); builder.Services.AddScoped, CarService>(); builder.Services.AddScoped, ClientService>(); diff --git a/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental.AppHost/CarRental.AppHost.csproj new file mode 100644 index 000000000..30e9af443 --- /dev/null +++ b/CarRental.AppHost/CarRental.AppHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + 54701a95-76ef-4922-a1a8-8f3a20203073 + + + + + + + + + + + + diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs new file mode 100644 index 000000000..d5d748312 --- /dev/null +++ b/CarRental.AppHost/Program.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var mongodb = builder.AddMongoDB("mongodb"); + +var carDb = mongodb.AddDatabase("CarRentalDb"); + +builder.AddProject("carrental-api") + .WithReference(carDb); + +builder.Build().Run(); diff --git a/CarRental.AppHost/Properties/launchSettings.json b/CarRental.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..3f58aa95b --- /dev/null +++ b/CarRental.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17106;http://localhost:15095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21035", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22055" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19259", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20117" + } + } + } +} diff --git a/CarRental.AppHost/appsettings.Development.json b/CarRental.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/CarRental.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CarRental.AppHost/appsettings.json b/CarRental.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/CarRental.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj index 7450f9583..771be8696 100644 --- a/CarRental.Infrastructure/CarRental.Infrastructure.csproj +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -4,6 +4,10 @@ + + + + net8.0 enable diff --git a/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj b/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj new file mode 100644 index 000000000..9f4d04856 --- /dev/null +++ b/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/CarRental.ServiceDefaults/Extensions.cs b/CarRental.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..19401f882 --- /dev/null +++ b/CarRental.ServiceDefaults/Extensions.cs @@ -0,0 +1,118 @@ +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 CarRental.ServiceDefaults; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/CarRental.sln b/CarRental.sln index ae77556f4..1f0664c5e 100644 --- a/CarRental.sln +++ b/CarRental.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Application", "Ca EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Infrastructure", "CarRental.Infrastructure\CarRental.Infrastructure.csproj", "{BC450137-ECDC-D0A6-9C70-887579DD4AD0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.AppHost", "CarRental.AppHost\CarRental.AppHost.csproj", "{B8A65BF5-D8AA-4612-B694-388605683F4E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.ServiceDefaults", "CarRental.ServiceDefaults\CarRental.ServiceDefaults.csproj", "{0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +87,30 @@ Global {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x64.Build.0 = Release|Any CPU {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x86.ActiveCfg = Release|Any CPU {BC450137-ECDC-D0A6-9C70-887579DD4AD0}.Release|x86.Build.0 = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|x64.Build.0 = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Debug|x86.Build.0 = Debug|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|Any CPU.Build.0 = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|x64.ActiveCfg = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|x64.Build.0 = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|x86.ActiveCfg = Release|Any CPU + {B8A65BF5-D8AA-4612-B694-388605683F4E}.Release|x86.Build.0 = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|x64.ActiveCfg = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|x64.Build.0 = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|x86.ActiveCfg = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Debug|x86.Build.0 = Debug|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|Any CPU.Build.0 = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x64.ActiveCfg = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x64.Build.0 = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x86.ActiveCfg = Release|Any CPU + {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From ebe7790ab3650845c5a0abff78bc831c0dcb61a6 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 24 Dec 2025 01:20:06 +0400 Subject: [PATCH 19/37] added attributes with response codes, fixed the return types in the controllers to ActionResult, separated DTOs for analytical methods, added logging of controllers, changed the operation of the element deletion call --- .../Controllers/AnalyticsController.cs | 101 +++++++++++++++--- CarRental.Api/Controllers/CarControllers.cs | 75 ++++++++++--- CarRental.Api/Controllers/ClientController.cs | 89 +++++++++++++-- CarRental.Api/Controllers/RentController.cs | 89 +++++++++++++-- .../Contracts/Analytics/CarInRentDto.cs | 11 ++ .../Analytics/CarWithRentalCountDto.cs | 10 ++ .../Analytics/ClientWithTotalAmountDto.cs | 10 ++ .../Contracts/AnalyticsDtos.cs | 45 -------- .../Interfaces/IAnalyticsService.cs | 3 +- .../Services/AnalyticsService.cs | 4 +- CarRental.Application/Services/RentService.cs | 4 +- 11 files changed, 339 insertions(+), 102 deletions(-) create mode 100644 CarRental.Application/Contracts/Analytics/CarInRentDto.cs create mode 100644 CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs create mode 100644 CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs delete mode 100644 CarRental.Application/Contracts/AnalyticsDtos.cs diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index 2d317c477..a09b2fc2f 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -1,4 +1,6 @@ -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Analytics; +using CarRental.Application.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; @@ -8,17 +10,30 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase +public class AnalyticsController(IAnalyticsService analyticsService, ILogger logger) : ControllerBase { /// /// Retrieves a list of clients who have rented cars associated with a specific model name /// /// The name of the car model to filter by [HttpGet("clients-by-model")] - public IActionResult GetClientsByModel([FromQuery] string modelName) + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public ActionResult> GetClientsByModel([FromQuery] string modelName) { - var result = analyticsService.ReadClientsByModelName(modelName); - return Ok(result); + logger.LogInformation("{method} method of {controller} is called with {@string} parameter", nameof(GetClientsByModel), GetType().Name, modelName); + try + { + var result = analyticsService.ReadClientsByModelName(modelName); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetClientsByModel), GetType().Name); + return result != null ? Ok(result) : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetClientsByModel), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -26,39 +41,91 @@ public IActionResult GetClientsByModel([FromQuery] string modelName) /// /// The point in time to check for active rentals [HttpGet("cars-in-rent")] - public IActionResult GetCarsInRent([FromQuery] DateTime atTime) + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public ActionResult> GetCarsInRent([FromQuery] DateTime atTime) { - var result = analyticsService.ReadCarsInRent(atTime); - return Ok(result); + logger.LogInformation("{method} method of {controller} is called with {parameterName} = {parameterValue}", nameof(GetCarsInRent), GetType().Name, nameof(atTime), atTime); + try + { + var result = analyticsService.ReadCarsInRent(atTime); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetCarsInRent), GetType().Name); + return result != null ? Ok(result) : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetCarsInRent), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// /// Returns the top 5 most popular cars based on total rental frequency /// [HttpGet("top-5-rented-cars")] - public IActionResult GetTop5Cars() + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public ActionResult> GetTop5Cars() { - var result = analyticsService.ReadTop5MostRentedCars(); - return Ok(result); + logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Cars), GetType().Name); + try + { + var result = analyticsService.ReadTop5MostRentedCars(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Cars), GetType().Name); + return result != null ? Ok(result) : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5Cars), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// /// Provides a comprehensive list of all cars and how many times each has been rented /// [HttpGet("all-cars-with-rental-count")] - public IActionResult GetAllCarsWithCount() + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public ActionResult> GetAllCarsWithCount() { - var result = analyticsService.ReadAllCarsWithRentalCount(); - return Ok(result); + logger.LogInformation("{method} method of {controller} is called", nameof(GetAllCarsWithCount), GetType().Name); + try + { + var result = analyticsService.ReadAllCarsWithRentalCount(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAllCarsWithCount), GetType().Name); + return result != null ? Ok(result) : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetAllCarsWithCount), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// /// Returns the top 5 clients who have contributed the most to total revenue /// [HttpGet("top-5-clients-by-money")] - public IActionResult GetTop5Clients() + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public ActionResult> GetTop5Clients() { - var result = analyticsService.ReadTop5ClientsByTotalAmount(); - return Ok(result); + logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Clients), GetType().Name); + try + { + var result = analyticsService.ReadTop5ClientsByTotalAmount(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Clients), GetType().Name); + return result != null ? Ok(result) : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5Clients), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index b5dedb774..01b6889d4 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -9,16 +9,28 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class CarsController(IApplicationService carService) : ControllerBase +public class CarController(IApplicationService carService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all cars available in the system /// [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public ActionResult> GetAll() { - var cars = carService.ReadAll(); - return Ok(cars); + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var cars = carService.ReadAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(cars); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -26,14 +38,28 @@ public ActionResult> GetAll() /// /// The unique identifier of the car [HttpGet("{id}")] - public ActionResult GetById(uint id) + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public ActionResult Get(uint id) { - var car = carService.Read(id); - if (car == null) + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); + try + { + var car = carService.Read(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(car); + } + catch (KeyNotFoundException ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + catch (Exception ex) { - return NotFound($" ID {id} ."); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); } - return Ok(car); } /// @@ -41,16 +67,21 @@ public ActionResult GetById(uint id) /// /// The data for the new car record [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] public ActionResult Create([FromBody] CarCreateUpdateDto dto) { + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); try { var createdCar = carService.Create(dto); - return CreatedAtAction(nameof(GetById), new { id = createdCar.Id }, createdCar); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), createdCar); } catch (Exception ex) { - return BadRequest(ex.Message); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); } } @@ -60,16 +91,21 @@ public ActionResult Create([FromBody] CarCreateUpdateDto dto) /// The unique identifier of the car to update /// The updated data [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) { + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try { var updatedCar = carService.Update(dto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); return Ok(updatedCar); } catch (Exception ex) { - return BadRequest(ex.Message); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); } } @@ -78,13 +114,22 @@ public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) /// /// The unique identifier of the car to delete [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] public ActionResult Delete(uint id) { - var result = carService.Delete(id); - if (!result) + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var result = carService.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return result ? Ok() : NoContent(); + } + catch (Exception ex) { - return NotFound($" ID {id}."); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); } - return NoContent(); } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index ee7da67ba..6ebb6b334 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -9,23 +9,57 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class ClientController(IApplicationService clientService) : ControllerBase +public class ClientController(IApplicationService clientService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all registered clients /// [HttpGet] - public ActionResult> GetAll() => Ok(clientService.ReadAll()); + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public ActionResult> GetAll() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var result = clientService.ReadAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } /// /// Retrieves a specific client by their unique identifier /// /// The unique identifier of the client [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] public ActionResult Get(uint id) { - var client = clientService.Read(id); - return client != null ? Ok(client) : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); + try + { + var client = clientService.Read(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(client); + } + catch (KeyNotFoundException ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -33,10 +67,22 @@ public ActionResult Get(uint id) /// /// The client information to create [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] public ActionResult Create(ClientCreateUpdateDto dto) { - var result = clientService.Create(dto); - return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + try + { + var createdClient = clientService.Create(dto); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), createdClient); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -45,9 +91,22 @@ public ActionResult Create(ClientCreateUpdateDto dto) /// The ID of the client to update /// The updated client data [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public ActionResult Update(uint id, ClientCreateUpdateDto dto) { - return clientService.Update(dto, id) ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + try + { + var updatedClient = clientService.Update(dto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); + return Ok(updatedClient); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -55,8 +114,22 @@ public ActionResult Update(uint id, ClientCreateUpdateDto dto) /// /// The unique identifier of the client to delete [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] public ActionResult Delete(uint id) { - return clientService.Delete(id) ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var result = clientService.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return result ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index f78aa553d..abcb9db8a 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -9,23 +9,57 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class RentController(IApplicationService rentService) : ControllerBase +public class RentController(IApplicationService rentService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all rental records, including calculated costs and linked entity names /// [HttpGet] - public ActionResult> GetAll() => Ok(rentService.ReadAll()); + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public ActionResult> GetAll() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var result = rentService.ReadAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } /// /// Retrieves a specific rental agreement by its identifier. /// /// The unique identifier of the rental record. [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] public ActionResult Get(uint id) { - var rent = rentService.Read(id); - return rent != null ? Ok(rent) : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); + try + { + var rent = rentService.Read(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(rent); + } + catch (KeyNotFoundException ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -33,14 +67,22 @@ public ActionResult Get(uint id) /// /// The rental details, including CarId and ClientId. [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] public ActionResult Create(RentCreateUpdateDto dto) { - var result = rentService.Create(dto); - if (result == null) + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + try + { + var createdRent = rentService.Create(dto); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), createdRent); + } + catch (Exception ex) { - return BadRequest("Client or car is not exist"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); } - return CreatedAtAction(nameof(Get), new { id = result.Id }, result); } /// @@ -49,9 +91,22 @@ public ActionResult Create(RentCreateUpdateDto dto) /// The ID of the rental to update. /// The updated rental data. [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public ActionResult Update(uint id, RentCreateUpdateDto dto) { - return rentService.Update(dto, id) ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + try + { + var updatedRent = rentService.Update(dto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); + return Ok(updatedRent); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } /// @@ -59,8 +114,22 @@ public ActionResult Update(uint id, RentCreateUpdateDto dto) /// /// The unique identifier of the rental to remove. [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] public ActionResult Delete(uint id) { - return rentService.Delete(id) ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var result = rentService.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return result ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } } \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs new file mode 100644 index 000000000..d3785a4f0 --- /dev/null +++ b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs @@ -0,0 +1,11 @@ +namespace CarRental.Application.Contracts.Analytics; + +/// +/// Data transfer object representing a car that is currently or was previously in an active rental state. +/// +/// The unique identifier of the car. +/// The descriptive name of the car model. +/// The vehicle's license plate number. +/// The exact start time of the rental period. +/// The length of the rental in hours. +public record CarInRentDto(uint CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs new file mode 100644 index 000000000..56860338a --- /dev/null +++ b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs @@ -0,0 +1,10 @@ +namespace CarRental.Application.Contracts.Analytics; + +/// +/// Data transfer object for car statistics, including the total number of times it was rented. +/// +/// The unique identifier of the car. +/// The descriptive name of the car model. +/// The vehicle's license plate number. +/// Total number of rental agreements associated with this car. +public record CarWithRentalCountDto(uint Id, string ModelName, string NumberPlate, int RentalCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs new file mode 100644 index 000000000..52f868903 --- /dev/null +++ b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs @@ -0,0 +1,10 @@ +namespace CarRental.Application.Contracts.Analytics; + +/// +/// Data transfer object for client financial statistics. +/// +/// The unique identifier of the client. +/// The concatenated full name of the client. +/// The sum of all rental costs paid by the client. +/// Total number of times the client has rented vehicles. +public record ClientWithTotalAmountDto(uint Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/AnalyticsDtos.cs b/CarRental.Application/Contracts/AnalyticsDtos.cs deleted file mode 100644 index d328a8b81..000000000 --- a/CarRental.Application/Contracts/AnalyticsDtos.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace CarRental.Application.Contracts; - -/// -/// Data transfer object for car statistics, including the total number of times it was rented. -/// -/// The unique identifier of the car. -/// The descriptive name of the car model. -/// The vehicle's license plate number. -/// Total number of rental agreements associated with this car. -public record CarWithRentalCountDto( - uint Id, - string ModelName, - string NumberPlate, - int RentalCount -); - -/// -/// Data transfer object for client financial statistics. -/// -/// The unique identifier of the client. -/// The concatenated full name of the client. -/// The sum of all rental costs paid by the client. -/// Total number of times the client has rented vehicles. -public record ClientWithTotalAmountDto( - uint Id, - string FullName, - decimal TotalSpentAmount, - int TotalRentsCount -); - -/// -/// Data transfer object representing a car that is currently or was previously in an active rental state. -/// -/// The unique identifier of the car. -/// The descriptive name of the car model. -/// The vehicle's license plate number. -/// The exact start time of the rental period. -/// The length of the rental in hours. -public record CarInRentDto( - uint CarId, - string ModelName, - string NumberPlate, - DateTime RentStartDate, - int DurationHours -); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs index 14567b77f..8e21910ea 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -1,6 +1,5 @@ -using CarRental.Application.Contracts.Car; using CarRental.Application.Contracts.Client; -using CarRental.Application.Contracts; +using CarRental.Application.Contracts.Analytics; namespace CarRental.Application.Interfaces; diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 43e018cef..ea2ff1077 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -1,7 +1,5 @@ -using CarRental.Application.Contracts; -using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Analytics; using CarRental.Application.Contracts.Client; -using CarRental.Application.Contracts.Rent; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 23c3bce01..ebaa5db92 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -41,13 +41,13 @@ public List ReadAll() /// Creates a new rental agreement after validating that both the requested car and client exist. /// /// The created rental DTO, or null if validation fails. - public RentDto? Create(RentCreateUpdateDto dto) + public RentDto Create(RentCreateUpdateDto dto) { var car = carRepository.Read(dto.CarId); var client = clientRepository.Read(dto.ClientId); if (car == null || client == null) { - return null; + throw new Exception("Car or client is not found"); } var entity = dto.Adapt(); entity.Car = car; From e41070474c9808c6ae116bbbfbad1e7209033509 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 24 Dec 2025 01:47:13 +0400 Subject: [PATCH 20/37] fixed troubles with dissapeared patronymic when user tries to create new client or update it and added comments to methods and parameters --- CarRental.Api/CarRental.Api.csproj | 1 + CarRental.Api/Program.cs | 15 ++++++++++++++- .../CarRental.Application.csproj | 1 + .../Contracts/Client/ClientCreateUpdateDto.cs | 2 +- CarRental.Domain/CarRental.Domain.csproj | 1 + CarRental.Domain/Interfaces/BaseRepository.cs | 2 -- .../CarRental.Infrastructure.csproj | 1 + CarRental.Tests/CarRental.Tests.csproj | 1 + 8 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CarRental.Api/CarRental.Api.csproj b/CarRental.Api/CarRental.Api.csproj index b561f917d..48674fcff 100644 --- a/CarRental.Api/CarRental.Api.csproj +++ b/CarRental.Api/CarRental.Api.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + True diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 37fc56ccd..cceb7601d 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -37,7 +37,20 @@ options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; }); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("CarRental")) + .Distinct(); + + foreach (var assembly in assemblies) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } +}); var app = builder.Build(); diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index 05b2b5a42..e8afa5560 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -14,6 +14,7 @@ net8.0 enable enable + True diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs index 48189a06c..06c126b1d 100644 --- a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.Client; /// The client's contact phone number. /// The unique identifier of the client's driving license. /// The client's date of birth. -public record ClientCreateUpdateDto(string FirstName, string LastName, string PhoneNumber, string DriverLicense, DateOnly BirthDate); \ No newline at end of file +public record ClientCreateUpdateDto(string FirstName, string LastName, string? Patronymic, string PhoneNumber, string DriverLicenseId, DateOnly BirthDate); \ No newline at end of file diff --git a/CarRental.Domain/CarRental.Domain.csproj b/CarRental.Domain/CarRental.Domain.csproj index fa71b7ae6..5e7c51be3 100644 --- a/CarRental.Domain/CarRental.Domain.csproj +++ b/CarRental.Domain/CarRental.Domain.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + True diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index 84b1427b6..31fcddfeb 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -101,6 +101,4 @@ public virtual bool Delete(uint id) } return false; } - - } \ No newline at end of file diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj index 7450f9583..1e98d3444 100644 --- a/CarRental.Infrastructure/CarRental.Infrastructure.csproj +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -8,6 +8,7 @@ net8.0 enable enable + True diff --git a/CarRental.Tests/CarRental.Tests.csproj b/CarRental.Tests/CarRental.Tests.csproj index dd21883c9..5a9c2b32f 100644 --- a/CarRental.Tests/CarRental.Tests.csproj +++ b/CarRental.Tests/CarRental.Tests.csproj @@ -5,6 +5,7 @@ enable enable false + True From 1a96f6d54467c6122556b9fab0814cbd7b242a4b Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 24 Dec 2025 15:23:27 +0400 Subject: [PATCH 21/37] fixed problem with summaries and CarCreateUpdateDto --- CarRental.Api/Program.cs | 1 + .../Contracts/Client/ClientCreateUpdateDto.cs | 3 ++- CarRental.Domain/DataModels/Client.cs | 14 +++++++------- CarRental.Domain/Interfaces/BaseRepository.cs | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index cceb7601d..968641232 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -19,6 +19,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton, CarModelRepository>(); builder.Services.AddSingleton, CarModelGenerationRepository>(); +builder.Services.AddSingleton, CarModelGenerationRepository>(); builder.Services.AddSingleton, CarRepository>(); builder.Services.AddSingleton, ClientRepository>(); builder.Services.AddSingleton, RentRepository>(); diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs index 06c126b1d..50f2b5a66 100644 --- a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs @@ -5,7 +5,8 @@ namespace CarRental.Application.Contracts.Client; /// /// The client's given name. /// The client's family name. +/// The client's patronymic. /// The client's contact phone number. -/// The unique identifier of the client's driving license. +/// The unique identifier of the client's driving license. /// The client's date of birth. public record ClientCreateUpdateDto(string FirstName, string LastName, string? Patronymic, string PhoneNumber, string DriverLicenseId, DateOnly BirthDate); \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index f3a5a1928..9f5787220 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -2,36 +2,36 @@ namespace CarRental.Domain.DataModels; /// /// Represents a client (rental customer) with personal and identification information -/// +/// public class Client { /// /// Unique identifier of the client - /// + /// public required uint Id { get; set; } /// /// Unique identifier of the client's driver's license - /// + /// public required string DriverLicenseId { get; set; } /// /// Client's last name (surname) - /// + /// public required string LastName { get; set; } /// /// Client's first name (given name) - /// + /// public required string FirstName { get; set; } /// /// Client's patronymic (middle name), if applicable - /// + /// public string? Patronymic { get; set; } /// /// Client's date of birth - /// + /// public DateOnly? BirthDate { get; set; } } \ No newline at end of file diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index 31fcddfeb..e6f1cc535 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -10,7 +10,7 @@ public abstract class BaseRepository : IBaseRepository /// /// Private field for obtaining a unique identifier /// to assign it to the next entity in the repository - /// + /// private uint _nextId; From 49642d7c5336332214b7bc081193c81768f7b22c Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 24 Dec 2025 19:11:31 +0400 Subject: [PATCH 22/37] type of the ID was changed from uint to int for better communication with DB, added correct type for returning in crud methods --- CarRental.Api/Program.cs | 2 +- .../Contracts/Analytics/CarInRentDto.cs | 2 +- .../Analytics/CarWithRentalCountDto.cs | 2 +- .../Analytics/ClientWithTotalAmountDto.cs | 2 +- .../Contracts/Car/CarCreateUpdateDto.cs | 2 +- CarRental.Application/Contracts/Car/CarDto.cs | 2 +- .../CarModel/CarModelCreateUpdateDto.cs | 2 +- .../Contracts/CarModel/CarModelDto.cs | 2 +- .../CarModelGenerationCreateUpdateDto.cs | 2 +- .../CarModelGenerationDto.cs | 2 +- .../Contracts/Client/ClientDto.cs | 2 +- .../Contracts/Rent/RentCreateUpdateDto.cs | 2 +- .../Contracts/Rent/RentDto.cs | 2 +- .../Interfaces/IAnalyticsService.cs | 10 +-- .../Interfaces/IApplicationService.cs | 10 +-- .../Mapping/MappingConfig.cs | 3 - .../Services/AnalyticsService.cs | 12 +-- CarRental.Application/Services/CarService.cs | 10 +-- .../Services/ClientService.cs | 6 +- CarRental.Application/Services/RentService.cs | 6 +- CarRental.Domain/DataModels/Car.cs | 2 +- CarRental.Domain/DataModels/Client.cs | 2 +- CarRental.Domain/DataModels/Rent.cs | 2 +- CarRental.Domain/Interfaces/BaseRepository.cs | 26 +++---- .../Interfaces/IBaseRepository.cs | 10 +-- .../InternalData/ComponentClasses/CarModel.cs | 4 +- .../ComponentClasses/CarModelGeneration.cs | 2 +- .../CarModelGenerationRepository.cs | 4 +- .../InMemoryRepository/CarModelRepository.cs | 4 +- .../InMemoryRepository/CarRepository.cs | 4 +- .../InMemoryRepository/ClientRepository.cs | 4 +- .../InMemoryRepository/RentRepository.cs | 5 +- .../CarRental.ServiceDefaults.csproj | 15 ++-- CarRental.ServiceDefaults/Extensions.cs | 73 ++++++------------- 34 files changed, 103 insertions(+), 137 deletions(-) diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 6fdc10e17..a8b0c64f4 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -13,8 +13,8 @@ using MapsterMapper; var builder = WebApplication.CreateBuilder(args); -builder.AddMongoDBClient("CarRentalDb"); builder.AddServiceDefaults(); +builder.AddMongoDBClient("CarRentalDb"); builder.Services.AddSingleton(TypeAdapterConfig.GlobalSettings); builder.Services.AddScoped(); diff --git a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs index d3785a4f0..b898d5041 100644 --- a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs +++ b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The vehicle's license plate number. /// The exact start time of the rental period. /// The length of the rental in hours. -public record CarInRentDto(uint CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours); \ No newline at end of file +public record CarInRentDto(int CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs index 56860338a..0497b59be 100644 --- a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs +++ b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The descriptive name of the car model. /// The vehicle's license plate number. /// Total number of rental agreements associated with this car. -public record CarWithRentalCountDto(uint Id, string ModelName, string NumberPlate, int RentalCount); \ No newline at end of file +public record CarWithRentalCountDto(int Id, string ModelName, string NumberPlate, int RentalCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs index 52f868903..8b6bf8c97 100644 --- a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs +++ b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The concatenated full name of the client. /// The sum of all rental costs paid by the client. /// Total number of times the client has rented vehicles. -public record ClientWithTotalAmountDto(uint Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file +public record ClientWithTotalAmountDto(int Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs index ff9535b02..9e99fe6d1 100644 --- a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs @@ -6,4 +6,4 @@ namespace CarRental.Application.Contracts.Car; /// The vehicle's license plate number. /// The color of the car. /// The unique identifier of the associated car model generation. -public record CarCreateUpdateDto(string NumberPlate, string Colour, uint ModelGenerationId); \ No newline at end of file +public record CarCreateUpdateDto(string NumberPlate, string Colour, int ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs index 7725a9d1d..aa8d1af99 100644 --- a/CarRental.Application/Contracts/Car/CarDto.cs +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Car; /// The vehicle's license plate number. /// The color of the car. /// The descriptive name of the car model. -public record CarDto(uint Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file +public record CarDto(int Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs index 6c8c0137b..2c7a11c1d 100644 --- a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.CarModel; /// The total passenger capacity. /// The style of the vehicle body (e.g., Sedan, SUV). /// The market segment or luxury class of the vehicle. -public record CarModelCreateUpdateDto(string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file +public record CarModelCreateUpdateDto(string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelDto.cs b/CarRental.Application/Contracts/CarModel/CarModelDto.cs index 9d2f8da93..a041be885 100644 --- a/CarRental.Application/Contracts/CarModel/CarModelDto.cs +++ b/CarRental.Application/Contracts/CarModel/CarModelDto.cs @@ -9,4 +9,4 @@ namespace CarRental.Application.Contracts.CarModel; /// The total passenger capacity. /// The style of the vehicle body (e.g., Sedan, SUV). /// The market segment or luxury class of the vehicle. -public record CarModelDto(uint Id, string Name, string? DriveType, uint SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file +public record CarModelDto(int Id, string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs index 7334a2add..7105c8e09 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.CarModelGeneration; /// The type of transmission (e.g., Manual, Automatic). /// The rental cost per hour for this generation. /// The unique identifier of the parent car model. -public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file +public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, int ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs index 83e5a99b5..46e2793e9 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.CarModelGeneration; /// The type of transmission used in this generation. /// The rental cost per hour. /// The identifier of the parent car model. -public record CarModelGenerationDto(uint Id, int Year, string? TransmissionType, decimal HourCost, uint ModelId); \ No newline at end of file +public record CarModelGenerationDto(int Id, int Year, string? TransmissionType, decimal HourCost, int ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientDto.cs b/CarRental.Application/Contracts/Client/ClientDto.cs index b2802925a..d153fb66d 100644 --- a/CarRental.Application/Contracts/Client/ClientDto.cs +++ b/CarRental.Application/Contracts/Client/ClientDto.cs @@ -9,4 +9,4 @@ namespace CarRental.Application.Contracts.Client; /// The client's first name. /// The client's middle name (optional). /// The client's date of birth (optional). -public record ClientDto(uint Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file +public record ClientDto(int Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs index bd2a6b5a7..9bd2bedda 100644 --- a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Rent; /// The length of the rental period in hours. /// The unique identifier of the car to be rented. /// The unique identifier of the client renting the car. -public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, uint CarId, uint ClientId); \ No newline at end of file +public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, int CarId, int ClientId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application/Contracts/Rent/RentDto.cs index 36441d76c..7aca98b92 100644 --- a/CarRental.Application/Contracts/Rent/RentDto.cs +++ b/CarRental.Application/Contracts/Rent/RentDto.cs @@ -11,4 +11,4 @@ namespace CarRental.Application.Contracts.Rent; /// The unique identifier of the client. /// The last name of the client. /// The total calculated cost for the rental duration. -public record RentDto(uint Id, DateTime StartDateTime, double Duration, uint CarId, string CarLicensePlate, uint ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file +public record RentDto(int Id, DateTime StartDateTime, double Duration, int CarId, string CarLicensePlate, int ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application/Interfaces/IAnalyticsService.cs index 8e21910ea..501301c9a 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application/Interfaces/IAnalyticsService.cs @@ -11,25 +11,25 @@ public interface IAnalyticsService /// /// Retrieves all clients who have rented a specific car model. /// - public List ReadClientsByModelName(string modelName); + public Task> ReadClientsByModelName(string modelName); /// /// Lists all cars that are currently occupied at a specific point in time. /// - public List ReadCarsInRent(DateTime atTime); + public Task> ReadCarsInRent(DateTime atTime); /// /// Returns the top 5 cars with the highest number of rental agreements. /// - public List ReadTop5MostRentedCars(); + public Task> ReadTop5MostRentedCars(); /// /// Returns a list of all cars along with their total rental frequency. /// - public List ReadAllCarsWithRentalCount(); + public Task> ReadAllCarsWithRentalCount(); /// /// Returns the top 5 clients who have spent the most money on rentals. /// - public List ReadTop5ClientsByTotalAmount(); + public Task> ReadTop5ClientsByTotalAmount(); } \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index 9893feb1a..031a694a9 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -12,25 +12,25 @@ public interface IApplicationService /// /// Creates a new record from the provided input DTO and returns the resulting output DTO. /// - public TDto Create(TCreateUpdateDto dto); + public Task Create(TCreateUpdateDto dto); /// /// Retrieves a single record by its unique identifier, mapped to an output DTO. /// - public TDto? Read(uint id); + public Task Read(int id); /// /// Retrieves all records mapped to a list of output DTOs. /// - public List ReadAll(); + public Task> ReadAll(); /// /// Updates an existing record identified by the given ID using the input DTO data. /// - public bool Update(TCreateUpdateDto dto, uint id); + public Task Update(TCreateUpdateDto dto, int id); /// /// Removes a record from the system by its unique identifier. /// - public bool Delete(uint id); + public Task Delete(int id); } \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs index d6e803e84..e521c9b93 100644 --- a/CarRental.Application/Mapping/MappingConfig.cs +++ b/CarRental.Application/Mapping/MappingConfig.cs @@ -1,11 +1,8 @@ using Mapster; using CarRental.Domain.DataModels; -using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Application.Contracts.Car; using CarRental.Application.Contracts.Client; using CarRental.Application.Contracts.Rent; -using CarRental.Application.Contracts.CarModel; -using CarRental.Application.Contracts.CarModelGeneration; namespace CarRental.Application.Mapping; diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index ea2ff1077..3b203d83b 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -17,9 +17,9 @@ public class AnalyticsService( /// /// Finds unique clients who rented cars of a specific model name. /// - public List ReadClientsByModelName(string modelName) + public async Task> ReadClientsByModelName(string modelName) { - return rentRepository.ReadAll() + return await rentRepository.ReadAll() .Where(r => r.Car.ModelGeneration!.Model!.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) .Select(r => r.Client.Adapt()) .DistinctBy(c => c.Id) @@ -29,7 +29,7 @@ public List ReadClientsByModelName(string modelName) /// /// Identifies the top 5 most frequently rented cars. /// - public List ReadTop5MostRentedCars() + public Task> ReadTop5MostRentedCars() { return rentRepository.ReadAll() .GroupBy(r => r.Car.Id) @@ -47,7 +47,7 @@ public List ReadTop5MostRentedCars() /// /// Retrieves cars that were actively rented at a specific point in time. /// - public List ReadCarsInRent(DateTime atTime) + public Task> ReadCarsInRent(DateTime atTime) { return rentRepository.ReadAll() .Where(r => r.StartDateTime <= atTime && r.StartDateTime.AddHours(r.Duration) >= atTime) @@ -64,7 +64,7 @@ public List ReadCarsInRent(DateTime atTime) /// /// Lists all cars and their total rental frequency. /// - public List ReadAllCarsWithRentalCount() + public Task> ReadAllCarsWithRentalCount() { var allRents = rentRepository.ReadAll(); @@ -81,7 +81,7 @@ public List ReadAllCarsWithRentalCount() /// /// Identifies the top 5 clients by total revenue generated. /// - public List ReadTop5ClientsByTotalAmount() + public Task> ReadTop5ClientsByTotalAmount() { return rentRepository.ReadAll() .GroupBy(r => r.Client.Id) diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index ccb5813f1..6c00f401b 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -18,13 +18,13 @@ public class CarService( /// /// Retrieves all car records and maps them to DTOs. /// - public List ReadAll() => - repository.ReadAll().Select(e => e.Adapt()).ToList(); + public async Task> ReadAll() => + await repository.ReadAll().Select(e => e.Adapt()).ToList(); /// /// Retrieves a specific car by its identifier. /// - public CarDto? Read(uint id) => + public CarDto? Read(int id) => repository.Read(id)?.Adapt(); /// @@ -49,7 +49,7 @@ public CarDto Create(CarCreateUpdateDto dto) /// /// Updates an existing car's information and its relationship with a model generation. /// - public bool Update(CarCreateUpdateDto dto, uint id) + public bool Update(CarCreateUpdateDto dto, int id) { var existing = repository.Read(id); if (existing is null) return false; @@ -67,5 +67,5 @@ public bool Update(CarCreateUpdateDto dto, uint id) /// /// Deletes a car record by its identifier. /// - public bool Delete(uint id) => repository.Delete(id); + public bool Delete(int id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 66e14d6d4..79fdedbae 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -20,7 +20,7 @@ public List ReadAll() => /// /// Finds a specific client by their unique identifier. /// - public ClientDto? Read(uint id) => + public ClientDto? Read(int id) => repository.Read(id)?.Adapt(); /// @@ -37,7 +37,7 @@ public ClientDto Create(ClientCreateUpdateDto dto) /// /// Updates an existing client's personal and contact information. /// - public bool Update(ClientCreateUpdateDto dto, uint id) + public bool Update(ClientCreateUpdateDto dto, int id) { var existing = repository.Read(id); if (existing is null) return false; @@ -48,5 +48,5 @@ public bool Update(ClientCreateUpdateDto dto, uint id) /// /// Removes a client record from the database. /// - public bool Delete(uint id) => repository.Delete(id); + public bool Delete(int id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index ebaa5db92..9b8bee27f 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -34,7 +34,7 @@ public List ReadAll() /// /// Retrieves a specific rental agreement by its identifier. /// - public RentDto? Read(uint id) => + public RentDto? Read(int id) => repository.Read(id)?.Adapt(); /// @@ -62,7 +62,7 @@ public RentDto Create(RentCreateUpdateDto dto) /// /// Updates an existing rental agreement's details. /// - public bool Update(RentCreateUpdateDto dto, uint id) + public bool Update(RentCreateUpdateDto dto, int id) { var existing = repository.Read(id); if (existing is null) return false; @@ -73,5 +73,5 @@ public bool Update(RentCreateUpdateDto dto, uint id) /// /// Permanently removes a rental record from the system. /// - public bool Delete(uint id) => repository.Delete(id); + public bool Delete(int id) => repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs index be8ce4cf9..f0d76c05e 100644 --- a/CarRental.Domain/DataModels/Car.cs +++ b/CarRental.Domain/DataModels/Car.cs @@ -10,7 +10,7 @@ public class Car /// /// Unique identifier of the car /// - public required uint Id { get; set; } + public required int Id { get; set; } /// /// The model generation this car belongs to, defining its year, transmission type, and base rental cost diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index 9f5787220..fc45cb0b6 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -8,7 +8,7 @@ public class Client /// /// Unique identifier of the client /// - public required uint Id { get; set; } + public required int Id { get; set; } /// /// Unique identifier of the client's driver's license diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs index 15d688238..1548b4561 100644 --- a/CarRental.Domain/DataModels/Rent.cs +++ b/CarRental.Domain/DataModels/Rent.cs @@ -8,7 +8,7 @@ public class Rent /// /// Unique identifier of the rental record /// - public uint Id { get; set; } + public int Id { get; set; } /// /// Date and time when the rental period starts diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index e6f1cc535..9bff49602 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -12,18 +12,18 @@ public abstract class BaseRepository : IBaseRepository /// to assign it to the next entity in the repository /// - private uint _nextId; + private int _nextId; private readonly List _entities; /// /// Gets the unique identifier from the entity. /// - protected abstract uint GetEntityId(TEntity entity); + protected abstract int GetEntityId(TEntity entity); /// /// Sets the unique identifier for the entity /// - protected abstract void SetEntityId(TEntity entity, uint id); + protected abstract void SetEntityId(TEntity entity, int id); /// /// Initializes the repository and determines the starting ID based on existing data. @@ -44,7 +44,7 @@ protected BaseRepository(List? entities = null) /// /// Adds a new entity to the collection and assigns a unique ID. /// - public virtual uint Create(TEntity entity) + public virtual Task Create(TEntity entity) { if (entity == null) { @@ -54,31 +54,31 @@ public virtual uint Create(TEntity entity) SetEntityId(entity, currentId); _entities.Add(entity); _nextId++; - return currentId; + return Task.FromResult(currentId); } /// /// Retrieves an entity by its unique identifier. /// - public virtual TEntity? Read(uint id) + public virtual Task Read(int id) { - return _entities.FirstOrDefault(e => GetEntityId(e) == id); + return Task.FromResult(_entities.FirstOrDefault(e => GetEntityId(e) == id)); } /// /// Returns all entities in the collection. /// - public virtual List ReadAll() + public virtual Task> ReadAll() { - return _entities.ToList(); + return Task.FromResult(_entities.ToList()); } /// /// Replaces an existing entity at the specified ID. /// - public virtual bool Update(TEntity entity, uint id) + public virtual async Task Update(TEntity entity, int id) { - var existing = Read(id); + var existing = await Read(id); if (existing != null) { var index = _entities.IndexOf(existing); @@ -92,9 +92,9 @@ public virtual bool Update(TEntity entity, uint id) /// /// Removes an entity from the collection by its ID. /// - public virtual bool Delete(uint id) + public virtual async Task Delete(int id) { - var entity = Read(id); + var entity = await Read(id); if (entity != null) { return _entities.Remove(entity); diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs index c16764d24..0ec5c381a 100644 --- a/CarRental.Domain/Interfaces/IBaseRepository.cs +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -10,25 +10,25 @@ public interface IBaseRepository /// /// Adds a new entity to the collection and returns a unique ID. /// - public uint Create(TEntity entity); + public Task Create(TEntity entity); /// /// Retrieves an entity by its unique identifier. /// - public TEntity? Read(uint id); + public Task Read(int id); /// /// Returns all entities in the collection. /// - public List ReadAll(); + public Task> ReadAll(); /// /// Replaces an existing entity at the specified ID. /// - public bool Update(TEntity entity, uint id); + public Task Update(TEntity entity, int id); /// /// Removes an entity from the collection by its ID. /// - public bool Delete(uint id); + public Task Delete(int id); } \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index 542f2f817..3fa091ae4 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -12,7 +12,7 @@ public class CarModel /// /// Unique identifier of the car model /// - public required uint Id { get; set; } + public required int Id { get; set; } /// /// Name of the car model (e.g., "Camry", "Golf", "Model 3") @@ -27,7 +27,7 @@ public class CarModel /// /// Number of passenger seats in the vehicle /// - public required uint SeatsNumber { get; set; } + public required int SeatsNumber { get; set; } /// /// Body style of the car model (e.g., sedan, SUV, hatchback) diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs index 7b9a3bd83..7098c6888 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -13,7 +13,7 @@ public class CarModelGeneration /// /// Unique identifier of the car model generation /// - public required uint Id { get; set; } + public required int Id { get; set; } /// /// Calendar year when this generation of the car model was produced diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs index 6de588976..f5b75b5e3 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -13,10 +13,10 @@ public class CarModelGenerationRepository(DataSeed data) : BaseRepository /// Gets the unique identifier from the specified CarModelGeneration entity /// - protected override uint GetEntityId(CarModelGeneration generation) => generation.Id; + protected override int GetEntityId(CarModelGeneration generation) => generation.Id; /// /// Sets the unique identifier for the specified CarModelGeneration entity /// - protected override void SetEntityId(CarModelGeneration generation, uint id) => generation.Id = id; + protected override void SetEntityId(CarModelGeneration generation, int id) => generation.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs index 6c4f0274f..4bdaec130 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -13,10 +13,10 @@ public class CarModelRepository(DataSeed data) : BaseRepository(data.M /// /// Gets the unique identifier from the specified CarModel entity /// - protected override uint GetEntityId(CarModel model) => model.Id; + protected override int GetEntityId(CarModel model) => model.Id; /// /// Sets the unique identifier for the specified CarModel entity /// - protected override void SetEntityId(CarModel model, uint id) => model.Id = id; + protected override void SetEntityId(CarModel model, int id) => model.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs index 3ffb1000c..752239df5 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs @@ -13,10 +13,10 @@ public class CarRepository(DataSeed data) : BaseRepository(data.Cars) /// /// Gets the unique identifier from the specified Car entity /// - protected override uint GetEntityId(Car car) => car.Id; + protected override int GetEntityId(Car car) => car.Id; /// /// Sets the unique identifier for the specified Car entity /// - protected override void SetEntityId(Car car, uint id) => car.Id = id; + protected override void SetEntityId(Car car, int id) => car.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs index 0092844d5..0671cf022 100644 --- a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs @@ -13,10 +13,10 @@ public class ClientRepository(DataSeed data) : BaseRepository(data.Clien /// /// Gets the unique identifier from the specified Client entity /// - protected override uint GetEntityId(Client client) => client.Id; + protected override int GetEntityId(Client client) => client.Id; /// /// Sets the unique identifier for the specified Client entity /// - protected override void SetEntityId(Client client, uint id) => client.Id = id; + protected override void SetEntityId(Client client, int id) => client.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs index 518698725..45d6db2fd 100644 --- a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -13,11 +13,10 @@ public class RentRepository(DataSeed data) : BaseRepository(data.Rents) /// /// Gets the unique identifier from the specified Rent entity /// - protected override uint GetEntityId(Rent rent) => rent.Id; + protected override int GetEntityId(Rent rent) => rent.Id; /// /// Sets the unique identifier for the specified Rent entity /// - protected override void SetEntityId(Rent rent, uint id) => rent.Id = id; - + protected override void SetEntityId(Rent rent, int id) => rent.Id = id; } \ No newline at end of file diff --git a/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj b/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj index 9f4d04856..a98507561 100644 --- a/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj +++ b/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj @@ -9,14 +9,15 @@ + - - - - - - - + + + + + + + diff --git a/CarRental.ServiceDefaults/Extensions.cs b/CarRental.ServiceDefaults/Extensions.cs index 19401f882..f02f4775d 100644 --- a/CarRental.ServiceDefaults/Extensions.cs +++ b/CarRental.ServiceDefaults/Extensions.cs @@ -2,117 +2,86 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace CarRental.ServiceDefaults; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. -// This project should be referenced by each service project in your solution. -// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { - public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); - builder.AddDefaultHealthChecks(); - builder.Services.AddServiceDiscovery(); - builder.Services.ConfigureHttpClientDefaults(http => { - // Turn on resilience by default http.AddStandardResilienceHandler(); - - // Turn on service discovery by default http.AddServiceDiscovery(); }); - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - return builder; } - public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder 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(); + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - tracing.AddAspNetCoreInstrumentation() - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) && + !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)) + .AddHttpClientInstrumentation(); }); - builder.AddOpenTelemetryExporters(); - return builder; } - private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) + if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"])) { builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - return builder; } - public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - + .AddCheck("self", () => HealthCheckResult.Healthy(), new[] { "live" }); return builder; } public static WebApplication MapDefaultEndpoints(this WebApplication app) { - // Adding health checks endpoints to applications in non-development environments has security implications. - // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. if (app.Environment.IsDevelopment()) { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + app.MapHealthChecks(HealthEndpointPath); + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }); } - return app; } -} +} \ No newline at end of file From 72aa8bf94753af379dc736837266c8ea4416f31f Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 24 Dec 2025 21:14:20 +0400 Subject: [PATCH 23/37] made services and controllers asynchronous --- .../Controllers/AnalyticsController.cs | 20 +++++----- CarRental.Api/Controllers/CarControllers.cs | 20 +++++----- CarRental.Api/Controllers/ClientController.cs | 20 +++++----- CarRental.Api/Controllers/RentController.cs | 20 +++++----- .../Services/AnalyticsService.cs | 34 ++++++++-------- CarRental.Application/Services/CarService.cs | 40 +++++++++++-------- .../Services/ClientService.cs | 31 +++++++++----- CarRental.Application/Services/RentService.cs | 37 ++++++++++------- 8 files changed, 122 insertions(+), 100 deletions(-) diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index a09b2fc2f..12d3ab393 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -20,12 +20,12 @@ public class AnalyticsController(IAnalyticsService analyticsService, ILogger> GetClientsByModel([FromQuery] string modelName) + public async Task>> GetClientsByModel([FromQuery] string modelName) { logger.LogInformation("{method} method of {controller} is called with {@string} parameter", nameof(GetClientsByModel), GetType().Name, modelName); try { - var result = analyticsService.ReadClientsByModelName(modelName); + var result = await analyticsService.ReadClientsByModelName(modelName); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetClientsByModel), GetType().Name); return result != null ? Ok(result) : NoContent(); } @@ -44,12 +44,12 @@ public ActionResult> GetClientsByModel([FromQuery] string modelN [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult> GetCarsInRent([FromQuery] DateTime atTime) + public async Task>> GetCarsInRent([FromQuery] DateTime atTime) { logger.LogInformation("{method} method of {controller} is called with {parameterName} = {parameterValue}", nameof(GetCarsInRent), GetType().Name, nameof(atTime), atTime); try { - var result = analyticsService.ReadCarsInRent(atTime); + var result = await analyticsService.ReadCarsInRent(atTime); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetCarsInRent), GetType().Name); return result != null ? Ok(result) : NoContent(); } @@ -67,12 +67,12 @@ public ActionResult> GetCarsInRent([FromQuery] DateTime atTim [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult> GetTop5Cars() + public async Task>> GetTop5Cars() { logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Cars), GetType().Name); try { - var result = analyticsService.ReadTop5MostRentedCars(); + var result = await analyticsService.ReadTop5MostRentedCars(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Cars), GetType().Name); return result != null ? Ok(result) : NoContent(); } @@ -90,12 +90,12 @@ public ActionResult> GetTop5Cars() [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult> GetAllCarsWithCount() + public async Task>> GetAllCarsWithCount() { logger.LogInformation("{method} method of {controller} is called", nameof(GetAllCarsWithCount), GetType().Name); try { - var result = analyticsService.ReadAllCarsWithRentalCount(); + var result = await analyticsService.ReadAllCarsWithRentalCount(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAllCarsWithCount), GetType().Name); return result != null ? Ok(result) : NoContent(); } @@ -113,12 +113,12 @@ public ActionResult> GetAllCarsWithCount() [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult> GetTop5Clients() + public async Task>> GetTop5Clients() { logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5Clients), GetType().Name); try { - var result = analyticsService.ReadTop5ClientsByTotalAmount(); + var result = await analyticsService.ReadTop5ClientsByTotalAmount(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Clients), GetType().Name); return result != null ? Ok(result) : NoContent(); } diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index 01b6889d4..b8202a850 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -17,12 +17,12 @@ public class CarController(IApplicationService carSe [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] - public ActionResult> GetAll() + public async Task>> GetAll() { logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); try { - var cars = carService.ReadAll(); + var cars = await carService.ReadAll(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); return Ok(cars); } @@ -41,12 +41,12 @@ public ActionResult> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public ActionResult Get(uint id) + public async Task> Get(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try { - var car = carService.Read(id); + var car = await carService.Read(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); return Ok(car); } @@ -69,12 +69,12 @@ public ActionResult Get(uint id) [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] - public ActionResult Create([FromBody] CarCreateUpdateDto dto) + public async Task> Create([FromBody] CarCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); try { - var createdCar = carService.Create(dto); + var createdCar = await carService.Create(dto); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); return CreatedAtAction(nameof(this.Create), createdCar); } @@ -93,12 +93,12 @@ public ActionResult Create([FromBody] CarCreateUpdateDto dto) [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) + public async Task> Update(int id, [FromBody] CarCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try { - var updatedCar = carService.Update(dto, id); + var updatedCar = await carService.Update(dto, id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); return Ok(updatedCar); } @@ -117,12 +117,12 @@ public ActionResult Update(uint id, [FromBody] CarCreateUpdateDto dto) [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult Delete(uint id) + public async Task Delete(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try { - var result = carService.Delete(id); + var result = await carService.Delete(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); return result ? Ok() : NoContent(); } diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index 6ebb6b334..e0a4d0cbb 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -17,12 +17,12 @@ public class ClientController(IApplicationService> GetAll() + public async Task>> GetAll() { logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); try { - var result = clientService.ReadAll(); + var result = await clientService.ReadAll(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); return Ok(result); } @@ -41,12 +41,12 @@ public ActionResult> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public ActionResult Get(uint id) + public async Task> Get(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try { - var client = clientService.Read(id); + var client = await clientService.Read(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); return Ok(client); } @@ -69,12 +69,12 @@ public ActionResult Get(uint id) [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] - public ActionResult Create(ClientCreateUpdateDto dto) + public async Task> Create(ClientCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); try { - var createdClient = clientService.Create(dto); + var createdClient = await clientService.Create(dto); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); return CreatedAtAction(nameof(this.Create), createdClient); } @@ -93,12 +93,12 @@ public ActionResult Create(ClientCreateUpdateDto dto) [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public ActionResult Update(uint id, ClientCreateUpdateDto dto) + public async Task Update(int id, ClientCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try { - var updatedClient = clientService.Update(dto, id); + var updatedClient = await clientService.Update(dto, id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); return Ok(updatedClient); } @@ -117,12 +117,12 @@ public ActionResult Update(uint id, ClientCreateUpdateDto dto) [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult Delete(uint id) + public async Task Delete(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try { - var result = clientService.Delete(id); + var result = await clientService.Delete(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); return result ? Ok() : NoContent(); } diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index abcb9db8a..1db5860d4 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -17,12 +17,12 @@ public class RentController(IApplicationService re [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] - public ActionResult> GetAll() + public async Task>> GetAll() { logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); try { - var result = rentService.ReadAll(); + var result = await rentService.ReadAll(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); return Ok(result); } @@ -41,12 +41,12 @@ public ActionResult> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public ActionResult Get(uint id) + public async Task> Get(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try { - var rent = rentService.Read(id); + var rent = await rentService.Read(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); return Ok(rent); } @@ -69,12 +69,12 @@ public ActionResult Get(uint id) [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] - public ActionResult Create(RentCreateUpdateDto dto) + public async Task> Create(RentCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); try { - var createdRent = rentService.Create(dto); + var createdRent = await rentService.Create(dto); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); return CreatedAtAction(nameof(this.Create), createdRent); } @@ -93,12 +93,12 @@ public ActionResult Create(RentCreateUpdateDto dto) [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public ActionResult Update(uint id, RentCreateUpdateDto dto) + public async Task Update(int id, RentCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try { - var updatedRent = rentService.Update(dto, id); + var updatedRent = await rentService.Update(dto, id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); return Ok(updatedRent); } @@ -117,12 +117,12 @@ public ActionResult Update(uint id, RentCreateUpdateDto dto) [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public ActionResult Delete(uint id) + public async Task Delete(int id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try { - var result = rentService.Delete(id); + var result = await rentService.Delete(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); return result ? Ok() : NoContent(); } diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 3b203d83b..c1aa2c10d 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -19,8 +19,8 @@ public class AnalyticsService( /// public async Task> ReadClientsByModelName(string modelName) { - return await rentRepository.ReadAll() - .Where(r => r.Car.ModelGeneration!.Model!.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + var clients = await rentRepository.ReadAll(); + return clients.Where(r => r.Car.ModelGeneration!.Model!.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) .Select(r => r.Client.Adapt()) .DistinctBy(c => c.Id) .ToList(); @@ -29,10 +29,10 @@ public async Task> ReadClientsByModelName(string modelName) /// /// Identifies the top 5 most frequently rented cars. /// - public Task> ReadTop5MostRentedCars() + public async Task> ReadTop5MostRentedCars() { - return rentRepository.ReadAll() - .GroupBy(r => r.Car.Id) + var rents = await rentRepository.ReadAll(); + return rents.GroupBy(r => r.Car.Id) .Select(g => new CarWithRentalCountDto( g.First().Car.Id, g.First().Car.ModelGeneration?.Model!.Name ?? "Unknown", @@ -47,10 +47,10 @@ public Task> ReadTop5MostRentedCars() /// /// Retrieves cars that were actively rented at a specific point in time. /// - public Task> ReadCarsInRent(DateTime atTime) + public async Task> ReadCarsInRent(DateTime atTime) { - return rentRepository.ReadAll() - .Where(r => r.StartDateTime <= atTime && r.StartDateTime.AddHours(r.Duration) >= atTime) + var rents = await rentRepository.ReadAll(); + return rents.Where(r => r.StartDateTime <= atTime && r.StartDateTime.AddHours(r.Duration) >= atTime) .Select(r => new CarInRentDto( r.Car.Id, r.Car.ModelGeneration?.Model!.Name ?? "Unknown", @@ -64,27 +64,25 @@ public Task> ReadCarsInRent(DateTime atTime) /// /// Lists all cars and their total rental frequency. /// - public Task> ReadAllCarsWithRentalCount() + public async Task> ReadAllCarsWithRentalCount() { - var allRents = rentRepository.ReadAll(); - - return carRepository.ReadAll() - .Select(car => new CarWithRentalCountDto( + var allRents = await rentRepository.ReadAll(); + var allCars = await carRepository.ReadAll(); + return allCars.Select(car => new CarWithRentalCountDto( car.Id, car.ModelGeneration?.Model!.Name ?? "Unknown", car.NumberPlate, allRents.Count(r => r.Car.Id == car.Id) - )) - .ToList(); + )).ToList(); } /// /// Identifies the top 5 clients by total revenue generated. /// - public Task> ReadTop5ClientsByTotalAmount() + public async Task> ReadTop5ClientsByTotalAmount() { - return rentRepository.ReadAll() - .GroupBy(r => r.Client.Id) + var rents = await rentRepository.ReadAll(); + return rents.GroupBy(r => r.Client.Id) .Select(g => { var client = g.First().Client; var totalAmount = g.Sum(r => (decimal)r.Duration * (r.Car.ModelGeneration?.HourCost ?? 0)); diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index 6c00f401b..241efc86f 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -18,54 +18,60 @@ public class CarService( /// /// Retrieves all car records and maps them to DTOs. /// - public async Task> ReadAll() => - await repository.ReadAll().Select(e => e.Adapt()).ToList(); + public async Task> ReadAll() + { + var rep = await repository.ReadAll(); + return rep.Select(e => e.Adapt()).ToList(); + } /// /// Retrieves a specific car by its identifier. /// - public CarDto? Read(int id) => - repository.Read(id)?.Adapt(); + public async Task Read(int id) + { + var rep = await repository.Read(id); + return rep.Adapt(); + } /// /// Creates a new car record after validating the associated model generation. /// /// Thrown when the specified ModelGenerationId does not exist. - public CarDto Create(CarCreateUpdateDto dto) + public async Task Create(CarCreateUpdateDto dto) { var entity = dto.Adapt(); - var fullGeneration = generationRepository.Read(dto.ModelGenerationId); + var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); if (fullGeneration == null) throw new Exception("Generation not found"); entity.ModelGeneration = fullGeneration; - - var id = repository.Create(entity); - var savedEntity = repository.Read(id); - + var id = await repository.Create(entity); + var savedEntity = await repository.Read(id); return savedEntity!.Adapt(); } /// /// Updates an existing car's information and its relationship with a model generation. /// - public bool Update(CarCreateUpdateDto dto, int id) + public async Task Update(CarCreateUpdateDto dto, int id) { - var existing = repository.Read(id); + var existing = await repository.Read(id); if (existing is null) return false; - dto.Adapt(existing); - var fullGeneration = generationRepository.Read(dto.ModelGenerationId); + var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); if (fullGeneration != null) { existing.ModelGeneration = fullGeneration; } - - return repository.Update(existing, id); + var res = await repository.Update(existing, id); + return res; } /// /// Deletes a car record by its identifier. /// - public bool Delete(int id) => repository.Delete(id); + public async Task Delete(int id) { + var rep = await repository.Delete(id); + return rep; + } } \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 79fdedbae..25c616071 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -14,39 +14,48 @@ public class ClientService(IBaseRepository repository) : IApplicationSer /// /// Retrieves a complete list of registered clients. /// - public List ReadAll() => - repository.ReadAll().Select(e => e.Adapt()).ToList(); + public async Task> ReadAll() { + var rep = await repository.ReadAll(); + return rep.Select(e => e.Adapt()).ToList(); + } /// /// Finds a specific client by their unique identifier. /// - public ClientDto? Read(int id) => - repository.Read(id)?.Adapt(); + public async Task Read(int id) { + var rep = await repository.Read(id); + return rep.Adapt(); + } /// /// Registers a new client in the system. /// - public ClientDto Create(ClientCreateUpdateDto dto) + public async Task Create(ClientCreateUpdateDto dto) { var entity = dto.Adapt(); - var id = repository.Create(entity); - var savedEntity = repository.Read(id); + var id = await repository.Create(entity); + var savedEntity = await repository.Read(id); return savedEntity!.Adapt(); } /// /// Updates an existing client's personal and contact information. /// - public bool Update(ClientCreateUpdateDto dto, int id) + public async Task Update(ClientCreateUpdateDto dto, int id) { - var existing = repository.Read(id); + var existing = await repository.Read(id); if (existing is null) return false; dto.Adapt(existing); - return repository.Update(existing, id); + var res = await repository.Update(existing, id); + return res; } /// /// Removes a client record from the database. /// - public bool Delete(int id) => repository.Delete(id); + public async Task Delete(int id) + { + var res = await repository.Delete(id); + return res; + } } \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 9b8bee27f..5ab333d41 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -18,12 +18,13 @@ public class RentService( /// /// Retrieves all rental records, performing safety checks for deleted clients to ensure data integrity during mapping. /// - public List ReadAll() + public async Task> ReadAll() { - var rents = repository.ReadAll(); + var rents = await repository.ReadAll(); foreach (var rent in rents) { - if (clientRepository.Read(rent.Client!.Id) == null) + var rep = await clientRepository.Read(rent.Client!.Id); + if (rep == null) { rent.Client = null!; } @@ -34,17 +35,20 @@ public List ReadAll() /// /// Retrieves a specific rental agreement by its identifier. /// - public RentDto? Read(int id) => - repository.Read(id)?.Adapt(); + public async Task Read(int id) + { + var rep = await repository.Read(id); + return rep.Adapt(); + } /// /// Creates a new rental agreement after validating that both the requested car and client exist. /// /// The created rental DTO, or null if validation fails. - public RentDto Create(RentCreateUpdateDto dto) + public async Task Create(RentCreateUpdateDto dto) { - var car = carRepository.Read(dto.CarId); - var client = clientRepository.Read(dto.ClientId); + var car = await carRepository.Read(dto.CarId); + var client = await clientRepository.Read(dto.ClientId); if (car == null || client == null) { throw new Exception("Car or client is not found"); @@ -53,8 +57,8 @@ public RentDto Create(RentCreateUpdateDto dto) entity.Car = car; entity.Client = client; - var id = repository.Create(entity); - var savedEntity = repository.Read(id); + var id = await repository.Create(entity); + var savedEntity = await repository.Read(id); return savedEntity!.Adapt(); } @@ -62,16 +66,21 @@ public RentDto Create(RentCreateUpdateDto dto) /// /// Updates an existing rental agreement's details. /// - public bool Update(RentCreateUpdateDto dto, int id) + public async Task Update(RentCreateUpdateDto dto, int id) { - var existing = repository.Read(id); + var existing = await repository.Read(id); if (existing is null) return false; dto.Adapt(existing); - return repository.Update(existing, id); + var res = await repository.Update(existing, id); + return res; } /// /// Permanently removes a rental record from the system. /// - public bool Delete(int id) => repository.Delete(id); + public async Task Delete(int id) + { + var res = await repository.Delete(id); + return res; + } } \ No newline at end of file From 3479fc48ee92818966cfe69d90b2eecfeee8062b Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 25 Dec 2025 00:35:43 +0400 Subject: [PATCH 24/37] updated MongoDBDriver and Aspire.Hosting.AppHost, now Aspire can runs the server and database --- CarRental.Api/Program.cs | 9 +++ CarRental.AppHost/CarRental.AppHost.csproj | 19 +++--- CarRental.AppHost/Program.cs | 3 +- .../CarRental.Infrastructure.csproj | 5 ++ .../CarRentalDbContext.cs | 58 +++++++++++++++++++ 5 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 CarRental.Infrastructure/CarRentalDbContext.cs diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index a8b0c64f4..777ada070 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -7,8 +7,11 @@ using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Infrastructure; using CarRental.Infrastructure.InMemoryRepository; using CarRental.ServiceDefaults; +using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; using Mapster; using MapsterMapper; @@ -55,6 +58,12 @@ } }); +builder.Services.AddDbContext((serviceProvider, options) => +{ + var client = serviceProvider.GetRequiredService(); + options.UseMongoDB(client, "CarRentalDb"); +}); + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental.AppHost/CarRental.AppHost.csproj index 30e9af443..e310f8e4b 100644 --- a/CarRental.AppHost/CarRental.AppHost.csproj +++ b/CarRental.AppHost/CarRental.AppHost.csproj @@ -1,16 +1,17 @@ - - Exe - net8.0 - enable - enable - true - 54701a95-76ef-4922-a1a8-8f3a20203073 - + + + Exe + net8.0 + enable + enable + true + 54701a95-76ef-4922-a1a8-8f3a20203073 + - + diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs index d5d748312..5d2cd09d4 100644 --- a/CarRental.AppHost/Program.cs +++ b/CarRental.AppHost/Program.cs @@ -1,10 +1,9 @@ var builder = DistributedApplication.CreateBuilder(args); var mongodb = builder.AddMongoDB("mongodb"); - var carDb = mongodb.AddDatabase("CarRentalDb"); builder.AddProject("carrental-api") - .WithReference(carDb); + .WithReference(carDb); builder.Build().Run(); diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj index e84c295a1..63a4ddb09 100644 --- a/CarRental.Infrastructure/CarRental.Infrastructure.csproj +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -6,6 +6,11 @@ + + + + + diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs new file mode 100644 index 000000000..4f92f19e5 --- /dev/null +++ b/CarRental.Infrastructure/CarRentalDbContext.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; +using MongoDB.EntityFrameworkCore.Extensions; + +namespace CarRental.Infrastructure; + +public class CarRentalDbContext : DbContext +{ + public DbSet Cars { get; init; } + public DbSet Clients { get; init; } + public DbSet Rents { get; init; } + public DbSet CarModels { get; init; } + public DbSet ModelGenerations { get; init; } + + public CarRentalDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().ToCollection("clients"); + modelBuilder.Entity().ToCollection("cars"); + modelBuilder.Entity().ToCollection("rents"); + modelBuilder.Entity().ToCollection("car_models"); + modelBuilder.Entity().ToCollection("model_generations"); + + modelBuilder.Entity().HasKey(c => c.Id); + modelBuilder.Entity().HasKey(c => c.Id); + modelBuilder.Entity().HasKey(r => r.Id); + modelBuilder.Entity().HasKey(m => m.Id); + modelBuilder.Entity().HasKey(g => g.Id); + + modelBuilder.Entity(entity => + { + entity.Property(c => c.BodyType).HasConversion(); + entity.Property(c => c.DriveType).HasConversion(); + entity.Property(c => c.ClassType).HasConversion(); + }); + + modelBuilder.Entity(entity => + { + entity.Property(c => c.TransmissionType).HasConversion(); + }); + modelBuilder.Entity(entity => + { + entity.Property(r => r.Duration).HasElementName("duration_hours"); + }); + + modelBuilder.Entity(entity => + { + entity.Property(c => c.BirthDate).HasElementName("birth_date"); + }); + } +} From c7312cfc419dd87d9f1664bc68f641016c2c563d Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 25 Dec 2025 13:05:10 +0400 Subject: [PATCH 25/37] tried to made good communication between database and server --- CarRental.Api/CarRental.Api.csproj | 2 +- .../Controllers/CarModelController.cs | 41 ++++++++ .../CarModelGenerationController.cs | 41 ++++++++ CarRental.Api/Program.cs | 97 +++++++++++++++---- CarRental.AppHost/Program.cs | 5 +- .../CarRental.Application.csproj | 3 +- CarRental.Application/CarRentalProfile.cs | 70 +++++++++++++ CarRental.Application/Contracts/Car/CarDto.cs | 4 +- .../Mapping/MappingConfig.cs | 40 -------- .../Services/AnalyticsService.cs | 72 +++++++------- .../Services/CarModelGenerationService.cs | 52 ++++++++++ .../Services/CarModelService.cs | 42 ++++++++ CarRental.Application/Services/CarService.cs | 52 ++++------ .../Services/ClientService.cs | 53 ++++------ CarRental.Application/Services/RentService.cs | 65 ++++--------- .../CarRental.Infrastructure.csproj | 5 - .../CarRentalDbContext.cs | 65 ++++++++----- CarRental.Infrastructure/DbInitializer.cs | 35 +++++++ .../DbCarModelGenerationRepository.cs | 37 +++++++ .../Repository/DbCarModelRepository.cs | 35 +++++++ .../Repository/DbCarRepository.cs | 35 +++++++ .../Repository/DbClientRepository.cs | 35 +++++++ .../Repository/DbRentRepository.cs | 37 +++++++ 23 files changed, 675 insertions(+), 248 deletions(-) create mode 100644 CarRental.Api/Controllers/CarModelController.cs create mode 100644 CarRental.Api/Controllers/CarModelGenerationController.cs create mode 100644 CarRental.Application/CarRentalProfile.cs delete mode 100644 CarRental.Application/Mapping/MappingConfig.cs create mode 100644 CarRental.Application/Services/CarModelGenerationService.cs create mode 100644 CarRental.Application/Services/CarModelService.cs create mode 100644 CarRental.Infrastructure/DbInitializer.cs create mode 100644 CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs create mode 100644 CarRental.Infrastructure/Repository/DbCarModelRepository.cs create mode 100644 CarRental.Infrastructure/Repository/DbCarRepository.cs create mode 100644 CarRental.Infrastructure/Repository/DbClientRepository.cs create mode 100644 CarRental.Infrastructure/Repository/DbRentRepository.cs diff --git a/CarRental.Api/CarRental.Api.csproj b/CarRental.Api/CarRental.Api.csproj index 7102a906e..6c96eaf1f 100644 --- a/CarRental.Api/CarRental.Api.csproj +++ b/CarRental.Api/CarRental.Api.csproj @@ -9,7 +9,7 @@ - + diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs new file mode 100644 index 000000000..d48dba1dd --- /dev/null +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -0,0 +1,41 @@ +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CarModelController(IApplicationService service) : ControllerBase +{ + [HttpGet] + public async Task>> GetAll() => Ok(await service.ReadAll()); + + [HttpGet("{id:int}")] + public async Task> Get(int id) + { + var result = await service.Read(id); + return result == null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task> Create(CarModelCreateUpdateDto dto) + { + var result = await service.Create(dto); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + [HttpPut("{id:int}")] + public async Task Update(int id, CarModelCreateUpdateDto dto) + { + var result = await service.Update(dto, id); + return result ? NoContent() : NotFound(); + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var result = await service.Delete(id); + return result ? NoContent() : NotFound(); + } +} \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs new file mode 100644 index 000000000..912068239 --- /dev/null +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -0,0 +1,41 @@ +using CarRental.Application.Contracts.CarModelGeneration; +using CarRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CarModelGenerationsController(IApplicationService service) : ControllerBase +{ + [HttpGet] + public async Task>> GetAll() => Ok(await service.ReadAll()); + + [HttpGet("{id:int}")] + public async Task> Get(int id) + { + var result = await service.Read(id); + return result == null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task> Create(CarModelGenerationCreateUpdateDto dto) + { + var result = await service.Create(dto); + return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + } + + [HttpPut("{id:int}")] + public async Task Update(int id, CarModelGenerationCreateUpdateDto dto) + { + var result = await service.Update(dto, id); + return result ? NoContent() : NotFound(); + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var result = await service.Delete(id); + return result ? NoContent() : NotFound(); + } +} \ No newline at end of file diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 777ada070..9f9b5752c 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -1,47 +1,70 @@ +using CarRental.Application; using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Contracts.CarModelGeneration; using CarRental.Application.Contracts.Client; using CarRental.Application.Contracts.Rent; using CarRental.Application.Interfaces; -using CarRental.Application.Mapping; using CarRental.Application.Services; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; +using CarRental.Domain.DataSeed; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Infrastructure; -using CarRental.Infrastructure.InMemoryRepository; +using CarRental.Infrastructure.Repository; using CarRental.ServiceDefaults; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; -using Mapster; -using MapsterMapper; var builder = WebApplication.CreateBuilder(args); + +// --- 1. Aspire MongoDB --- builder.AddServiceDefaults(); builder.AddMongoDBClient("CarRentalDb"); -builder.Services.AddSingleton(TypeAdapterConfig.GlobalSettings); -builder.Services.AddScoped(); +builder.Services.AddDbContext((serviceProvider, options) => +{ + var db = serviceProvider.GetRequiredService(); + options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); +}); + +// --- 2. AutoMapper ( Mapster) --- +// CarRentalProfile IMapper DI +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new CarRentalProfile()); +}); + +builder.Services.AddSingleton(); + +// --- 3. MONGODB --- +builder.Services.AddScoped, DbCarModelRepository>(); +builder.Services.AddScoped, DbCarModelGenerationRepository>(); +builder.Services.AddScoped, DbCarRepository>(); +builder.Services.AddScoped, DbClientRepository>(); +builder.Services.AddScoped, DbRentRepository>(); -builder.Services.AddScoped(); -builder.Services.AddScoped, CarModelRepository>(); -builder.Services.AddScoped, CarModelGenerationRepository>(); -builder.Services.AddScoped, CarRepository>(); -builder.Services.AddScoped, ClientRepository>(); -builder.Services.AddScoped, RentRepository>(); +// AnalyticsService ( ) +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +// --- 4. --- builder.Services.AddScoped, CarService>(); builder.Services.AddScoped, ClientService>(); builder.Services.AddScoped, RentService>(); +builder.Services.AddScoped, CarModelService>(); +builder.Services.AddScoped, CarModelGenerationService>(); builder.Services.AddScoped(); -MappingConfig.Configure(); - +// --- 5. Swagger --- builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; }); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { @@ -58,14 +81,49 @@ } }); -builder.Services.AddDbContext((serviceProvider, options) => +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// --- 6. (Seed) --- +using (var scope = app.Services.CreateScope()) { - var client = serviceProvider.GetRequiredService(); - options.UseMongoDB(client, "CarRentalDb"); -}); + var services = scope.ServiceProvider; + try + { + var context = services.GetRequiredService(); + var dataseed = services.GetRequiredService(); -var app = builder.Build(); + if (await context.CarModels.AnyAsync()) return; + + // 1. + await context.CarModels.AddRangeAsync(dataseed.Models); + await context.SaveChangesAsync(); + + // 2. ( ) + await context.ModelGenerations.AddRangeAsync(dataseed.Generations); + await context.SaveChangesAsync(); + + // 3. ( ) + await context.Cars.AddRangeAsync(dataseed.Cars); + await context.SaveChangesAsync(); + // 4. + await context.Clients.AddRangeAsync(dataseed.Clients); + await context.SaveChangesAsync(); + + // 5. ( ) + await context.Rents.AddRangeAsync(dataseed.Rents); + await context.SaveChangesAsync(); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred while seeding the database."); + } +} + +// --- 7. Pipeline --- if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -73,7 +131,6 @@ } app.UseHttpsRedirection(); - app.UseAuthorization(); app.MapControllers(); diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs index 5d2cd09d4..b2d0b91c6 100644 --- a/CarRental.AppHost/Program.cs +++ b/CarRental.AppHost/Program.cs @@ -1,9 +1,10 @@ var builder = DistributedApplication.CreateBuilder(args); var mongodb = builder.AddMongoDB("mongodb"); -var carDb = mongodb.AddDatabase("CarRentalDb"); +mongodb.AddDatabase("car-rental"); builder.AddProject("carrental-api") - .WithReference(carDb); + .WithReference(mongodb, "CarRentalDb") + .WaitFor(mongodb); builder.Build().Run(); diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index e8afa5560..a805aa631 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -6,8 +6,7 @@ - - + diff --git a/CarRental.Application/CarRentalProfile.cs b/CarRental.Application/CarRentalProfile.cs new file mode 100644 index 000000000..56fe90951 --- /dev/null +++ b/CarRental.Application/CarRentalProfile.cs @@ -0,0 +1,70 @@ +using CarRental.Domain; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Contracts.CarModelGeneration; +using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; +using AutoMapper; + +namespace CarRental.Application; + +public class CarRentalProfile : Profile +{ + public CarRentalProfile() + { + // --- 1. Client Mapping --- + CreateMap().ReverseMap(); + CreateMap(); + + // --- 2. CarModel Mapping (Enum to String) --- + CreateMap() + .ForMember(dest => dest.BodyType, opt => opt.MapFrom(src => src.BodyType.ToString())) + .ForMember(dest => dest.DriveType, opt => opt.MapFrom(src => src.DriveType.HasValue ? src.DriveType.Value.ToString() : null)) + .ForMember(dest => dest.ClassType, opt => opt.MapFrom(src => src.ClassType.HasValue ? src.ClassType.Value.ToString() : null)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()); + + // --- 3. CarModelGeneration Mapping --- + CreateMap() + .ForMember(dest => dest.ModelId, opt => opt.MapFrom(src => src.Model != null ? src.Model.Id : 0)) + .ForMember(dest => dest.TransmissionType, opt => opt.MapFrom(src => src.TransmissionType.HasValue ? src.TransmissionType.Value.ToString() : null)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.Model, opt => opt.Ignore()); + + // --- 4. Car Mapping (Flattening) --- + CreateMap() + .ForMember(dest => dest.GenerationId, opt => opt.MapFrom(src => src.ModelGeneration != null ? src.ModelGeneration.Id : 0)) + .ForMember(dest => dest.Year, opt => opt.MapFrom(src => src.ModelGeneration != null ? src.ModelGeneration.Year : 0)) + .ForMember(dest => dest.ModelName, opt => opt.MapFrom(src => + (src.ModelGeneration != null && src.ModelGeneration.Model != null) + ? src.ModelGeneration.Model.Name + : "Unknown Model")); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.ModelGeneration, opt => opt.Ignore()); + + // --- 5. Rent Mapping (Complex Logic) --- + CreateMap() + .ForMember(dest => dest.CarId, opt => opt.MapFrom(src => src.Car != null ? src.Car.Id : 0)) + .ForMember(dest => dest.ClientId, opt => opt.MapFrom(src => src.Client != null ? src.Client.Id : 0)) + .ForMember(dest => dest.ClientLastName, opt => opt.MapFrom(src => + src.Client != null ? src.Client.LastName : "Deleted")) + .ForMember(dest => dest.CarLicensePlate, opt => opt.MapFrom(src => + src.Car != null ? src.Car.NumberPlate : "Deleted")) + .ForMember(dest => dest.TotalCost, opt => opt.MapFrom(src => + (src.Car != null && src.Car.ModelGeneration != null) + ? (decimal)src.Duration * src.Car.ModelGeneration.HourCost + : 0)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.Car, opt => opt.Ignore()) + .ForMember(dest => dest.Client, opt => opt.Ignore()); + } +} \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs index aa8d1af99..e44f0eda2 100644 --- a/CarRental.Application/Contracts/Car/CarDto.cs +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -6,5 +6,7 @@ namespace CarRental.Application.Contracts.Car; /// The unique identifier of the car. /// The vehicle's license plate number. /// The color of the car. +/// ID of the model generation. /// The descriptive name of the car model. -public record CarDto(int Id, string NumberPlate, string Colour, string ModelName); \ No newline at end of file +/// Year of release +public record CarDto(int Id, string NumberPlate, string Colour, int GenerationId, string ModelName, int Year); \ No newline at end of file diff --git a/CarRental.Application/Mapping/MappingConfig.cs b/CarRental.Application/Mapping/MappingConfig.cs deleted file mode 100644 index e521c9b93..000000000 --- a/CarRental.Application/Mapping/MappingConfig.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Mapster; -using CarRental.Domain.DataModels; -using CarRental.Application.Contracts.Car; -using CarRental.Application.Contracts.Client; -using CarRental.Application.Contracts.Rent; - -namespace CarRental.Application.Mapping; - -/// -/// Provides global configuration for object-to-object mapping using Mapster. -/// Defines rules for converting domain entities into data transfer objects. -/// -public static class MappingConfig -{ - /// - /// Initializes and registers mapping configurations between domain models and DTOs. - /// - public static void Configure() - { - TypeAdapterConfig.GlobalSettings.Default.PreserveReference(false); - - // Client mapping - TypeAdapterConfig.NewConfig() - .MapToConstructor(true); - - // Car mapping with flattened ModelName - TypeAdapterConfig.NewConfig() - .MapToConstructor(true) - .Map(dest => dest.ModelName, src => src.ModelGeneration!.Model!.Name); - - // Rent mapping with complex logic for associated entities and costs - TypeAdapterConfig.NewConfig() - .MapToConstructor(true) - .Map(dest => dest.CarId, src => src.Car.Id) - .Map(dest => dest.ClientId, src => src.Client.Id) - .Map(dest => dest.ClientLastName, src => src.Client != null? src.Client.LastName: "Client is deleted") - .Map(dest => dest.CarLicensePlate, src => src.Car != null? src.Car.NumberPlate: "Car is deleted") - .Map(dest => dest.TotalCost, src => src.Car != null && src.Car.ModelGeneration != null? (decimal)src.Duration * src.Car.ModelGeneration.HourCost: 0); - } -} \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index c1aa2c10d..d9bed28af 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -1,59 +1,61 @@ -using CarRental.Application.Contracts.Analytics; +using AutoMapper; +using CarRental.Application.Contracts.Analytics; using CarRental.Application.Contracts.Client; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using Mapster; namespace CarRental.Application.Services; -/// -/// Implements business logic for data aggregation and rental statistics. -/// public class AnalyticsService( IBaseRepository rentRepository, - IBaseRepository carRepository) : IAnalyticsService + IBaseRepository carRepository, + IMapper mapper) : IAnalyticsService { - /// - /// Finds unique clients who rented cars of a specific model name. - /// public async Task> ReadClientsByModelName(string modelName) { - var clients = await rentRepository.ReadAll(); - return clients.Where(r => r.Car.ModelGeneration!.Model!.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) - .Select(r => r.Client.Adapt()) + var rents = await rentRepository.ReadAll(); + + return rents + .Where(r => r.Car?.ModelGeneration?.Model?.Name != null && + r.Car.ModelGeneration.Model.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + .Select(r => mapper.Map(r.Client)) .DistinctBy(c => c.Id) .ToList(); } - /// - /// Identifies the top 5 most frequently rented cars. - /// public async Task> ReadTop5MostRentedCars() { var rents = await rentRepository.ReadAll(); - return rents.GroupBy(r => r.Car.Id) - .Select(g => new CarWithRentalCountDto( - g.First().Car.Id, - g.First().Car.ModelGeneration?.Model!.Name ?? "Unknown", - g.First().Car.NumberPlate, - g.Count() - )) + + return rents + .Where(r => r.Car != null) + .GroupBy(r => r.Car.Id) + .Select(g => { + var firstRent = g.First(); + return new CarWithRentalCountDto( + firstRent.Car.Id, + firstRent.Car.ModelGeneration?.Model?.Name ?? "Unknown", + firstRent.Car.NumberPlate, + g.Count() + ); + }) .OrderByDescending(x => x.RentalCount) .Take(5) .ToList(); } - /// - /// Retrieves cars that were actively rented at a specific point in time. - /// public async Task> ReadCarsInRent(DateTime atTime) { var rents = await rentRepository.ReadAll(); - return rents.Where(r => r.StartDateTime <= atTime && r.StartDateTime.AddHours(r.Duration) >= atTime) + + return rents + .Where(r => r.Car != null && + r.StartDateTime <= atTime && + r.StartDateTime.AddHours(r.Duration) >= atTime) .Select(r => new CarInRentDto( r.Car.Id, - r.Car.ModelGeneration?.Model!.Name ?? "Unknown", + r.Car.ModelGeneration?.Model?.Name ?? "Unknown", r.Car.NumberPlate, r.StartDateTime, (int)r.Duration @@ -61,28 +63,26 @@ public async Task> ReadCarsInRent(DateTime atTime) .ToList(); } - /// - /// Lists all cars and their total rental frequency. - /// public async Task> ReadAllCarsWithRentalCount() { var allRents = await rentRepository.ReadAll(); var allCars = await carRepository.ReadAll(); + return allCars.Select(car => new CarWithRentalCountDto( car.Id, - car.ModelGeneration?.Model!.Name ?? "Unknown", + car.ModelGeneration?.Model?.Name ?? "Unknown", car.NumberPlate, - allRents.Count(r => r.Car.Id == car.Id) + allRents.Count(r => r.Car?.Id == car.Id) )).ToList(); } - /// - /// Identifies the top 5 clients by total revenue generated. - /// public async Task> ReadTop5ClientsByTotalAmount() { var rents = await rentRepository.ReadAll(); - return rents.GroupBy(r => r.Client.Id) + + return rents + .Where(r => r.Client != null && r.Car?.ModelGeneration != null) + .GroupBy(r => r.Client.Id) .Select(g => { var client = g.First().Client; var totalAmount = g.Sum(r => (decimal)r.Duration * (r.Car.ModelGeneration?.HourCost ?? 0)); diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs new file mode 100644 index 000000000..375454498 --- /dev/null +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -0,0 +1,52 @@ +using AutoMapper; +using CarRental.Application.Contracts.CarModelGeneration; +using CarRental.Application.Interfaces; +using CarRental.Domain.Interfaces; +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Application.Services; + +public class CarModelGenerationService( + IBaseRepository repository, + IBaseRepository modelRepository, + IMapper mapper) + : IApplicationService +{ + public async Task Create(CarModelGenerationCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var model = await modelRepository.Read(dto.ModelId); + entity.Model = model; + + var id = await repository.Create(entity); + entity.Id = id; + + return mapper.Map(entity); + } + + public async Task Read(int id) + { + var entity = await repository.Read(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> ReadAll() + { + var entities = await repository.ReadAll(); + return mapper.Map>(entities); + } + + public async Task Update(CarModelGenerationCreateUpdateDto dto, int id) + { + var existing = await repository.Read(id); + if (existing == null) return false; + + mapper.Map(dto, existing); + var model = await modelRepository.Read(dto.ModelId); + existing.Model = model; + + return await repository.Update(existing, id); + } + + public async Task Delete(int id) => await repository.Delete(id); +} \ No newline at end of file diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs new file mode 100644 index 000000000..ee5f51d82 --- /dev/null +++ b/CarRental.Application/Services/CarModelService.cs @@ -0,0 +1,42 @@ +using AutoMapper; +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Interfaces; +using CarRental.Domain.Interfaces; +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Application.Services; + +public class CarModelService(IBaseRepository repository, IMapper mapper) + : IApplicationService +{ + public async Task Create(CarModelCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var id = await repository.Create(entity); + entity.Id = id; + return mapper.Map(entity); + } + + public async Task Read(int id) + { + var entity = await repository.Read(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> ReadAll() + { + var entities = await repository.ReadAll(); + return mapper.Map>(entities); + } + + public async Task Update(CarModelCreateUpdateDto dto, int id) + { + var existing = await repository.Read(id); + if (existing == null) return false; + + mapper.Map(dto, existing); // Обновляем существующий объект + return await repository.Update(existing, id); + } + + public async Task Delete(int id) => await repository.Delete(id); +} \ No newline at end of file diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index 241efc86f..ecc4bd98a 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -1,4 +1,4 @@ -using Mapster; +using AutoMapper; using CarRental.Application.Contracts.Car; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; @@ -7,71 +7,53 @@ namespace CarRental.Application.Services; -/// -/// Provides CRUD operations for car entities, including relationship management with car model generations. -/// public class CarService( IBaseRepository repository, - IBaseRepository generationRepository) + IBaseRepository generationRepository, + IMapper mapper) : IApplicationService { - /// - /// Retrieves all car records and maps them to DTOs. - /// public async Task> ReadAll() { - var rep = await repository.ReadAll(); - return rep.Select(e => e.Adapt()).ToList(); - } + var entities = await repository.ReadAll(); + return mapper.Map>(entities); + } - /// - /// Retrieves a specific car by its identifier. - /// public async Task Read(int id) { - var rep = await repository.Read(id); - return rep.Adapt(); + var entity = await repository.Read(id); + return entity == null ? null : mapper.Map(entity); } - /// - /// Creates a new car record after validating the associated model generation. - /// - /// Thrown when the specified ModelGenerationId does not exist. public async Task Create(CarCreateUpdateDto dto) { - var entity = dto.Adapt(); + var entity = mapper.Map(dto); var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); if (fullGeneration == null) throw new Exception("Generation not found"); + entity.ModelGeneration = fullGeneration; var id = await repository.Create(entity); var savedEntity = await repository.Read(id); - return savedEntity!.Adapt(); + return mapper.Map(savedEntity!); } - /// - /// Updates an existing car's information and its relationship with a model generation. - /// public async Task Update(CarCreateUpdateDto dto, int id) { var existing = await repository.Read(id); if (existing is null) return false; - dto.Adapt(existing); + + mapper.Map(dto, existing); + var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); if (fullGeneration != null) { existing.ModelGeneration = fullGeneration; } - var res = await repository.Update(existing, id); - return res; - } - /// - /// Deletes a car record by its identifier. - /// - public async Task Delete(int id) { - var rep = await repository.Delete(id); - return rep; + return await repository.Update(existing, id); } + + public async Task Delete(int id) => await repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 25c616071..2ba08ed04 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -1,4 +1,4 @@ -using Mapster; +using AutoMapper; using CarRental.Application.Contracts.Client; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; @@ -6,56 +6,37 @@ namespace CarRental.Application.Services; -/// -/// Manages client-related operations, including registration and profile management. -/// -public class ClientService(IBaseRepository repository) : IApplicationService +public class ClientService(IBaseRepository repository, IMapper mapper) + : IApplicationService { - /// - /// Retrieves a complete list of registered clients. - /// - public async Task> ReadAll() { - var rep = await repository.ReadAll(); - return rep.Select(e => e.Adapt()).ToList(); + public async Task> ReadAll() + { + var entities = await repository.ReadAll(); + return mapper.Map>(entities); } - /// - /// Finds a specific client by their unique identifier. - /// - public async Task Read(int id) { - var rep = await repository.Read(id); - return rep.Adapt(); + public async Task Read(int id) + { + var entity = await repository.Read(id); + return entity == null ? null : mapper.Map(entity); } - /// - /// Registers a new client in the system. - /// public async Task Create(ClientCreateUpdateDto dto) { - var entity = dto.Adapt(); + var entity = mapper.Map(dto); var id = await repository.Create(entity); var savedEntity = await repository.Read(id); - return savedEntity!.Adapt(); + return mapper.Map(savedEntity!); } - /// - /// Updates an existing client's personal and contact information. - /// public async Task Update(ClientCreateUpdateDto dto, int id) { var existing = await repository.Read(id); if (existing is null) return false; - dto.Adapt(existing); - var res = await repository.Update(existing, id); - return res; - } - /// - /// Removes a client record from the database. - /// - public async Task Delete(int id) - { - var res = await repository.Delete(id); - return res; + mapper.Map(dto, existing); + return await repository.Update(existing, id); } + + public async Task Delete(int id) => await repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 5ab333d41..3fb4ea507 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -1,86 +1,63 @@ +using AutoMapper; using CarRental.Application.Contracts.Rent; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using Mapster; namespace CarRental.Application.Services; -/// -/// Managing associations between cars, clients, and rental periods. -/// public class RentService( IBaseRepository repository, IBaseRepository carRepository, - IBaseRepository clientRepository) + IBaseRepository clientRepository, + IMapper mapper) : IApplicationService { - /// - /// Retrieves all rental records, performing safety checks for deleted clients to ensure data integrity during mapping. - /// public async Task> ReadAll() { var rents = await repository.ReadAll(); - foreach (var rent in rents) - { - var rep = await clientRepository.Read(rent.Client!.Id); - if (rep == null) - { - rent.Client = null!; - } - } - return rents.Select(r => r.Adapt()).ToList(); + return mapper.Map>(rents); } - /// - /// Retrieves a specific rental agreement by its identifier. - /// public async Task Read(int id) { - var rep = await repository.Read(id); - return rep.Adapt(); + var entity = await repository.Read(id); + return entity == null ? null : mapper.Map(entity); } - /// - /// Creates a new rental agreement after validating that both the requested car and client exist. - /// - /// The created rental DTO, or null if validation fails. public async Task Create(RentCreateUpdateDto dto) { var car = await carRepository.Read(dto.CarId); var client = await clientRepository.Read(dto.ClientId); + if (car == null || client == null) - { throw new Exception("Car or client is not found"); - } - var entity = dto.Adapt(); + + var entity = mapper.Map(dto); entity.Car = car; entity.Client = client; var id = await repository.Create(entity); var savedEntity = await repository.Read(id); - return savedEntity!.Adapt(); + return mapper.Map(savedEntity!); } - /// - /// Updates an existing rental agreement's details. - /// public async Task Update(RentCreateUpdateDto dto, int id) { var existing = await repository.Read(id); if (existing is null) return false; - dto.Adapt(existing); - var res = await repository.Update(existing, id); - return res; - } - /// - /// Permanently removes a rental record from the system. - /// - public async Task Delete(int id) - { - var res = await repository.Delete(id); - return res; + mapper.Map(dto, existing); + + var car = await carRepository.Read(dto.CarId); + var client = await clientRepository.Read(dto.ClientId); + + if (car != null) existing.Car = car; + if (client != null) existing.Client = client; + + return await repository.Update(existing, id); } + + public async Task Delete(int id) => await repository.Delete(id); } \ No newline at end of file diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj index 63a4ddb09..151629217 100644 --- a/CarRental.Infrastructure/CarRental.Infrastructure.csproj +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -5,14 +5,9 @@ - - - - - net8.0 enable diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs index 4f92f19e5..29a77a28b 100644 --- a/CarRental.Infrastructure/CarRentalDbContext.cs +++ b/CarRental.Infrastructure/CarRentalDbContext.cs @@ -5,7 +5,7 @@ namespace CarRental.Infrastructure; -public class CarRentalDbContext : DbContext +public class CarRentalDbContext(DbContextOptions options) : DbContext(options) { public DbSet Cars { get; init; } public DbSet Clients { get; init; } @@ -13,46 +13,59 @@ public class CarRentalDbContext : DbContext public DbSet CarModels { get; init; } public DbSet ModelGenerations { get; init; } - public CarRentalDbContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().ToCollection("clients"); - modelBuilder.Entity().ToCollection("cars"); - modelBuilder.Entity().ToCollection("rents"); - modelBuilder.Entity().ToCollection("car_models"); - modelBuilder.Entity().ToCollection("model_generations"); + // В MongoDB EF Core транзакции не поддерживаются в базовом режиме + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - modelBuilder.Entity().HasKey(c => c.Id); - modelBuilder.Entity().HasKey(c => c.Id); - modelBuilder.Entity().HasKey(r => r.Id); - modelBuilder.Entity().HasKey(m => m.Id); - modelBuilder.Entity().HasKey(g => g.Id); + // 1. Машины (Car) + modelBuilder.Entity(builder => + { + builder.ToCollection("cars"); + builder.HasKey(c => c.Id); + builder.Property(c => c.Id).HasElementName("_id"); + // Остальные свойства маппятся автоматически, + // но если нужно изменить имя поля в базе, используй .HasElementName("имя") + }); - modelBuilder.Entity(entity => + // 2. Клиенты (Client) + modelBuilder.Entity(builder => { - entity.Property(c => c.BodyType).HasConversion(); - entity.Property(c => c.DriveType).HasConversion(); - entity.Property(c => c.ClassType).HasConversion(); + builder.ToCollection("clients"); + builder.HasKey(cl => cl.Id); + builder.Property(cl => cl.Id).HasElementName("_id"); }); - modelBuilder.Entity(entity => + // 3. Аренда (Rent) + modelBuilder.Entity(builder => { - entity.Property(c => c.TransmissionType).HasConversion(); + builder.ToCollection("rents"); + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).HasElementName("_id"); + + // Маппинг внешних ключей (если они есть как свойства в модели) + // builder.Property(r => r.CarId).HasElementName("car_id"); + // builder.Property(r => r.ClientId).HasElementName("client_id"); }); - modelBuilder.Entity(entity => + + // 4. Модели (CarModel) + modelBuilder.Entity(builder => { - entity.Property(r => r.Duration).HasElementName("duration_hours"); + builder.ToCollection("car_models"); + builder.HasKey(m => m.Id); + builder.Property(m => m.Id).HasElementName("_id"); }); - modelBuilder.Entity(entity => + // 5. Поколения (CarModelGeneration) + modelBuilder.Entity(builder => { - entity.Property(c => c.BirthDate).HasElementName("birth_date"); + builder.ToCollection("model_generations"); + builder.HasKey(g => g.Id); + builder.Property(g => g.Id).HasElementName("_id"); + + // builder.Property(g => g.ModelId).HasElementName("model_id"); }); } } diff --git a/CarRental.Infrastructure/DbInitializer.cs b/CarRental.Infrastructure/DbInitializer.cs new file mode 100644 index 000000000..401735db2 --- /dev/null +++ b/CarRental.Infrastructure/DbInitializer.cs @@ -0,0 +1,35 @@ +using CarRental.Domain.DataSeed; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Infrastructure; + +public static class DbInitializer +{ + public static async Task SeedData(CarRentalDbContext context) + { + // Проверяем, есть ли данные в базе. Если уже есть хотя бы одна модель — ничего не делаем. + if (await context.CarModels.AnyAsync()) return; + + var data = new DataSeed(); + + // 1. Сначала добавляем базовые модели + await context.CarModels.AddRangeAsync(data.Models); + await context.SaveChangesAsync(); + + // 2. Затем поколения (они ссылаются на модели) + await context.ModelGenerations.AddRangeAsync(data.Generations); + await context.SaveChangesAsync(); + + // 3. Машины (ссылаются на поколения) + await context.Cars.AddRangeAsync(data.Cars); + await context.SaveChangesAsync(); + + // 4. Клиентов + await context.Clients.AddRangeAsync(data.Clients); + await context.SaveChangesAsync(); + + // 5. И в конце аренду (ссылается на машины и клиентов) + await context.Rents.AddRangeAsync(data.Rents); + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs new file mode 100644 index 000000000..8475caaba --- /dev/null +++ b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure; + +namespace CarRental.Infrastructure.Repository; + +public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseRepository +{ + public async Task> ReadAll() => + await context.ModelGenerations.Include(g => g.Model).ToListAsync(); + + public async Task Read(int id) => + (await context.ModelGenerations.Include(g => g.Model).ToListAsync()) + .FirstOrDefault(x => x.Id == id); + + public async Task Create(CarModelGeneration entity) + { + await context.ModelGenerations.AddAsync(entity); + await context.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(CarModelGeneration entity, int id) + { + context.ModelGenerations.Update(entity); + return await context.SaveChangesAsync() > 0; + } + + public async Task Delete(int id) + { + var entity = await Read(id); + if (entity is null) return false; + context.ModelGenerations.Remove(entity); + return await context.SaveChangesAsync() > 0; + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs new file mode 100644 index 000000000..81fcc2f6f --- /dev/null +++ b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure; + +namespace CarRental.Infrastructure.Repository; + +public class DbCarModelRepository(CarRentalDbContext context) : IBaseRepository +{ + public async Task> ReadAll() => await context.CarModels.ToListAsync(); + + public async Task Read(int id) => + (await context.CarModels.ToListAsync()).FirstOrDefault(x => x.Id == id); + + public async Task Create(CarModel entity) + { + await context.CarModels.AddAsync(entity); + await context.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(CarModel entity, int id) + { + context.CarModels.Update(entity); + return await context.SaveChangesAsync() > 0; + } + + public async Task Delete(int id) + { + var entity = await Read(id); + if (entity is null) return false; + context.CarModels.Remove(entity); + return await context.SaveChangesAsync() > 0; + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarRepository.cs b/CarRental.Infrastructure/Repository/DbCarRepository.cs new file mode 100644 index 000000000..06321ac36 --- /dev/null +++ b/CarRental.Infrastructure/Repository/DbCarRepository.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure; + +namespace CarRental.Infrastructure.Repository; + +public class DbCarRepository(CarRentalDbContext context) : IBaseRepository +{ + public async Task> ReadAll() => await context.Cars.ToListAsync(); + + public async Task Read(int id) => + (await context.Cars.ToListAsync()).FirstOrDefault(x => x.Id == id); + + public async Task Create(Car entity) + { + await context.Cars.AddAsync(entity); + await context.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Car entity, int id) + { + context.Cars.Update(entity); + return await context.SaveChangesAsync() > 0; + } + + public async Task Delete(int id) + { + var entity = await Read(id); + if (entity is null) return false; + context.Cars.Remove(entity); + return await context.SaveChangesAsync() > 0; + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbClientRepository.cs b/CarRental.Infrastructure/Repository/DbClientRepository.cs new file mode 100644 index 000000000..ebb8ffc00 --- /dev/null +++ b/CarRental.Infrastructure/Repository/DbClientRepository.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure; + +namespace CarRental.Infrastructure.Repository; + +public class DbClientRepository(CarRentalDbContext context) : IBaseRepository +{ + public async Task> ReadAll() => await context.Clients.ToListAsync(); + + public async Task Read(int id) => + (await context.Clients.ToListAsync()).FirstOrDefault(x => x.Id == id); + + public async Task Create(Client entity) + { + await context.Clients.AddAsync(entity); + await context.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Client entity, int id) + { + context.Clients.Update(entity); + return await context.SaveChangesAsync() > 0; + } + + public async Task Delete(int id) + { + var entity = await Read(id); + if (entity is null) return false; + context.Clients.Remove(entity); + return await context.SaveChangesAsync() > 0; + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbRentRepository.cs b/CarRental.Infrastructure/Repository/DbRentRepository.cs new file mode 100644 index 000000000..16a367a8a --- /dev/null +++ b/CarRental.Infrastructure/Repository/DbRentRepository.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using CarRental.Domain.DataModels; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure; + +namespace CarRental.Infrastructure.Repository; + +public class DbRentRepository(CarRentalDbContext context) : IBaseRepository +{ + public async Task> ReadAll() => + await context.Rents.Include(r => r.Car).Include(r => r.Client).ToListAsync(); + + public async Task Read(int id) => + (await context.Rents.Include(r => r.Car).Include(r => r.Client).ToListAsync()) + .FirstOrDefault(x => x.Id == id); + + public async Task Create(Rent entity) + { + await context.Rents.AddAsync(entity); + await context.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Rent entity, int id) + { + context.Rents.Update(entity); + return await context.SaveChangesAsync() > 0; + } + + public async Task Delete(int id) + { + var entity = await Read(id); + if (entity is null) return false; + context.Rents.Remove(entity); + return await context.SaveChangesAsync() > 0; + } +} \ No newline at end of file From 985cb5685a1d38e0b95ffb72e5d1aaa84b5221d7 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Tue, 30 Dec 2025 22:29:31 +0400 Subject: [PATCH 26/37] changed type for all ID fields and remaked repositories for working with database --- CarRental.Api/Controllers/CarControllers.cs | 8 +- .../Controllers/CarModelController.cs | 8 +- .../CarModelGenerationController.cs | 8 +- CarRental.Api/Controllers/ClientController.cs | 8 +- CarRental.Api/Controllers/RentController.cs | 8 +- CarRental.Api/Program.cs | 40 ++- CarRental.Application/CarRentalProfile.cs | 90 +++---- .../Contracts/Analytics/CarInRentDto.cs | 2 +- .../Analytics/CarWithRentalCountDto.cs | 2 +- .../Analytics/ClientWithTotalAmountDto.cs | 2 +- .../Contracts/Car/CarCreateUpdateDto.cs | 2 +- CarRental.Application/Contracts/Car/CarDto.cs | 6 +- .../Contracts/CarModel/CarModelDto.cs | 2 +- .../CarModelGenerationCreateUpdateDto.cs | 2 +- .../CarModelGenerationDto.cs | 2 +- .../Contracts/Client/ClientCreateUpdateDto.cs | 2 +- .../Contracts/Client/ClientDto.cs | 2 +- .../Contracts/Rent/RentCreateUpdateDto.cs | 2 +- .../Contracts/Rent/RentDto.cs | 2 +- .../Interfaces/IApplicationService.cs | 9 +- .../Services/AnalyticsService.cs | 70 +++-- .../Services/CarModelGenerationService.cs | 49 +++- .../Services/CarModelService.cs | 31 ++- CarRental.Application/Services/CarService.cs | 54 ++-- .../Services/ClientService.cs | 34 ++- CarRental.Application/Services/RentService.cs | 57 ++-- CarRental.Domain/DataModels/Car.cs | 7 +- CarRental.Domain/DataModels/Client.cs | 2 +- CarRental.Domain/DataModels/Rent.cs | 12 +- CarRental.Domain/DataSeed/DataSeed.cs | 250 +++++++++--------- CarRental.Domain/Interfaces/BaseRepository.cs | 64 ++--- .../Interfaces/IBaseRepository.cs | 12 +- .../InternalData/ComponentClasses/CarModel.cs | 2 +- .../ComponentClasses/CarModelGeneration.cs | 9 +- .../CarRentalDbContext.cs | 6 +- .../CarModelGenerationRepository.cs | 4 +- .../InMemoryRepository/CarModelRepository.cs | 4 +- .../InMemoryRepository/CarRepository.cs | 4 +- .../InMemoryRepository/ClientRepository.cs | 4 +- .../InMemoryRepository/RentRepository.cs | 4 +- .../DbCarModelGenerationRepository.cs | 32 ++- .../Repository/DbCarModelRepository.cs | 10 +- .../Repository/DbCarRepository.cs | 10 +- .../Repository/DbClientRepository.cs | 10 +- .../Repository/DbRentRepository.cs | 34 ++- CarRental.Tests/DomainTests.cs | 56 +++- 46 files changed, 581 insertions(+), 457 deletions(-) diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index b8202a850..b7db68675 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -9,7 +9,7 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class CarController(IApplicationService carService, ILogger logger) : ControllerBase +public class CarController(IApplicationService carService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all cars available in the system @@ -41,7 +41,7 @@ public async Task>> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public async Task> Get(int id) + public async Task> Get(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try @@ -93,7 +93,7 @@ public async Task> Create([FromBody] CarCreateUpdateDto dto [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public async Task> Update(int id, [FromBody] CarCreateUpdateDto dto) + public async Task> Update(Guid id, [FromBody] CarCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try @@ -117,7 +117,7 @@ public async Task> Update(int id, [FromBody] CarCreateUpdat [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public async Task Delete(int id) + public async Task Delete(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs index d48dba1dd..390c78afd 100644 --- a/CarRental.Api/Controllers/CarModelController.cs +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -6,13 +6,13 @@ namespace CarRental.Api.Controllers; [ApiController] [Route("api/[controller]")] -public class CarModelController(IApplicationService service) : ControllerBase +public class CarModelController(IApplicationService service) : ControllerBase { [HttpGet] public async Task>> GetAll() => Ok(await service.ReadAll()); [HttpGet("{id:int}")] - public async Task> Get(int id) + public async Task> Get(Guid id) { var result = await service.Read(id); return result == null ? NotFound() : Ok(result); @@ -26,14 +26,14 @@ public async Task> Create(CarModelCreateUpdateDto dto) } [HttpPut("{id:int}")] - public async Task Update(int id, CarModelCreateUpdateDto dto) + public async Task Update(Guid id, CarModelCreateUpdateDto dto) { var result = await service.Update(dto, id); return result ? NoContent() : NotFound(); } [HttpDelete("{id:int}")] - public async Task Delete(int id) + public async Task Delete(Guid id) { var result = await service.Delete(id); return result ? NoContent() : NotFound(); diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs index 912068239..94c4fd82c 100644 --- a/CarRental.Api/Controllers/CarModelGenerationController.cs +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -6,13 +6,13 @@ namespace CarRental.Api.Controllers; [ApiController] [Route("api/[controller]")] -public class CarModelGenerationsController(IApplicationService service) : ControllerBase +public class CarModelGenerationsController(IApplicationService service) : ControllerBase { [HttpGet] public async Task>> GetAll() => Ok(await service.ReadAll()); [HttpGet("{id:int}")] - public async Task> Get(int id) + public async Task> Get(Guid id) { var result = await service.Read(id); return result == null ? NotFound() : Ok(result); @@ -26,14 +26,14 @@ public async Task> Create(CarModelGeneration } [HttpPut("{id:int}")] - public async Task Update(int id, CarModelGenerationCreateUpdateDto dto) + public async Task Update(Guid id, CarModelGenerationCreateUpdateDto dto) { var result = await service.Update(dto, id); return result ? NoContent() : NotFound(); } [HttpDelete("{id:int}")] - public async Task Delete(int id) + public async Task Delete(Guid id) { var result = await service.Delete(id); return result ? NoContent() : NotFound(); diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index e0a4d0cbb..07a87f850 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -9,7 +9,7 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class ClientController(IApplicationService clientService, ILogger logger) : ControllerBase +public class ClientController(IApplicationService clientService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all registered clients @@ -41,7 +41,7 @@ public async Task>> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public async Task> Get(int id) + public async Task> Get(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try @@ -93,7 +93,7 @@ public async Task> Create(ClientCreateUpdateDto dto) [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public async Task Update(int id, ClientCreateUpdateDto dto) + public async Task Update(Guid id, ClientCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try @@ -117,7 +117,7 @@ public async Task Update(int id, ClientCreateUpdateDto dto) [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public async Task Delete(int id) + public async Task Delete(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index 1db5860d4..96dd62c79 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -9,7 +9,7 @@ namespace CarRental.Api.Controllers; /// [ApiController] [Route("api/[controller]")] -public class RentController(IApplicationService rentService, ILogger logger) : ControllerBase +public class RentController(IApplicationService rentService, ILogger logger) : ControllerBase { /// /// Retrieves a list of all rental records, including calculated costs and linked entity names @@ -41,7 +41,7 @@ public async Task>> GetAll() [ProducesResponseType(200)] [ProducesResponseType(404)] [ProducesResponseType(500)] - public async Task> Get(int id) + public async Task> Get(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); try @@ -93,7 +93,7 @@ public async Task> Create(RentCreateUpdateDto dto) [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] - public async Task Update(int id, RentCreateUpdateDto dto) + public async Task Update(Guid id, RentCreateUpdateDto dto) { logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); try @@ -117,7 +117,7 @@ public async Task Update(int id, RentCreateUpdateDto dto) [ProducesResponseType(200)] [ProducesResponseType(204)] [ProducesResponseType(500)] - public async Task Delete(int id) + public async Task Delete(Guid id) { logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 9f9b5752c..1a685e611 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -18,7 +18,6 @@ var builder = WebApplication.CreateBuilder(args); -// --- 1. Aspire MongoDB --- builder.AddServiceDefaults(); builder.AddMongoDBClient("CarRentalDb"); @@ -28,8 +27,12 @@ options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); }); -// --- 2. AutoMapper ( Mapster) --- -// CarRentalProfile IMapper DI +builder.Services.AddSingleton(sp => +{ + var client = sp.GetRequiredService(); + return client.GetDatabase("car-rental"); +}); + builder.Services.AddAutoMapper(config => { config.AddProfile(new CarRentalProfile()); @@ -37,28 +40,24 @@ builder.Services.AddSingleton(); -// --- 3. MONGODB --- -builder.Services.AddScoped, DbCarModelRepository>(); -builder.Services.AddScoped, DbCarModelGenerationRepository>(); -builder.Services.AddScoped, DbCarRepository>(); -builder.Services.AddScoped, DbClientRepository>(); -builder.Services.AddScoped, DbRentRepository>(); +builder.Services.AddScoped, DbCarModelRepository>(); +builder.Services.AddScoped, DbCarModelGenerationRepository>(); +builder.Services.AddScoped, DbCarRepository>(); +builder.Services.AddScoped, DbClientRepository>(); +builder.Services.AddScoped, DbRentRepository>(); -// AnalyticsService ( ) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -// --- 4. --- -builder.Services.AddScoped, CarService>(); -builder.Services.AddScoped, ClientService>(); -builder.Services.AddScoped, RentService>(); -builder.Services.AddScoped, CarModelService>(); -builder.Services.AddScoped, CarModelGenerationService>(); +builder.Services.AddScoped, CarService>(); +builder.Services.AddScoped, ClientService>(); +builder.Services.AddScoped, RentService>(); +builder.Services.AddScoped, CarModelService>(); +builder.Services.AddScoped, CarModelGenerationService>(); builder.Services.AddScoped(); -// --- 5. Swagger --- builder.Services.AddControllers() .AddJsonOptions(options => { @@ -85,7 +84,6 @@ app.MapDefaultEndpoints(); -// --- 6. (Seed) --- using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; @@ -96,23 +94,18 @@ if (await context.CarModels.AnyAsync()) return; - // 1. await context.CarModels.AddRangeAsync(dataseed.Models); await context.SaveChangesAsync(); - // 2. ( ) await context.ModelGenerations.AddRangeAsync(dataseed.Generations); await context.SaveChangesAsync(); - // 3. ( ) await context.Cars.AddRangeAsync(dataseed.Cars); await context.SaveChangesAsync(); - // 4. await context.Clients.AddRangeAsync(dataseed.Clients); await context.SaveChangesAsync(); - // 5. ( ) await context.Rents.AddRangeAsync(dataseed.Rents); await context.SaveChangesAsync(); } @@ -123,7 +116,6 @@ } } -// --- 7. Pipeline --- if (app.Environment.IsDevelopment()) { app.UseSwagger(); diff --git a/CarRental.Application/CarRentalProfile.cs b/CarRental.Application/CarRentalProfile.cs index 56fe90951..f96abc321 100644 --- a/CarRental.Application/CarRentalProfile.cs +++ b/CarRental.Application/CarRentalProfile.cs @@ -1,12 +1,15 @@ -using CarRental.Domain; +using AutoMapper; using CarRental.Application.Contracts.Car; -using CarRental.Application.Contracts.Client; -using CarRental.Application.Contracts.Rent; using CarRental.Application.Contracts.CarModel; using CarRental.Application.Contracts.CarModelGeneration; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; using CarRental.Domain.DataModels; using CarRental.Domain.InternalData.ComponentClasses; -using AutoMapper; +using DriveTypeEnum = CarRental.Domain.InternalData.ComponentEnums.DriveType; +using ClassTypeEnum = CarRental.Domain.InternalData.ComponentEnums.ClassType; +using BodyTypeEnum = CarRental.Domain.InternalData.ComponentEnums.BodyType; +using TransmissionTypeEnum = CarRental.Domain.InternalData.ComponentEnums.TransmissionType; namespace CarRental.Application; @@ -14,57 +17,34 @@ public class CarRentalProfile : Profile { public CarRentalProfile() { - // --- 1. Client Mapping --- - CreateMap().ReverseMap(); + // ===================== + // Client + // ===================== + CreateMap(); CreateMap(); - // --- 2. CarModel Mapping (Enum to String) --- - CreateMap() - .ForMember(dest => dest.BodyType, opt => opt.MapFrom(src => src.BodyType.ToString())) - .ForMember(dest => dest.DriveType, opt => opt.MapFrom(src => src.DriveType.HasValue ? src.DriveType.Value.ToString() : null)) - .ForMember(dest => dest.ClassType, opt => opt.MapFrom(src => src.ClassType.HasValue ? src.ClassType.Value.ToString() : null)); - - CreateMap() - .ForMember(dest => dest.Id, opt => opt.Ignore()); - - // --- 3. CarModelGeneration Mapping --- - CreateMap() - .ForMember(dest => dest.ModelId, opt => opt.MapFrom(src => src.Model != null ? src.Model.Id : 0)) - .ForMember(dest => dest.TransmissionType, opt => opt.MapFrom(src => src.TransmissionType.HasValue ? src.TransmissionType.Value.ToString() : null)); - - CreateMap() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.Model, opt => opt.Ignore()); - - // --- 4. Car Mapping (Flattening) --- - CreateMap() - .ForMember(dest => dest.GenerationId, opt => opt.MapFrom(src => src.ModelGeneration != null ? src.ModelGeneration.Id : 0)) - .ForMember(dest => dest.Year, opt => opt.MapFrom(src => src.ModelGeneration != null ? src.ModelGeneration.Year : 0)) - .ForMember(dest => dest.ModelName, opt => opt.MapFrom(src => - (src.ModelGeneration != null && src.ModelGeneration.Model != null) - ? src.ModelGeneration.Model.Name - : "Unknown Model")); - - CreateMap() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.ModelGeneration, opt => opt.Ignore()); - - // --- 5. Rent Mapping (Complex Logic) --- - CreateMap() - .ForMember(dest => dest.CarId, opt => opt.MapFrom(src => src.Car != null ? src.Car.Id : 0)) - .ForMember(dest => dest.ClientId, opt => opt.MapFrom(src => src.Client != null ? src.Client.Id : 0)) - .ForMember(dest => dest.ClientLastName, opt => opt.MapFrom(src => - src.Client != null ? src.Client.LastName : "Deleted")) - .ForMember(dest => dest.CarLicensePlate, opt => opt.MapFrom(src => - src.Car != null ? src.Car.NumberPlate : "Deleted")) - .ForMember(dest => dest.TotalCost, opt => opt.MapFrom(src => - (src.Car != null && src.Car.ModelGeneration != null) - ? (decimal)src.Duration * src.Car.ModelGeneration.HourCost - : 0)); - - CreateMap() - .ForMember(dest => dest.Id, opt => opt.Ignore()) - .ForMember(dest => dest.Car, opt => opt.Ignore()) - .ForMember(dest => dest.Client, opt => opt.Ignore()); + // ===================== + // CarModel + // ===================== + CreateMap(); + CreateMap(); + + // ===================== + // CarModelGeneration + // ===================== + CreateMap(); + CreateMap(); + + // ===================== + // Car + // ===================== + CreateMap(); + CreateMap(); + + // ===================== + // Rent + // ===================== + CreateMap(); + CreateMap(); } -} \ No newline at end of file +} diff --git a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs index b898d5041..d3233a040 100644 --- a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs +++ b/CarRental.Application/Contracts/Analytics/CarInRentDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The vehicle's license plate number. /// The exact start time of the rental period. /// The length of the rental in hours. -public record CarInRentDto(int CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours); \ No newline at end of file +public record CarInRentDto(Guid CarId, string ModelName, string NumberPlate, DateTime RentStartDate, int DurationHours); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs index 0497b59be..a0066bda1 100644 --- a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs +++ b/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The descriptive name of the car model. /// The vehicle's license plate number. /// Total number of rental agreements associated with this car. -public record CarWithRentalCountDto(int Id, string ModelName, string NumberPlate, int RentalCount); \ No newline at end of file +public record CarWithRentalCountDto(Guid Id, string ModelName, string NumberPlate, int RentalCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs index 8b6bf8c97..a0cb61879 100644 --- a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs +++ b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Analytics; /// The concatenated full name of the client. /// The sum of all rental costs paid by the client. /// Total number of times the client has rented vehicles. -public record ClientWithTotalAmountDto(int Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file +public record ClientWithTotalAmountDto(Guid Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs index 9e99fe6d1..53eb87b56 100644 --- a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs @@ -6,4 +6,4 @@ namespace CarRental.Application.Contracts.Car; /// The vehicle's license plate number. /// The color of the car. /// The unique identifier of the associated car model generation. -public record CarCreateUpdateDto(string NumberPlate, string Colour, int ModelGenerationId); \ No newline at end of file +public record CarCreateUpdateDto(string NumberPlate, string Colour, Guid ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application/Contracts/Car/CarDto.cs index e44f0eda2..768f20b62 100644 --- a/CarRental.Application/Contracts/Car/CarDto.cs +++ b/CarRental.Application/Contracts/Car/CarDto.cs @@ -6,7 +6,5 @@ namespace CarRental.Application.Contracts.Car; /// The unique identifier of the car. /// The vehicle's license plate number. /// The color of the car. -/// ID of the model generation. -/// The descriptive name of the car model. -/// Year of release -public record CarDto(int Id, string NumberPlate, string Colour, int GenerationId, string ModelName, int Year); \ No newline at end of file +/// ID of the model generation. +public record CarDto(Guid Id, string NumberPlate, string Colour, Guid ModelGenerationId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModel/CarModelDto.cs b/CarRental.Application/Contracts/CarModel/CarModelDto.cs index a041be885..affb96206 100644 --- a/CarRental.Application/Contracts/CarModel/CarModelDto.cs +++ b/CarRental.Application/Contracts/CarModel/CarModelDto.cs @@ -9,4 +9,4 @@ namespace CarRental.Application.Contracts.CarModel; /// The total passenger capacity. /// The style of the vehicle body (e.g., Sedan, SUV). /// The market segment or luxury class of the vehicle. -public record CarModelDto(int Id, string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file +public record CarModelDto(Guid Id, string Name, string? DriveType, int SeatsNumber, string BodyType, string? ClassType); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs index 7105c8e09..b08d19333 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.CarModelGeneration; /// The type of transmission (e.g., Manual, Automatic). /// The rental cost per hour for this generation. /// The unique identifier of the parent car model. -public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, int ModelId); \ No newline at end of file +public record CarModelGenerationCreateUpdateDto(int Year, string? TransmissionType, decimal HourCost, Guid ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs index 46e2793e9..9c04372f9 100644 --- a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs +++ b/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs @@ -8,4 +8,4 @@ namespace CarRental.Application.Contracts.CarModelGeneration; /// The type of transmission used in this generation. /// The rental cost per hour. /// The identifier of the parent car model. -public record CarModelGenerationDto(int Id, int Year, string? TransmissionType, decimal HourCost, int ModelId); \ No newline at end of file +public record CarModelGenerationDto(Guid Id, int Year, string? TransmissionType, decimal HourCost, Guid ModelId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs index 50f2b5a66..00be2c7d0 100644 --- a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs @@ -9,4 +9,4 @@ namespace CarRental.Application.Contracts.Client; /// The client's contact phone number. /// The unique identifier of the client's driving license. /// The client's date of birth. -public record ClientCreateUpdateDto(string FirstName, string LastName, string? Patronymic, string PhoneNumber, string DriverLicenseId, DateOnly BirthDate); \ No newline at end of file +public record ClientCreateUpdateDto(string FirstName, string LastName, string? Patronymic, string PhoneNumber, string DriverLicenseId, DateOnly? BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Client/ClientDto.cs b/CarRental.Application/Contracts/Client/ClientDto.cs index d153fb66d..1dfa33235 100644 --- a/CarRental.Application/Contracts/Client/ClientDto.cs +++ b/CarRental.Application/Contracts/Client/ClientDto.cs @@ -9,4 +9,4 @@ namespace CarRental.Application.Contracts.Client; /// The client's first name. /// The client's middle name (optional). /// The client's date of birth (optional). -public record ClientDto(int Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file +public record ClientDto(Guid Id, string DriverLicenseId, string LastName, string FirstName, string? Patronymic, DateOnly? BirthDate); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs index 9bd2bedda..5e8b97378 100644 --- a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs +++ b/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs @@ -7,4 +7,4 @@ namespace CarRental.Application.Contracts.Rent; /// The length of the rental period in hours. /// The unique identifier of the car to be rented. /// The unique identifier of the client renting the car. -public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, int CarId, int ClientId); \ No newline at end of file +public record RentCreateUpdateDto(DateTime StartDateTime, double Duration, Guid CarId, Guid ClientId); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application/Contracts/Rent/RentDto.cs index 7aca98b92..9157f9098 100644 --- a/CarRental.Application/Contracts/Rent/RentDto.cs +++ b/CarRental.Application/Contracts/Rent/RentDto.cs @@ -11,4 +11,4 @@ namespace CarRental.Application.Contracts.Rent; /// The unique identifier of the client. /// The last name of the client. /// The total calculated cost for the rental duration. -public record RentDto(int Id, DateTime StartDateTime, double Duration, int CarId, string CarLicensePlate, int ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file +public record RentDto(Guid Id, DateTime StartDateTime, double Duration, Guid CarId, string CarLicensePlate, Guid ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index 031a694a9..cf5a90334 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -5,9 +5,10 @@ namespace CarRental.Application.Interfaces; /// /// The data transfer object used for output. /// The data transfer object used for input operations. -public interface IApplicationService +public interface IApplicationService where TDto : class where TCreateUpdateDto : class + where TKey : struct { /// /// Creates a new record from the provided input DTO and returns the resulting output DTO. @@ -17,7 +18,7 @@ public interface IApplicationService /// /// Retrieves a single record by its unique identifier, mapped to an output DTO. /// - public Task Read(int id); + public Task Read(TKey id); /// /// Retrieves all records mapped to a list of output DTOs. @@ -27,10 +28,10 @@ public interface IApplicationService /// /// Updates an existing record identified by the given ID using the input DTO data. /// - public Task Update(TCreateUpdateDto dto, int id); + public Task Update(TCreateUpdateDto dto, TKey id); /// /// Removes a record from the system by its unique identifier. /// - public Task Delete(int id); + public Task Delete(TKey id); } \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index d9bed28af..848a0ec5c 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -8,19 +8,25 @@ namespace CarRental.Application.Services; public class AnalyticsService( - IBaseRepository rentRepository, - IBaseRepository carRepository, - IMapper mapper) : IAnalyticsService + IBaseRepository rentRepository, + IBaseRepository carRepository, + IMapper mapper) + : IAnalyticsService { public async Task> ReadClientsByModelName(string modelName) { var rents = await rentRepository.ReadAll(); return rents - .Where(r => r.Car?.ModelGeneration?.Model?.Name != null && - r.Car.ModelGeneration.Model.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + .Where(r => r.Client != null && + r.Car?.ModelGeneration?.Model?.Name != null && + r.Car.ModelGeneration.Model.Name.Contains( + modelName, + StringComparison.OrdinalIgnoreCase)) .Select(r => mapper.Map(r.Client)) .DistinctBy(c => c.Id) + .OrderBy(c => c.LastName) + .ThenBy(c => c.FirstName) .ToList(); } @@ -31,16 +37,18 @@ public async Task> ReadTop5MostRentedCars() return rents .Where(r => r.Car != null) .GroupBy(r => r.Car.Id) - .Select(g => { - var firstRent = g.First(); + .Select(g => + { + var car = g.First().Car!; return new CarWithRentalCountDto( - firstRent.Car.Id, - firstRent.Car.ModelGeneration?.Model?.Name ?? "Unknown", - firstRent.Car.NumberPlate, + car.Id, + car.ModelGeneration?.Model?.Name ?? "Unknown", + car.NumberPlate, g.Count() ); }) .OrderByDescending(x => x.RentalCount) + .ThenBy(x => x.NumberPlate) // детерминированно вместо Guid .Take(5) .ToList(); } @@ -52,14 +60,19 @@ public async Task> ReadCarsInRent(DateTime atTime) return rents .Where(r => r.Car != null && r.StartDateTime <= atTime && - r.StartDateTime.AddHours(r.Duration) >= atTime) - .Select(r => new CarInRentDto( - r.Car.Id, - r.Car.ModelGeneration?.Model?.Name ?? "Unknown", - r.Car.NumberPlate, - r.StartDateTime, - (int)r.Duration - )) + atTime < r.StartDateTime.AddHours(r.Duration)) + .Select(r => + { + var car = r.Car!; + return new CarInRentDto( + car.Id, + car.ModelGeneration?.Model?.Name ?? "Unknown", + car.NumberPlate, + r.StartDateTime, + (int)r.Duration + ); + }) + .OrderBy(x => x.NumberPlate) .ToList(); } @@ -68,12 +81,15 @@ public async Task> ReadAllCarsWithRentalCount() var allRents = await rentRepository.ReadAll(); var allCars = await carRepository.ReadAll(); - return allCars.Select(car => new CarWithRentalCountDto( + return allCars + .Select(car => new CarWithRentalCountDto( car.Id, car.ModelGeneration?.Model?.Name ?? "Unknown", car.NumberPlate, allRents.Count(r => r.Car?.Id == car.Id) - )).ToList(); + )) + .OrderBy(x => x.NumberPlate) + .ToList(); } public async Task> ReadTop5ClientsByTotalAmount() @@ -82,10 +98,13 @@ public async Task> ReadTop5ClientsByTotalAmount() return rents .Where(r => r.Client != null && r.Car?.ModelGeneration != null) - .GroupBy(r => r.Client.Id) - .Select(g => { - var client = g.First().Client; - var totalAmount = g.Sum(r => (decimal)r.Duration * (r.Car.ModelGeneration?.HourCost ?? 0)); + .GroupBy(r => r.Client!.Id) + .Select(g => + { + var client = g.First().Client!; + var totalAmount = g.Sum(r => + (decimal)r.Duration * r.Car!.ModelGeneration!.HourCost); + return new ClientWithTotalAmountDto( client.Id, $"{client.LastName} {client.FirstName}", @@ -94,7 +113,8 @@ public async Task> ReadTop5ClientsByTotalAmount() ); }) .OrderByDescending(x => x.TotalSpentAmount) + .ThenBy(x => x.FullName) .Take(5) .ToList(); } -} \ No newline at end of file +} diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs index 375454498..46401707f 100644 --- a/CarRental.Application/Services/CarModelGenerationService.cs +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -7,46 +7,75 @@ namespace CarRental.Application.Services; public class CarModelGenerationService( - IBaseRepository repository, - IBaseRepository modelRepository, + IBaseRepository repository, + IBaseRepository modelRepository, IMapper mapper) - : IApplicationService + : IApplicationService { public async Task Create(CarModelGenerationCreateUpdateDto dto) { + // DTO -> Entity var entity = mapper.Map(dto); + + // Загружаем модель var model = await modelRepository.Read(dto.ModelId); + if (model is null) + throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); + entity.Model = model; + entity.ModelId = model.Id; + // Репозиторий сам генерирует Guid var id = await repository.Create(entity); entity.Id = id; return mapper.Map(entity); } - public async Task Read(int id) + public async Task Read(Guid id) { - var entity = await repository.Read(id); - return entity == null ? null : mapper.Map(entity); + var entity = await repository.Read(id) + ?? throw new KeyNotFoundException($"CarModelGeneration with Id {id} not found."); + + return mapper.Map(entity); } public async Task> ReadAll() { var entities = await repository.ReadAll(); + + // Подгружаем модели (эмуляция Include) + foreach (var generation in entities) + { + if (generation.ModelId != Guid.Empty) + { + generation.Model = await modelRepository.Read(generation.ModelId); + } + } + return mapper.Map>(entities); } - public async Task Update(CarModelGenerationCreateUpdateDto dto, int id) + public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing == null) return false; + if (existing is null) + return false; + // Обновляем scalar-поля mapper.Map(dto, existing); + + // Обновляем модель var model = await modelRepository.Read(dto.ModelId); + if (model is null) + throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); + existing.Model = model; + existing.ModelId = model.Id; return await repository.Update(existing, id); } - public async Task Delete(int id) => await repository.Delete(id); -} \ No newline at end of file + public async Task Delete(Guid id) + => await repository.Delete(id); +} diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs index ee5f51d82..1babcca2c 100644 --- a/CarRental.Application/Services/CarModelService.cs +++ b/CarRental.Application/Services/CarModelService.cs @@ -6,21 +6,28 @@ namespace CarRental.Application.Services; -public class CarModelService(IBaseRepository repository, IMapper mapper) - : IApplicationService +public class CarModelService( + IBaseRepository repository, + IMapper mapper) + : IApplicationService { public async Task Create(CarModelCreateUpdateDto dto) { var entity = mapper.Map(dto); + + // Репозиторий генерирует Guid var id = await repository.Create(entity); entity.Id = id; + return mapper.Map(entity); } - public async Task Read(int id) + public async Task Read(Guid id) { - var entity = await repository.Read(id); - return entity == null ? null : mapper.Map(entity); + var entity = await repository.Read(id) + ?? throw new KeyNotFoundException($"CarModel with Id {id} not found."); + + return mapper.Map(entity); } public async Task> ReadAll() @@ -29,14 +36,18 @@ public async Task> ReadAll() return mapper.Map>(entities); } - public async Task Update(CarModelCreateUpdateDto dto, int id) + public async Task Update(CarModelCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing == null) return false; + if (existing is null) + return false; + + // Обновляем поля существующей сущности + mapper.Map(dto, existing); - mapper.Map(dto, existing); // Обновляем существующий объект return await repository.Update(existing, id); } - public async Task Delete(int id) => await repository.Delete(id); -} \ No newline at end of file + public async Task Delete(Guid id) + => await repository.Delete(id); +} diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index ecc4bd98a..94a2ec924 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -8,10 +8,10 @@ namespace CarRental.Application.Services; public class CarService( - IBaseRepository repository, - IBaseRepository generationRepository, + IBaseRepository repository, + IBaseRepository generationRepository, IMapper mapper) - : IApplicationService + : IApplicationService { public async Task> ReadAll() { @@ -19,41 +19,51 @@ public async Task> ReadAll() return mapper.Map>(entities); } - public async Task Read(int id) + public async Task Read(Guid id) { - var entity = await repository.Read(id); - return entity == null ? null : mapper.Map(entity); + var entity = await repository.Read(id) + ?? throw new KeyNotFoundException($"Car with Id {id} not found."); + + return mapper.Map(entity); } public async Task Create(CarCreateUpdateDto dto) { - var entity = mapper.Map(dto); - var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); + // + var generation = await generationRepository.Read(dto.ModelGenerationId); + if (generation is null) + throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); - if (fullGeneration == null) - throw new Exception("Generation not found"); + var entity = mapper.Map(dto); - entity.ModelGeneration = fullGeneration; var id = await repository.Create(entity); - var savedEntity = await repository.Read(id); - return mapper.Map(savedEntity!); + + // (in-memory , ) + var savedEntity = await repository.Read(id) + ?? throw new InvalidOperationException("Created car was not found."); + + return mapper.Map(savedEntity); } - public async Task Update(CarCreateUpdateDto dto, int id) + public async Task Update(CarCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) return false; + if (existing is null) + return false; - mapper.Map(dto, existing); - - var fullGeneration = await generationRepository.Read(dto.ModelGenerationId); - if (fullGeneration != null) + // + if (dto.ModelGenerationId != existing.ModelGenerationId) { - existing.ModelGeneration = fullGeneration; + var generation = await generationRepository.Read(dto.ModelGenerationId); + if (generation is null) + throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); } + mapper.Map(dto, existing); + return await repository.Update(existing, id); } - public async Task Delete(int id) => await repository.Delete(id); -} \ No newline at end of file + public async Task Delete(Guid id) + => await repository.Delete(id); +} diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 2ba08ed04..df3f25d8a 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -6,8 +6,10 @@ namespace CarRental.Application.Services; -public class ClientService(IBaseRepository repository, IMapper mapper) - : IApplicationService +public class ClientService( + IBaseRepository repository, + IMapper mapper) + : IApplicationService { public async Task> ReadAll() { @@ -15,28 +17,38 @@ public async Task> ReadAll() return mapper.Map>(entities); } - public async Task Read(int id) + public async Task Read(Guid id) { - var entity = await repository.Read(id); - return entity == null ? null : mapper.Map(entity); + var entity = await repository.Read(id) + ?? throw new KeyNotFoundException($"Client with Id {id} not found."); + + return mapper.Map(entity); } public async Task Create(ClientCreateUpdateDto dto) { var entity = mapper.Map(dto); + var id = await repository.Create(entity); - var savedEntity = await repository.Read(id); - return mapper.Map(savedEntity!); + + // + var savedEntity = await repository.Read(id) + ?? throw new InvalidOperationException("Created client was not found."); + + return mapper.Map(savedEntity); } - public async Task Update(ClientCreateUpdateDto dto, int id) + public async Task Update(ClientCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) return false; + if (existing is null) + return false; mapper.Map(dto, existing); + return await repository.Update(existing, id); } - public async Task Delete(int id) => await repository.Delete(id); -} \ No newline at end of file + public async Task Delete(Guid id) + => await repository.Delete(id); +} diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 3fb4ea507..2b82af673 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -7,11 +7,11 @@ namespace CarRental.Application.Services; public class RentService( - IBaseRepository repository, - IBaseRepository carRepository, - IBaseRepository clientRepository, + IBaseRepository repository, + IBaseRepository carRepository, + IBaseRepository clientRepository, IMapper mapper) - : IApplicationService + : IApplicationService { public async Task> ReadAll() { @@ -19,45 +19,60 @@ public async Task> ReadAll() return mapper.Map>(rents); } - public async Task Read(int id) + public async Task Read(Guid id) { - var entity = await repository.Read(id); - return entity == null ? null : mapper.Map(entity); + var entity = await repository.Read(id) + ?? throw new KeyNotFoundException($"Rent with Id {id} not found."); + + return mapper.Map(entity); } public async Task Create(RentCreateUpdateDto dto) { - var car = await carRepository.Read(dto.CarId); - var client = await clientRepository.Read(dto.ClientId); + var car = await carRepository.Read(dto.CarId) + ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found."); - if (car == null || client == null) - throw new Exception("Car or client is not found"); + var client = await clientRepository.Read(dto.ClientId) + ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found."); var entity = mapper.Map(dto); entity.Car = car; entity.Client = client; var id = await repository.Create(entity); - var savedEntity = await repository.Read(id); - return mapper.Map(savedEntity!); + var savedEntity = await repository.Read(id) + ?? throw new InvalidOperationException("Created rent was not found."); + + return mapper.Map(savedEntity); } - public async Task Update(RentCreateUpdateDto dto, int id) + public async Task Update(RentCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) return false; + if (existing is null) + return false; mapper.Map(dto, existing); - var car = await carRepository.Read(dto.CarId); - var client = await clientRepository.Read(dto.ClientId); + // + if (dto.CarId != existing.Car?.Id) + { + var car = await carRepository.Read(dto.CarId) + ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found."); + existing.Car = car; + } - if (car != null) existing.Car = car; - if (client != null) existing.Client = client; + if (dto.ClientId != existing.Client?.Id) + { + var client = await clientRepository.Read(dto.ClientId) + ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found."); + existing.Client = client; + } return await repository.Update(existing, id); } - public async Task Delete(int id) => await repository.Delete(id); -} \ No newline at end of file + public async Task Delete(Guid id) + => await repository.Delete(id); +} diff --git a/CarRental.Domain/DataModels/Car.cs b/CarRental.Domain/DataModels/Car.cs index f0d76c05e..c31705644 100644 --- a/CarRental.Domain/DataModels/Car.cs +++ b/CarRental.Domain/DataModels/Car.cs @@ -10,7 +10,12 @@ public class Car /// /// Unique identifier of the car /// - public required int Id { get; set; } + public required Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The model generation id this car belongs to, defining its year, transmission type, and base rental cost + /// + public required Guid ModelGenerationId { get; set; } /// /// The model generation this car belongs to, defining its year, transmission type, and base rental cost diff --git a/CarRental.Domain/DataModels/Client.cs b/CarRental.Domain/DataModels/Client.cs index fc45cb0b6..c059c1a7d 100644 --- a/CarRental.Domain/DataModels/Client.cs +++ b/CarRental.Domain/DataModels/Client.cs @@ -8,7 +8,7 @@ public class Client /// /// Unique identifier of the client /// - public required int Id { get; set; } + public required Guid Id { get; set; } = Guid.NewGuid(); /// /// Unique identifier of the client's driver's license diff --git a/CarRental.Domain/DataModels/Rent.cs b/CarRental.Domain/DataModels/Rent.cs index 1548b4561..34a29d8ee 100644 --- a/CarRental.Domain/DataModels/Rent.cs +++ b/CarRental.Domain/DataModels/Rent.cs @@ -8,7 +8,7 @@ public class Rent /// /// Unique identifier of the rental record /// - public int Id { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); /// /// Date and time when the rental period starts @@ -20,11 +20,21 @@ public class Rent /// public required double Duration { get; set; } + /// + /// The car ID that is being rented + /// + public required Guid CarId { get; set; } + /// /// The car that is being rented /// public required Car Car { get; set; } + /// + /// The client ID who is renting the car + /// + public required Guid ClientId { get; set; } + /// /// The client who is renting the car /// diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs index 7c1478132..9e8f06e51 100644 --- a/CarRental.Domain/DataSeed/DataSeed.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -41,148 +41,148 @@ public DataSeed() { Models = new List { - new() { Id = 1, Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, - new() { Id = 2, Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, - new() { Id = 3, Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new() { Id = 4, Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new() { Id = 5, Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, - new() { Id = 6, Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new() { Id = 7, Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new() { Id = 8, Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, - new() { Id = 9, Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, - new() { Id = 10, Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, - new() { Id = 11, Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, - new() { Id = 12, Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new() { Id = 13, Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, - new() { Id = 14, Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new() { Id = 15, Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, - new() { Id = 16, Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, - new() { Id = 17, Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, - new() { Id = 18, Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, - new() { Id = 19, Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, - new() { Id = 20, Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } + new() { Id = Guid.NewGuid(), Name = "Fiat 500", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 4, BodyType = BodyType.CityCar, ClassType = ClassType.A }, + new() { Id = Guid.NewGuid(), Name = "Subaru Outback", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.StationWagon, ClassType = ClassType.D }, + new() { Id = Guid.NewGuid(), Name = "Volkswagen Golf", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "Mazda CX-5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "Nissan Qashqai", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Crossover, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "Volvo XC90", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 7, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new() { Id = Guid.NewGuid(), Name = "Audi A4", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = Guid.NewGuid(), Name = "Honda CR-V", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.D }, + new() { Id = Guid.NewGuid(), Name = "Hyundai Tucson", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "Volkswagen Transporter", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 9, BodyType = BodyType.Van, ClassType = ClassType.F }, + new() { Id = Guid.NewGuid(), Name = "Mercedes E-Class", DriveType = InternalData.ComponentEnums.DriveType.RearWheel,SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.E }, + new() { Id = Guid.NewGuid(), Name = "Ford Focus", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "Jaguar F-Type", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.Coupe, ClassType = ClassType.E }, + new() { Id = Guid.NewGuid(), Name = "Tesla Model 3", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = Guid.NewGuid(), Name = "Toyota Camry", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.D }, + new() { Id = Guid.NewGuid(), Name = "Lexus LS", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.F }, + new() { Id = Guid.NewGuid(), Name = "Porsche 911", DriveType = InternalData.ComponentEnums.DriveType.RearWheel, SeatsNumber = 2, BodyType = BodyType.SportsCar, ClassType = ClassType.E }, + new() { Id = Guid.NewGuid(), Name = "Renault Megane", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Hatchback, ClassType = ClassType.C }, + new() { Id = Guid.NewGuid(), Name = "BMW X5", DriveType = InternalData.ComponentEnums.DriveType.AllWheel, SeatsNumber = 5, BodyType = BodyType.SportUtilityVehicle, ClassType = ClassType.E }, + new() { Id = Guid.NewGuid(), Name = "Kia Rio", DriveType = InternalData.ComponentEnums.DriveType.FrontWheel, SeatsNumber = 5, BodyType = BodyType.Sedan, ClassType = ClassType.B } }; Generations = new List { - new() { Id = 1, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[16], HourCost = 160.00m }, // Porsche 911 - new() { Id = 2, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[0], HourCost = 35.00m }, // Fiat 500 - new() { Id = 3, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[11], HourCost = 55.00m }, // Ford Focus - new() { Id = 4, Year = 2020, TransmissionType = TransmissionType.Variable, Model = Models[4], HourCost = 70.00m }, // Nissan Qashqai - new() { Id = 5, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[18], HourCost = 120.00m }, // BMW X5 - new() { Id = 6, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[15], HourCost = 140.00m }, // Lexus LS - new() { Id = 7, Year = 2018, TransmissionType = TransmissionType.Manual, Model = Models[19], HourCost = 40.00m }, // Kia Rio - new() { Id = 8, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[7], HourCost = 85.00m }, // Honda CR-V - new() { Id = 9, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[12], HourCost = 150.00m }, // Jaguar F-Type - new() { Id = 10, Year = 2020, TransmissionType = TransmissionType.Manual, Model = Models[9], HourCost = 60.00m }, // VW Transporter - new() { Id = 11, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[1], HourCost = 95.00m }, // Subaru Outback - new() { Id = 12, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[8], HourCost = 75.00m }, // Hyundai Tucson - new() { Id = 13, Year = 2019, TransmissionType = TransmissionType.Manual, Model = Models[2], HourCost = 50.00m }, // VW Golf - new() { Id = 14, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[13], HourCost = 100.00m }, // Tesla Model 3 - new() { Id = 15, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[14], HourCost = 80.00m }, // Toyota Camry - new() { Id = 16, Year = 2020, TransmissionType = TransmissionType.Automatic, Model = Models[6], HourCost = 90.00m }, // Audi A4 - new() { Id = 17, Year = 2022, TransmissionType = TransmissionType.Automatic, Model = Models[5], HourCost = 105.00m }, // Volvo XC90 - new() { Id = 18, Year = 2021, TransmissionType = TransmissionType.Manual, Model = Models[17], HourCost = 55.00m }, // Renault Megane - new() { Id = 19, Year = 2023, TransmissionType = TransmissionType.Automatic, Model = Models[10], HourCost = 110.00m }, // Mercedes E-Class - new() { Id = 20, Year = 2021, TransmissionType = TransmissionType.Automatic, Model = Models[3], HourCost = 80.00m } // Mazda CX-5 + new() { Id = Guid.NewGuid(), Year = 2019, TransmissionType = TransmissionType.Manual, ModelId = Models[16].Id, Model = Models[16], HourCost = 160.00m }, // Porsche 911 + new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[0].Id, Model = Models[0], HourCost = 35.00m }, // Fiat 500 + new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Manual, ModelId = Models[11].Id, Model = Models[11], HourCost = 55.00m }, // Ford Focus + new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Variable, ModelId = Models[4].Id, Model = Models[4], HourCost = 70.00m }, // Nissan Qashqai + new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[18].Id, Model = Models[18], HourCost = 120.00m }, // BMW X5 + new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[15].Id, Model = Models[15], HourCost = 140.00m }, // Lexus LS + new() { Id = Guid.NewGuid(), Year = 2018, TransmissionType = TransmissionType.Manual, ModelId = Models[19].Id, Model = Models[19], HourCost = 40.00m }, // Kia Rio + new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[7].Id, Model = Models[7], HourCost = 85.00m }, // Honda CR-V + new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[12].Id, Model = Models[12], HourCost = 150.00m }, // Jaguar F-Type + new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Manual, ModelId = Models[9].Id, Model = Models[9], HourCost = 60.00m }, // VW Transporter + new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[1].Id, Model = Models[1], HourCost = 95.00m }, // Subaru Outback + new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[8].Id, Model = Models[8], HourCost = 75.00m }, // Hyundai Tucson + new() { Id = Guid.NewGuid(), Year = 2019, TransmissionType = TransmissionType.Manual, ModelId = Models[2].Id, Model = Models[2], HourCost = 50.00m }, // VW Golf + new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[13].Id, Model = Models[13], HourCost = 100.00m }, // Tesla Model 3 + new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[14].Id, Model = Models[14], HourCost = 80.00m }, // Toyota Camry + new() { Id = Guid.NewGuid(), Year = 2020, TransmissionType = TransmissionType.Automatic, ModelId = Models[6].Id, Model = Models[6], HourCost = 90.00m }, // Audi A4 + new() { Id = Guid.NewGuid(), Year = 2022, TransmissionType = TransmissionType.Automatic, ModelId = Models[5].Id, Model = Models[5], HourCost = 105.00m }, // Volvo XC90 + new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Manual, ModelId = Models[17].Id, Model = Models[17], HourCost = 55.00m }, // Renault Megane + new() { Id = Guid.NewGuid(), Year = 2023, TransmissionType = TransmissionType.Automatic, ModelId = Models[10].Id, Model = Models[10], HourCost = 110.00m }, // Mercedes E-Class + new() { Id = Guid.NewGuid(), Year = 2021, TransmissionType = TransmissionType.Automatic, ModelId = Models[3].Id, Model = Models[3], HourCost = 80.00m } // Mazda CX-5 }; Cars = new List { - new() { Id = 1, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" }, - new() { Id = 2, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" }, - new() { Id = 3, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, - new() { Id = 4, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" }, - new() { Id = 5, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" }, - new() { Id = 6, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" }, - new() { Id = 7, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" }, - new() { Id = 8, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" }, - new() { Id = 9, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" }, - new() { Id = 10, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" }, - new() { Id = 11, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" }, - new() { Id = 12, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" }, - new() { Id = 13, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" }, - new() { Id = 14, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" }, - new() { Id = 15, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" }, - new() { Id = 16, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" }, - new() { Id = 17, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" }, - new() { Id = 18, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" }, - new() { Id = 19, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" }, - new() { Id = 20, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" } + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[5].Id, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[14].Id, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[0].Id, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[19].Id, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[6].Id, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[16].Id, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[7].Id, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[13].Id, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[3].Id, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[18].Id, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[10].Id, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[11].Id, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[8].Id, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[9].Id, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[1].Id, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[15].Id, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[2].Id, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[17].Id, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[4].Id, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" }, + new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[12].Id, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" } }; Clients = new List { - new() { Id = 1, DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, - new() { Id = 2, DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, - new() { Id = 3, DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, - new() { Id = 4, DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, - new() { Id = 5, DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, - new() { Id = 6, DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, - new() { Id = 7, DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, - new() { Id = 8, DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, - new() { Id = 9, DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, - new() { Id = 10, DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, - new() { Id = 11, DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, - new() { Id = 12, DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, - new() { Id = 13, DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, - new() { Id = 14, DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, - new() { Id = 15, DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, - new() { Id = 16, DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, - new() { Id = 17, DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, - new() { Id = 18, DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, - new() { Id = 19, DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, - new() { Id = 20, DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, + new() { Id = Guid.NewGuid(), DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } }; var baseTime = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); Rents = new List { - new() { Id = 1, StartDateTime = baseTime.AddDays(-2), Duration = 6, Car = Cars[13], Client = Clients[14] }, - new() { Id = 2, StartDateTime = baseTime.AddDays(12), Duration = 6, Car = Cars[19], Client = Clients[19] }, - new() { Id = 3, StartDateTime = baseTime.AddDays(-25), Duration = 48, Car = Cars[2], Client = Clients[2] }, - new() { Id = 4, StartDateTime = baseTime.AddDays(8), Duration = 24, Car = Cars[17], Client = Clients[17] }, - new() { Id = 5, StartDateTime = baseTime.AddDays(-20), Duration = 72, Car = Cars[4], Client = Clients[4] }, - new() { Id = 6, StartDateTime = baseTime.AddDays(4), Duration = 72, Car = Cars[15], Client = Clients[15] }, - new() { Id = 7, StartDateTime = baseTime.AddDays(-15), Duration = 168, Car = Cars[6], Client = Clients[6] }, - new() { Id = 8, StartDateTime = baseTime.AddDays(-4), Duration = 48, Car = Cars[11], Client = Clients[11] }, - new() { Id = 9, StartDateTime = baseTime.AddDays(-10), Duration = 36, Car = Cars[8], Client = Clients[8] }, - new() { Id = 10, StartDateTime = baseTime, Duration = 24, Car = Cars[1], Client = Clients[0] }, - new() { Id = 11, StartDateTime = baseTime.AddDays(2), Duration = 8, Car = Cars[14], Client = Clients[13] }, - new() { Id = 12, StartDateTime = baseTime.AddDays(-8), Duration = 24, Car = Cars[9], Client = Clients[9] }, - new() { Id = 13, StartDateTime = baseTime.AddDays(6), Duration = 12, Car = Cars[16], Client = Clients[16] }, - new() { Id = 14, StartDateTime = baseTime.AddDays(-6), Duration = 12, Car = Cars[10], Client = Clients[10] }, - new() { Id = 15, StartDateTime = baseTime.AddDays(10), Duration = 48, Car = Cars[18], Client = Clients[18] }, - new() { Id = 16, StartDateTime = baseTime.AddDays(-28), Duration = 12, Car = Cars[12], Client = Clients[12] }, - new() { Id = 17, StartDateTime = baseTime.AddDays(-22), Duration = 6, Car = Cars[3], Client = Clients[3] }, - new() { Id = 18, StartDateTime = baseTime.AddDays(-18), Duration = 8, Car = Cars[5], Client = Clients[5] }, - new() { Id = 19, StartDateTime = baseTime.AddDays(-12), Duration = 4, Car = Cars[7], Client = Clients[7] }, - new() { Id = 20, StartDateTime = baseTime.AddDays(-30), Duration = 24, Car = Cars[0], Client = Clients[1] }, - new() { Id = 21, StartDateTime = baseTime.AddDays(-25), Duration = 12, Car = Cars[5], Client = Clients[0] }, - new() { Id = 22, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[0], Client = Clients[1] }, - new() { Id = 23, StartDateTime = baseTime.AddDays(-5), Duration = 8, Car = Cars[10], Client = Clients[1] }, - new() { Id = 24, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[3], Client = Clients[2] }, - new() { Id = 25, StartDateTime = baseTime.AddDays(-15), Duration = 6, Car = Cars[7], Client = Clients[2] }, - new() { Id = 26, StartDateTime = baseTime.AddDays(-8), Duration = 12, Car = Cars[15], Client = Clients[2] }, - new() { Id = 27, StartDateTime = baseTime.AddDays(-22), Duration = 24, Car = Cars[4], Client = Clients[3] }, - new() { Id = 28, StartDateTime = baseTime.AddDays(-18), Duration = 36, Car = Cars[8], Client = Clients[3] }, - new() { Id = 29, StartDateTime = baseTime.AddDays(-12), Duration = 12, Car = Cars[12], Client = Clients[3] }, - new() { Id = 30, StartDateTime = baseTime.AddDays(-6), Duration = 6, Car = Cars[17], Client = Clients[3] }, - new() { Id = 31, StartDateTime = baseTime.AddDays(-28), Duration = 72, Car = Cars[1], Client = Clients[4] }, - new() { Id = 32, StartDateTime = baseTime.AddDays(-24), Duration = 24, Car = Cars[6], Client = Clients[4] }, - new() { Id = 33, StartDateTime = baseTime.AddDays(-20), Duration = 48, Car = Cars[9], Client = Clients[4] }, - new() { Id = 34, StartDateTime = baseTime.AddDays(-16), Duration = 12, Car = Cars[13], Client = Clients[4] }, - new() { Id = 35, StartDateTime = baseTime.AddDays(-10), Duration = 8, Car = Cars[18], Client = Clients[4] }, - new() { Id = 36, StartDateTime = baseTime.AddDays(-30), Duration = 168, Car = Cars[2], Client = Clients[5] }, - new() { Id = 37, StartDateTime = baseTime.AddDays(-26), Duration = 24, Car = Cars[7], Client = Clients[5] }, - new() { Id = 38, StartDateTime = baseTime.AddDays(-22), Duration = 48, Car = Cars[11], Client = Clients[5] }, - new() { Id = 39, StartDateTime = baseTime.AddDays(-18), Duration = 6, Car = Cars[14], Client = Clients[5] }, - new() { Id = 40, StartDateTime = baseTime.AddDays(-14), Duration = 12, Car = Cars[16], Client = Clients[5] }, - new() { Id = 41, StartDateTime = baseTime.AddDays(-10), Duration = 24, Car = Cars[19], Client = Clients[5] }, - new() { Id = 42, StartDateTime = baseTime.AddDays(-3), Duration = 10, Car = Cars[0], Client = Clients[6] }, - new() { Id = 43, StartDateTime = baseTime.AddDays(-1), Duration = 5, Car = Cars[2], Client = Clients[7] }, - new() { Id = 44, StartDateTime = baseTime.AddDays(1), Duration = 7, Car = Cars[5], Client = Clients[8] }, - new() { Id = 45, StartDateTime = baseTime.AddDays(3), Duration = 9, Car = Cars[10], Client = Clients[9] } + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-2), Duration = 6, CarId = Cars[13].Id, Car = Cars[13], ClientId = Clients[14].Id, Client = Clients[14] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(12), Duration = 6, CarId = Cars[19].Id, Car = Cars[19], ClientId = Clients[19].Id, Client = Clients[19] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-25), Duration = 48, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[2].Id, Client = Clients[2] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(8), Duration = 24, CarId = Cars[17].Id, Car = Cars[17], ClientId = Clients[17].Id, Client = Clients[17] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 72, CarId = Cars[4].Id, Car = Cars[4], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(4), Duration = 72, CarId = Cars[15].Id, Car = Cars[15], ClientId = Clients[15].Id, Client = Clients[15] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-15), Duration = 168, CarId = Cars[6].Id, Car = Cars[6], ClientId = Clients[6].Id, Client = Clients[6] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-4), Duration = 48, CarId = Cars[11].Id, Car = Cars[11], ClientId = Clients[11].Id, Client = Clients[11] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 36, CarId = Cars[8].Id, Car = Cars[8], ClientId = Clients[8].Id, Client = Clients[8] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime, Duration = 24, CarId = Cars[1].Id, Car = Cars[1], ClientId = Clients[0].Id, Client = Clients[0] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(2), Duration = 8, CarId = Cars[14].Id, Car = Cars[14], ClientId = Clients[13].Id, Client = Clients[13] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-8), Duration = 24, CarId = Cars[9].Id, Car = Cars[9], ClientId = Clients[9].Id, Client = Clients[9] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(6), Duration = 12, CarId = Cars[16].Id, Car = Cars[16], ClientId = Clients[16].Id, Client = Clients[16] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-6), Duration = 12, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[10].Id, Client = Clients[10] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(10), Duration = 48, CarId = Cars[18].Id, Car = Cars[18], ClientId = Clients[18].Id, Client = Clients[18] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-28), Duration = 12, CarId = Cars[12].Id, Car = Cars[12], ClientId = Clients[12].Id, Client = Clients[12] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 6, CarId = Cars[3].Id, Car = Cars[3], ClientId = Clients[3].Id, Client = Clients[3] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 8, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-12), Duration = 4, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[7].Id, Client = Clients[7] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-30), Duration = 24, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[1].Id, Client = Clients[1] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-25), Duration = 12, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[0].Id, Client = Clients[0] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 24, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[1].Id, Client = Clients[1] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-5), Duration = 8, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[1].Id, Client = Clients[1] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 48, CarId = Cars[3].Id, Car = Cars[3], ClientId = Clients[2].Id, Client = Clients[2] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-15), Duration = 6, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[2].Id, Client = Clients[2] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-8), Duration = 12, CarId = Cars[15].Id, Car = Cars[15], ClientId = Clients[2].Id, Client = Clients[2] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 24, CarId = Cars[4].Id, Car = Cars[4], ClientId = Clients[3].Id, Client = Clients[3] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 36, CarId = Cars[8].Id, Car = Cars[8], ClientId = Clients[3].Id, Client = Clients[3] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-12), Duration = 12, CarId = Cars[12].Id, Car = Cars[12], ClientId = Clients[3].Id, Client = Clients[3] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-6), Duration = 6, CarId = Cars[17].Id, Car = Cars[17], ClientId = Clients[3].Id, Client = Clients[3] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-28), Duration = 72, CarId = Cars[1].Id, Car = Cars[1], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-24), Duration = 24, CarId = Cars[6].Id, Car = Cars[6], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-20), Duration = 48, CarId = Cars[9].Id, Car = Cars[9], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-16), Duration = 12, CarId = Cars[13].Id, Car = Cars[13], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 8, CarId = Cars[18].Id, Car = Cars[18], ClientId = Clients[4].Id, Client = Clients[4] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-30), Duration = 168, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-26), Duration = 24, CarId = Cars[7].Id, Car = Cars[7], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-22), Duration = 48, CarId = Cars[11].Id, Car = Cars[11], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-18), Duration = 6, CarId = Cars[14].Id, Car = Cars[14], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-14), Duration = 12, CarId = Cars[16].Id, Car = Cars[16], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-10), Duration = 24, CarId = Cars[19].Id, Car = Cars[19], ClientId = Clients[5].Id, Client = Clients[5] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-3), Duration = 10, CarId = Cars[0].Id, Car = Cars[0], ClientId = Clients[6].Id, Client = Clients[6] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(-1), Duration = 5, CarId = Cars[2].Id, Car = Cars[2], ClientId = Clients[7].Id, Client = Clients[7] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(1), Duration = 7, CarId = Cars[5].Id, Car = Cars[5], ClientId = Clients[8].Id, Client = Clients[8] }, + new() { Id = Guid.NewGuid(), StartDateTime = baseTime.AddDays(3), Duration = 9, CarId = Cars[10].Id, Car = Cars[10], ClientId = Clients[9].Id, Client = Clients[9] } }; } } diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index 9bff49602..f2d3948b7 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -4,26 +4,19 @@ namespace CarRental.Domain.Interfaces; /// Provides a base implementation for in-memory CRUD operations. /// /// The type of the entity managed by the repository. -public abstract class BaseRepository : IBaseRepository +public abstract class BaseRepository : IBaseRepository where TEntity : class { - /// - /// Private field for obtaining a unique identifier - /// to assign it to the next entity in the repository - /// - - private int _nextId; - private readonly List _entities; /// /// Gets the unique identifier from the entity. /// - protected abstract int GetEntityId(TEntity entity); + protected abstract Guid GetEntityId(TEntity entity); /// /// Sets the unique identifier for the entity /// - protected abstract void SetEntityId(TEntity entity, int id); + protected abstract void SetEntityId(TEntity entity, Guid id); /// /// Initializes the repository and determines the starting ID based on existing data. @@ -31,38 +24,29 @@ public abstract class BaseRepository : IBaseRepository protected BaseRepository(List? entities = null) { _entities = entities ?? new List(); - if (_entities.Count > 0) - { - _nextId = _entities.Max(e => GetEntityId(e)) + 1; - } - else - { - _nextId = 1; - } } /// /// Adds a new entity to the collection and assigns a unique ID. /// - public virtual Task Create(TEntity entity) + public virtual Task Create(TEntity entity) { if (entity == null) - { throw new ArgumentNullException(nameof(entity)); - } - var currentId = _nextId; - SetEntityId(entity, currentId); + var id = Guid.NewGuid(); + SetEntityId(entity, id); _entities.Add(entity); - _nextId++; - return Task.FromResult(currentId); + return Task.FromResult(id); } /// /// Retrieves an entity by its unique identifier. /// - public virtual Task Read(int id) + public virtual Task Read(Guid id) { - return Task.FromResult(_entities.FirstOrDefault(e => GetEntityId(e) == id)); + return Task.FromResult( + _entities.FirstOrDefault(e => GetEntityId(e) == id) + ); } /// @@ -76,29 +60,25 @@ public virtual Task> ReadAll() /// /// Replaces an existing entity at the specified ID. /// - public virtual async Task Update(TEntity entity, int id) + public virtual async Task Update(TEntity entity, Guid id) { var existing = await Read(id); - if (existing != null) - { - var index = _entities.IndexOf(existing); - SetEntityId(entity, id); - _entities[index] = entity; - return true; - } - return false; + if (existing == null) + return false; + + var index = _entities.IndexOf(existing); + SetEntityId(entity, id); + _entities[index] = entity; + + return true; } /// /// Removes an entity from the collection by its ID. /// - public virtual async Task Delete(int id) + public virtual async Task Delete(Guid id) { var entity = await Read(id); - if (entity != null) - { - return _entities.Remove(entity); - } - return false; + return entity != null && _entities.Remove(entity); } } \ No newline at end of file diff --git a/CarRental.Domain/Interfaces/IBaseRepository.cs b/CarRental.Domain/Interfaces/IBaseRepository.cs index 0ec5c381a..6742230ae 100644 --- a/CarRental.Domain/Interfaces/IBaseRepository.cs +++ b/CarRental.Domain/Interfaces/IBaseRepository.cs @@ -4,18 +4,20 @@ namespace CarRental.Domain.Interfaces; /// Defines the standard contract for a generic repository supporting CRUD operations. /// /// The type of the entity object. -public interface IBaseRepository +/// The type of the key. +public interface IBaseRepository where TEntity : class + where TKey : struct { /// /// Adds a new entity to the collection and returns a unique ID. /// - public Task Create(TEntity entity); + public Task Create(TEntity entity); /// /// Retrieves an entity by its unique identifier. /// - public Task Read(int id); + public Task Read(TKey id); /// /// Returns all entities in the collection. @@ -25,10 +27,10 @@ public interface IBaseRepository /// /// Replaces an existing entity at the specified ID. /// - public Task Update(TEntity entity, int id); + public Task Update(TEntity entity, TKey id); /// /// Removes an entity from the collection by its ID. /// - public Task Delete(int id); + public Task Delete(TKey id); } \ No newline at end of file diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs index 3fa091ae4..9642fdc47 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModel.cs @@ -12,7 +12,7 @@ public class CarModel /// /// Unique identifier of the car model /// - public required int Id { get; set; } + public required Guid Id { get; set; } = Guid.NewGuid(); /// /// Name of the car model (e.g., "Camry", "Golf", "Model 3") diff --git a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs index 7098c6888..e12013c2a 100644 --- a/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs +++ b/CarRental.Domain/InternalData/ComponentClasses/CarModelGeneration.cs @@ -13,7 +13,7 @@ public class CarModelGeneration /// /// Unique identifier of the car model generation /// - public required int Id { get; set; } + public required Guid Id { get; set; } = Guid.NewGuid(); /// /// Calendar year when this generation of the car model was produced @@ -25,6 +25,13 @@ public class CarModelGeneration /// public TransmissionType? TransmissionType { get; set; } + /// + /// The car model ID to which this generation belongs (a class that describes + /// the main technical characteristics, such as the model name, + /// drive type, transmission type, body type, and vehicle class) + /// + public Guid ModelId { get; set; } + /// /// The car model to which this generation belongs (a class that describes /// the main technical characteristics, such as the model name, diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs index 29a77a28b..a3384755a 100644 --- a/CarRental.Infrastructure/CarRentalDbContext.cs +++ b/CarRental.Infrastructure/CarRentalDbContext.cs @@ -44,10 +44,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.ToCollection("rents"); builder.HasKey(r => r.Id); builder.Property(r => r.Id).HasElementName("_id"); - - // Маппинг внешних ключей (если они есть как свойства в модели) - // builder.Property(r => r.CarId).HasElementName("car_id"); - // builder.Property(r => r.ClientId).HasElementName("client_id"); + builder.Property(r => r.CarId).HasElementName("car_id"); + builder.Property(r => r.ClientId).HasElementName("client_id"); }); // 4. Модели (CarModel) diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs index f5b75b5e3..4d08feb20 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelGenerationRepository.cs @@ -13,10 +13,10 @@ public class CarModelGenerationRepository(DataSeed data) : BaseRepository /// Gets the unique identifier from the specified CarModelGeneration entity /// - protected override int GetEntityId(CarModelGeneration generation) => generation.Id; + protected override Guid GetEntityId(CarModelGeneration generation) => generation.Id; /// /// Sets the unique identifier for the specified CarModelGeneration entity /// - protected override void SetEntityId(CarModelGeneration generation, int id) => generation.Id = id; + protected override void SetEntityId(CarModelGeneration generation, Guid id) => generation.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs index 4bdaec130..3ec4a9b7d 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarModelRepository.cs @@ -13,10 +13,10 @@ public class CarModelRepository(DataSeed data) : BaseRepository(data.M /// /// Gets the unique identifier from the specified CarModel entity /// - protected override int GetEntityId(CarModel model) => model.Id; + protected override Guid GetEntityId(CarModel model) => model.Id; /// /// Sets the unique identifier for the specified CarModel entity /// - protected override void SetEntityId(CarModel model, int id) => model.Id = id; + protected override void SetEntityId(CarModel model, Guid id) => model.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs index 752239df5..129b3e8c9 100644 --- a/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/CarRepository.cs @@ -13,10 +13,10 @@ public class CarRepository(DataSeed data) : BaseRepository(data.Cars) /// /// Gets the unique identifier from the specified Car entity /// - protected override int GetEntityId(Car car) => car.Id; + protected override Guid GetEntityId(Car car) => car.Id; /// /// Sets the unique identifier for the specified Car entity /// - protected override void SetEntityId(Car car, int id) => car.Id = id; + protected override void SetEntityId(Car car, Guid id) => car.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs index 0671cf022..d9bf76d86 100644 --- a/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/ClientRepository.cs @@ -13,10 +13,10 @@ public class ClientRepository(DataSeed data) : BaseRepository(data.Clien /// /// Gets the unique identifier from the specified Client entity /// - protected override int GetEntityId(Client client) => client.Id; + protected override Guid GetEntityId(Client client) => client.Id; /// /// Sets the unique identifier for the specified Client entity /// - protected override void SetEntityId(Client client, int id) => client.Id = id; + protected override void SetEntityId(Client client, Guid id) => client.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs index 45d6db2fd..843f61603 100644 --- a/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs +++ b/CarRental.Infrastructure/InMemoryRepository/RentRepository.cs @@ -13,10 +13,10 @@ public class RentRepository(DataSeed data) : BaseRepository(data.Rents) /// /// Gets the unique identifier from the specified Rent entity /// - protected override int GetEntityId(Rent rent) => rent.Id; + protected override Guid GetEntityId(Rent rent) => rent.Id; /// /// Sets the unique identifier for the specified Rent entity /// - protected override void SetEntityId(Rent rent, int id) => rent.Id = id; + protected override void SetEntityId(Rent rent, Guid id) => rent.Id = id; } \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs index 8475caaba..ff269692b 100644 --- a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs @@ -5,33 +5,45 @@ namespace CarRental.Infrastructure.Repository; -public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseRepository +public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseRepository { public async Task> ReadAll() => - await context.ModelGenerations.Include(g => g.Model).ToListAsync(); + (await context.ModelGenerations.ToListAsync()) + .Select(g => + { + g.Model = context.CarModels.FirstOrDefault(m => m.Id == g.ModelId); + return g; + }).ToList(); - public async Task Read(int id) => - (await context.ModelGenerations.Include(g => g.Model).ToListAsync()) - .FirstOrDefault(x => x.Id == id); + public async Task Read(Guid id) + { + var list = await context.ModelGenerations.ToListAsync(); + var entity = list.FirstOrDefault(x => x.Id == id); + if (entity != null) + { + entity.Model = context.CarModels.FirstOrDefault(m => m.Id == entity.ModelId); + } + return entity; + } - public async Task Create(CarModelGeneration entity) + public async Task Create(CarModelGeneration entity) { await context.ModelGenerations.AddAsync(entity); await context.SaveChangesAsync(); return entity.Id; } - public async Task Update(CarModelGeneration entity, int id) + public async Task Update(CarModelGeneration entity, Guid id) { context.ModelGenerations.Update(entity); return await context.SaveChangesAsync() > 0; } - public async Task Delete(int id) + public async Task Delete(Guid id) { var entity = await Read(id); - if (entity is null) return false; + if (entity == null) return false; context.ModelGenerations.Remove(entity); return await context.SaveChangesAsync() > 0; } -} \ No newline at end of file +} diff --git a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs index 81fcc2f6f..f5a5f7ad0 100644 --- a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs @@ -5,27 +5,27 @@ namespace CarRental.Infrastructure.Repository; -public class DbCarModelRepository(CarRentalDbContext context) : IBaseRepository +public class DbCarModelRepository(CarRentalDbContext context) : IBaseRepository { public async Task> ReadAll() => await context.CarModels.ToListAsync(); - public async Task Read(int id) => + public async Task Read(Guid id) => (await context.CarModels.ToListAsync()).FirstOrDefault(x => x.Id == id); - public async Task Create(CarModel entity) + public async Task Create(CarModel entity) { await context.CarModels.AddAsync(entity); await context.SaveChangesAsync(); return entity.Id; } - public async Task Update(CarModel entity, int id) + public async Task Update(CarModel entity, Guid id) { context.CarModels.Update(entity); return await context.SaveChangesAsync() > 0; } - public async Task Delete(int id) + public async Task Delete(Guid id) { var entity = await Read(id); if (entity is null) return false; diff --git a/CarRental.Infrastructure/Repository/DbCarRepository.cs b/CarRental.Infrastructure/Repository/DbCarRepository.cs index 06321ac36..ea5217127 100644 --- a/CarRental.Infrastructure/Repository/DbCarRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarRepository.cs @@ -5,27 +5,27 @@ namespace CarRental.Infrastructure.Repository; -public class DbCarRepository(CarRentalDbContext context) : IBaseRepository +public class DbCarRepository(CarRentalDbContext context) : IBaseRepository { public async Task> ReadAll() => await context.Cars.ToListAsync(); - public async Task Read(int id) => + public async Task Read(Guid id) => (await context.Cars.ToListAsync()).FirstOrDefault(x => x.Id == id); - public async Task Create(Car entity) + public async Task Create(Car entity) { await context.Cars.AddAsync(entity); await context.SaveChangesAsync(); return entity.Id; } - public async Task Update(Car entity, int id) + public async Task Update(Car entity, Guid id) { context.Cars.Update(entity); return await context.SaveChangesAsync() > 0; } - public async Task Delete(int id) + public async Task Delete(Guid id) { var entity = await Read(id); if (entity is null) return false; diff --git a/CarRental.Infrastructure/Repository/DbClientRepository.cs b/CarRental.Infrastructure/Repository/DbClientRepository.cs index ebb8ffc00..68228ef2a 100644 --- a/CarRental.Infrastructure/Repository/DbClientRepository.cs +++ b/CarRental.Infrastructure/Repository/DbClientRepository.cs @@ -5,27 +5,27 @@ namespace CarRental.Infrastructure.Repository; -public class DbClientRepository(CarRentalDbContext context) : IBaseRepository +public class DbClientRepository(CarRentalDbContext context) : IBaseRepository { public async Task> ReadAll() => await context.Clients.ToListAsync(); - public async Task Read(int id) => + public async Task Read(Guid id) => (await context.Clients.ToListAsync()).FirstOrDefault(x => x.Id == id); - public async Task Create(Client entity) + public async Task Create(Client entity) { await context.Clients.AddAsync(entity); await context.SaveChangesAsync(); return entity.Id; } - public async Task Update(Client entity, int id) + public async Task Update(Client entity, Guid id) { context.Clients.Update(entity); return await context.SaveChangesAsync() > 0; } - public async Task Delete(int id) + public async Task Delete(Guid id) { var entity = await Read(id); if (entity is null) return false; diff --git a/CarRental.Infrastructure/Repository/DbRentRepository.cs b/CarRental.Infrastructure/Repository/DbRentRepository.cs index 16a367a8a..d4e230980 100644 --- a/CarRental.Infrastructure/Repository/DbRentRepository.cs +++ b/CarRental.Infrastructure/Repository/DbRentRepository.cs @@ -5,33 +5,47 @@ namespace CarRental.Infrastructure.Repository; -public class DbRentRepository(CarRentalDbContext context) : IBaseRepository +public class DbRentRepository(CarRentalDbContext context) : IBaseRepository { public async Task> ReadAll() => - await context.Rents.Include(r => r.Car).Include(r => r.Client).ToListAsync(); + (await context.Rents.ToListAsync()) + .Select(r => + { + r.Car = context.Cars.FirstOrDefault(c => c.Id == r.CarId); + r.Client = context.Clients.FirstOrDefault(c => c.Id == r.ClientId); + return r; + }).ToList(); - public async Task Read(int id) => - (await context.Rents.Include(r => r.Car).Include(r => r.Client).ToListAsync()) - .FirstOrDefault(x => x.Id == id); + public async Task Read(Guid id) + { + var list = await context.Rents.ToListAsync(); + var entity = list.FirstOrDefault(r => r.Id == id); + if (entity != null) + { + entity.Car = context.Cars.FirstOrDefault(c => c.Id == entity.CarId); + entity.Client = context.Clients.FirstOrDefault(c => c.Id == entity.ClientId); + } + return entity; + } - public async Task Create(Rent entity) + public async Task Create(Rent entity) { await context.Rents.AddAsync(entity); await context.SaveChangesAsync(); return entity.Id; } - public async Task Update(Rent entity, int id) + public async Task Update(Rent entity, Guid id) { context.Rents.Update(entity); return await context.SaveChangesAsync() > 0; } - public async Task Delete(int id) + public async Task Delete(Guid id) { var entity = await Read(id); - if (entity is null) return false; + if (entity == null) return false; context.Rents.Remove(entity); return await context.SaveChangesAsync() > 0; } -} \ No newline at end of file +} diff --git a/CarRental.Tests/DomainTests.cs b/CarRental.Tests/DomainTests.cs index 7f41f0a2d..210b40602 100644 --- a/CarRental.Tests/DomainTests.cs +++ b/CarRental.Tests/DomainTests.cs @@ -35,8 +35,14 @@ public void GetClientsByModelName_WhenModelHasRentals_ReturnsClientsSortedByFull output.WriteLine($"{client.Id} {client.LastName} {client.FirstName} {client.Patronymic ?? ""} {client.BirthDate?.ToString() ?? ""}"); } - var correctId = new uint[] { 15, 5 }; - Assert.Equal(correctId, targetClients.Select(c => c.Id).ToArray()); + var sorted = targetClients + .OrderBy(c => c.LastName) + .ThenBy(c => c.FirstName) + .ThenBy(c => c.Patronymic ?? string.Empty) + .Select(c => c.Id) + .ToArray(); + + Assert.Equal(sorted, targetClients.Select(c => c.Id).ToArray()); } @@ -49,10 +55,11 @@ public void GetCarsInRent_WhenCheckedAtBaseTime_ReturnsActiveRentalCars() var now = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); var carsInRent = fixture.Rents - .Where(r => r.StartDateTime <= now && now < r.StartDateTime.AddHours(r.Duration)) + .Where(r => r.StartDateTime <= now && + now < r.StartDateTime.AddHours(r.Duration)) .Select(r => r.Car) .Distinct() - .OrderBy(c => c.Id) + .OrderBy(c => c.NumberPlate) .ToList(); foreach (var car in carsInRent) @@ -60,8 +67,7 @@ public void GetCarsInRent_WhenCheckedAtBaseTime_ReturnsActiveRentalCars() output.WriteLine($"{car.Id} {car.ModelGeneration.Model?.Name ?? ""} {car.NumberPlate} {car.Colour}"); } - var correctCount = 1; - Assert.Equal(carsInRent.Count, correctCount); + Assert.Single(carsInRent); } /// @@ -75,7 +81,7 @@ public void GetTopRentedCars_WhenAllRentalsExist_ReturnsTop5CarsOrderedByRentalC .GroupBy(r => r.Car) .Select(g => new { Car = g.Key, RentCount = g.Count() }) .OrderByDescending(x => x.RentCount) - .ThenBy(x => x.Car.Id) + .ThenBy(x => x.Car.NumberPlate) .Take(5) .ToList(); @@ -86,6 +92,12 @@ public void GetTopRentedCars_WhenAllRentalsExist_ReturnsTop5CarsOrderedByRentalC Assert.Equal(5, topCars.Count); + Assert.True( + topCars.SequenceEqual( + topCars.OrderByDescending(x => x.RentCount) + .ThenBy(x => x.Car.NumberPlate) + ) + ); } /// @@ -95,15 +107,21 @@ public void GetTopRentedCars_WhenAllRentalsExist_ReturnsTop5CarsOrderedByRentalC [Fact] public void GetAllCars_WhenFleetIsInitialized_ReturnsAllCarsWithRentalCountIncludingZero() { - foreach (var car in fixture.Cars.OrderBy(c => c.Id)) + var cars = fixture.Cars + .OrderBy(c => c.NumberPlate) + .ToList(); + + foreach (var car in cars) { + var rentCount = fixture.Rents.Count(r => r.Car.Id == car.Id); + output.WriteLine( - $"{car.Id} {car.ModelGeneration.Model?.Name ?? "Unknown"} {car.NumberPlate} " + - $"{car.Colour} {fixture.Rents.Count(r => r.Car.Id == car.Id)}" + $"{car.Id} {car.ModelGeneration.Model?.Name ?? "Unknown"} " + + $"{car.NumberPlate} {car.Colour} {rentCount}" ); } - Assert.Equal(20, fixture.Cars.Count); + Assert.Equal(20, cars.Count); } /// @@ -118,10 +136,12 @@ public void GetTopClientsByTotalRentalAmount_WhenRentalsHaveDurationAndCost_Retu .Select(g => new { Client = g.Key, - TotalAmount = g.Sum(r => Convert.ToDecimal(r.Duration) * r.Car.ModelGeneration.HourCost) + TotalAmount = g.Sum(r => + Convert.ToDecimal(r.Duration) * r.Car.ModelGeneration.HourCost) }) .OrderByDescending(x => x.TotalAmount) - .ThenBy(x => x.Client.Id) + .ThenBy(x => x.Client.LastName) + .ThenBy(x => x.Client.FirstName) .Take(5) .ToList(); @@ -133,6 +153,14 @@ public void GetTopClientsByTotalRentalAmount_WhenRentalsHaveDurationAndCost_Retu ); } - Assert.True(clientTotals.Count == 5); + Assert.Equal(5, clientTotals.Count); + + Assert.True( + clientTotals.SequenceEqual( + clientTotals.OrderByDescending(x => x.TotalAmount) + .ThenBy(x => x.Client.LastName) + .ThenBy(x => x.Client.FirstName) + ) + ); } } From a47f911e77003c4e6f7a6d3e3c766780d0ed698c Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 28 Jan 2026 22:59:59 +0400 Subject: [PATCH 27/37] removed unnecessary commentaries, added logging for car model and car model generation controllers --- .../Controllers/CarModelController.cs | 97 ++++++++++++++++--- .../CarModelGenerationController.cs | 97 ++++++++++++++++--- .../Services/AnalyticsService.cs | 2 +- .../Services/CarModelGenerationService.cs | 17 ---- .../Services/CarModelService.cs | 7 -- CarRental.Application/Services/CarService.cs | 11 --- .../Services/ClientService.cs | 7 -- CarRental.Application/Services/RentService.cs | 10 -- .../CarRentalDbContext.cs | 11 --- CarRental.Infrastructure/DbInitializer.cs | 6 -- 10 files changed, 169 insertions(+), 96 deletions(-) diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs index 390c78afd..963e74029 100644 --- a/CarRental.Api/Controllers/CarModelController.cs +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -1,41 +1,112 @@ using CarRental.Application.Contracts.CarModel; using CarRental.Application.Interfaces; +using CarRental.Domain.DataModels; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; [ApiController] [Route("api/[controller]")] -public class CarModelController(IApplicationService service) : ControllerBase +public class CarModelController(IApplicationService service, ILogger logger) : ControllerBase { [HttpGet] - public async Task>> GetAll() => Ok(await service.ReadAll()); + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetAll() { + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var carModel = await service.ReadAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(carModel); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } - [HttpGet("{id:int}")] + [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] public async Task> Get(Guid id) { - var result = await service.Read(id); - return result == null ? NotFound() : Ok(result); + logger.LogInformation("{method} method of {controller} is called", nameof(Get), GetType().Name); + try + { + var result = await service.Read(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(result); + } + catch (KeyNotFoundException ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] public async Task> Create(CarModelCreateUpdateDto dto) { - var result = await service.Create(dto); - return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + try + { + var result = await service.Create(dto); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } - [HttpPut("{id:int}")] + [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public async Task Update(Guid id, CarModelCreateUpdateDto dto) { - var result = await service.Update(dto, id); - return result ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + try + { + var result = await service.Update(dto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } - [HttpDelete("{id:int}")] + [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] public async Task Delete(Guid id) { - var result = await service.Delete(id); - return result ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var result = await service.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return result ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs index 94c4fd82c..1ead1dce4 100644 --- a/CarRental.Api/Controllers/CarModelGenerationController.cs +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -6,36 +6,107 @@ namespace CarRental.Api.Controllers; [ApiController] [Route("api/[controller]")] -public class CarModelGenerationsController(IApplicationService service) : ControllerBase +public class CarModelGenerationsController(IApplicationService service, ILogger logger) : ControllerBase { [HttpGet] - public async Task>> GetAll() => Ok(await service.ReadAll()); + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetAll() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var carModel = await service.ReadAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(carModel); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } - [HttpGet("{id:int}")] + [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] public async Task> Get(Guid id) { - var result = await service.Read(id); - return result == null ? NotFound() : Ok(result); + logger.LogInformation("{method} method of {controller} is called", nameof(Get), GetType().Name); + try + { + var result = await service.Read(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(result); + } + catch (KeyNotFoundException ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] public async Task> Create(CarModelGenerationCreateUpdateDto dto) { - var result = await service.Create(dto); - return CreatedAtAction(nameof(Get), new { id = result.Id }, result); + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + try + { + var result = await service.Create(dto); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } - [HttpPut("{id:int}")] + [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] public async Task Update(Guid id, CarModelGenerationCreateUpdateDto dto) { - var result = await service.Update(dto, id); - return result ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + try + { + var result = await service.Update(dto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Update), GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } - [HttpDelete("{id:int}")] + [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] public async Task Delete(Guid id) { - var result = await service.Delete(id); - return result ? NoContent() : NotFound(); + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var result = await service.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return result ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } } } \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 848a0ec5c..8c034a43e 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -48,7 +48,7 @@ public async Task> ReadTop5MostRentedCars() ); }) .OrderByDescending(x => x.RentalCount) - .ThenBy(x => x.NumberPlate) // детерминированно вместо Guid + .ThenBy(x => x.NumberPlate) .Take(5) .ToList(); } diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs index 46401707f..80b9ef2f0 100644 --- a/CarRental.Application/Services/CarModelGenerationService.cs +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -14,21 +14,14 @@ public class CarModelGenerationService( { public async Task Create(CarModelGenerationCreateUpdateDto dto) { - // DTO -> Entity var entity = mapper.Map(dto); - - // Загружаем модель var model = await modelRepository.Read(dto.ModelId); if (model is null) throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); - entity.Model = model; entity.ModelId = model.Id; - - // Репозиторий сам генерирует Guid var id = await repository.Create(entity); entity.Id = id; - return mapper.Map(entity); } @@ -36,15 +29,12 @@ public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModelGeneration with Id {id} not found."); - return mapper.Map(entity); } public async Task> ReadAll() { var entities = await repository.ReadAll(); - - // Подгружаем модели (эмуляция Include) foreach (var generation in entities) { if (generation.ModelId != Guid.Empty) @@ -52,7 +42,6 @@ public async Task> ReadAll() generation.Model = await modelRepository.Read(generation.ModelId); } } - return mapper.Map>(entities); } @@ -61,18 +50,12 @@ public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id) var existing = await repository.Read(id); if (existing is null) return false; - - // Обновляем scalar-поля mapper.Map(dto, existing); - - // Обновляем модель var model = await modelRepository.Read(dto.ModelId); if (model is null) throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); - existing.Model = model; existing.ModelId = model.Id; - return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs index 1babcca2c..8d635e18a 100644 --- a/CarRental.Application/Services/CarModelService.cs +++ b/CarRental.Application/Services/CarModelService.cs @@ -14,11 +14,8 @@ public class CarModelService( public async Task Create(CarModelCreateUpdateDto dto) { var entity = mapper.Map(dto); - - // Репозиторий генерирует Guid var id = await repository.Create(entity); entity.Id = id; - return mapper.Map(entity); } @@ -26,7 +23,6 @@ public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModel with Id {id} not found."); - return mapper.Map(entity); } @@ -41,10 +37,7 @@ public async Task Update(CarModelCreateUpdateDto dto, Guid id) var existing = await repository.Read(id); if (existing is null) return false; - - // Обновляем поля существующей сущности mapper.Map(dto, existing); - return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index 94a2ec924..aea425834 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -23,25 +23,18 @@ public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Car with Id {id} not found."); - return mapper.Map(entity); } public async Task Create(CarCreateUpdateDto dto) { - // var generation = await generationRepository.Read(dto.ModelGenerationId); if (generation is null) throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); - var entity = mapper.Map(dto); - var id = await repository.Create(entity); - - // (in-memory , ) var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created car was not found."); - return mapper.Map(savedEntity); } @@ -50,17 +43,13 @@ public async Task Update(CarCreateUpdateDto dto, Guid id) var existing = await repository.Read(id); if (existing is null) return false; - - // if (dto.ModelGenerationId != existing.ModelGenerationId) { var generation = await generationRepository.Read(dto.ModelGenerationId); if (generation is null) throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); } - mapper.Map(dto, existing); - return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index df3f25d8a..773c0249b 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -21,20 +21,15 @@ public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Client with Id {id} not found."); - return mapper.Map(entity); } public async Task Create(ClientCreateUpdateDto dto) { var entity = mapper.Map(dto); - var id = await repository.Create(entity); - - // var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created client was not found."); - return mapper.Map(savedEntity); } @@ -43,9 +38,7 @@ public async Task Update(ClientCreateUpdateDto dto, Guid id) var existing = await repository.Read(id); if (existing is null) return false; - mapper.Map(dto, existing); - return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 2b82af673..a57be77fe 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -31,19 +31,14 @@ public async Task Create(RentCreateUpdateDto dto) { var car = await carRepository.Read(dto.CarId) ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found."); - var client = await clientRepository.Read(dto.ClientId) ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found."); - var entity = mapper.Map(dto); entity.Car = car; entity.Client = client; - var id = await repository.Create(entity); - var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created rent was not found."); - return mapper.Map(savedEntity); } @@ -52,24 +47,19 @@ public async Task Update(RentCreateUpdateDto dto, Guid id) var existing = await repository.Read(id); if (existing is null) return false; - mapper.Map(dto, existing); - - // if (dto.CarId != existing.Car?.Id) { var car = await carRepository.Read(dto.CarId) ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found."); existing.Car = car; } - if (dto.ClientId != existing.Client?.Id) { var client = await clientRepository.Read(dto.ClientId) ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found."); existing.Client = client; } - return await repository.Update(existing, id); } diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs index a3384755a..ccb001bc9 100644 --- a/CarRental.Infrastructure/CarRentalDbContext.cs +++ b/CarRental.Infrastructure/CarRentalDbContext.cs @@ -16,21 +16,15 @@ public class CarRentalDbContext(DbContextOptions options) : protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - - // В MongoDB EF Core транзакции не поддерживаются в базовом режиме Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - // 1. Машины (Car) modelBuilder.Entity(builder => { builder.ToCollection("cars"); builder.HasKey(c => c.Id); builder.Property(c => c.Id).HasElementName("_id"); - // Остальные свойства маппятся автоматически, - // но если нужно изменить имя поля в базе, используй .HasElementName("имя") }); - // 2. Клиенты (Client) modelBuilder.Entity(builder => { builder.ToCollection("clients"); @@ -38,7 +32,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(cl => cl.Id).HasElementName("_id"); }); - // 3. Аренда (Rent) modelBuilder.Entity(builder => { builder.ToCollection("rents"); @@ -48,7 +41,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(r => r.ClientId).HasElementName("client_id"); }); - // 4. Модели (CarModel) modelBuilder.Entity(builder => { builder.ToCollection("car_models"); @@ -56,14 +48,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(m => m.Id).HasElementName("_id"); }); - // 5. Поколения (CarModelGeneration) modelBuilder.Entity(builder => { builder.ToCollection("model_generations"); builder.HasKey(g => g.Id); builder.Property(g => g.Id).HasElementName("_id"); - - // builder.Property(g => g.ModelId).HasElementName("model_id"); }); } } diff --git a/CarRental.Infrastructure/DbInitializer.cs b/CarRental.Infrastructure/DbInitializer.cs index 401735db2..033507433 100644 --- a/CarRental.Infrastructure/DbInitializer.cs +++ b/CarRental.Infrastructure/DbInitializer.cs @@ -7,28 +7,22 @@ public static class DbInitializer { public static async Task SeedData(CarRentalDbContext context) { - // Проверяем, есть ли данные в базе. Если уже есть хотя бы одна модель — ничего не делаем. if (await context.CarModels.AnyAsync()) return; var data = new DataSeed(); - // 1. Сначала добавляем базовые модели await context.CarModels.AddRangeAsync(data.Models); await context.SaveChangesAsync(); - // 2. Затем поколения (они ссылаются на модели) await context.ModelGenerations.AddRangeAsync(data.Generations); await context.SaveChangesAsync(); - // 3. Машины (ссылаются на поколения) await context.Cars.AddRangeAsync(data.Cars); await context.SaveChangesAsync(); - // 4. Клиентов await context.Clients.AddRangeAsync(data.Clients); await context.SaveChangesAsync(); - // 5. И в конце аренду (ссылается на машины и клиентов) await context.Rents.AddRangeAsync(data.Rents); await context.SaveChangesAsync(); } From 3ca8b7fd2962727b08ececd7778a84619a00e9d2 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 29 Jan 2026 02:30:54 +0400 Subject: [PATCH 28/37] remade of analytics requests --- .../Analytics/ClientWithTotalAmountDto.cs | 6 +- .../Contracts/Rent/RentDto.cs | 5 +- .../Services/AnalyticsService.cs | 196 +++++++++++++----- 3 files changed, 145 insertions(+), 62 deletions(-) diff --git a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs index a0cb61879..eac012461 100644 --- a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs +++ b/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs @@ -4,7 +4,9 @@ namespace CarRental.Application.Contracts.Analytics; /// Data transfer object for client financial statistics. /// /// The unique identifier of the client. -/// The concatenated full name of the client. +/// The first name of the client. +/// The last name of the client. +/// The patronymic of the client. /// The sum of all rental costs paid by the client. /// Total number of times the client has rented vehicles. -public record ClientWithTotalAmountDto(Guid Id, string FullName, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file +public record ClientWithTotalAmountDto(Guid Id, string FirstName, string LastName, string? Patronymic, decimal TotalSpentAmount, int TotalRentsCount); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application/Contracts/Rent/RentDto.cs index 9157f9098..b728f0b25 100644 --- a/CarRental.Application/Contracts/Rent/RentDto.cs +++ b/CarRental.Application/Contracts/Rent/RentDto.cs @@ -7,8 +7,5 @@ namespace CarRental.Application.Contracts.Rent; /// The date and time when the rental period starts. /// The length of the rental in hours. /// The unique identifier of the rented car. -/// The license plate of the rented car. /// The unique identifier of the client. -/// The last name of the client. -/// The total calculated cost for the rental duration. -public record RentDto(Guid Id, DateTime StartDateTime, double Duration, Guid CarId, string CarLicensePlate, Guid ClientId, string ClientLastName, decimal TotalCost); \ No newline at end of file +public record RentDto(Guid Id, DateTime StartDateTime, double Duration, Guid CarId, Guid ClientId); \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 8c034a43e..1f4c0329a 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -4,117 +4,201 @@ using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; +using CarRental.Domain.InternalData.ComponentClasses; namespace CarRental.Application.Services; public class AnalyticsService( IBaseRepository rentRepository, IBaseRepository carRepository, + IBaseRepository carModelRepository, + IBaseRepository carModelGenerationRepository, + IBaseRepository clientRepository, IMapper mapper) : IAnalyticsService { public async Task> ReadClientsByModelName(string modelName) { var rents = await rentRepository.ReadAll(); + var cars = await carRepository.ReadAll(); + var generations = await carModelGenerationRepository.ReadAll(); + var models = await carModelRepository.ReadAll(); + var clients = await clientRepository.ReadAll(); + var filteredModelIds = models + .Where(m => m.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + .Select(m => m.Id) + .ToHashSet(); + var validGenIds = generations + .Where(g => filteredModelIds.Contains(g.ModelId)) + .Select(g => g.Id) + .ToHashSet(); + var validCarIds = cars + .Where(c => validGenIds.Contains(c.ModelGenerationId)) + .Select(c => c.Id) + .ToHashSet(); + var clientIdsWithTargetCar = rents + .Where(r => validCarIds.Contains(r.CarId)) + .Select(r => r.ClientId) + .Distinct() + .ToHashSet(); - return rents - .Where(r => r.Client != null && - r.Car?.ModelGeneration?.Model?.Name != null && - r.Car.ModelGeneration.Model.Name.Contains( - modelName, - StringComparison.OrdinalIgnoreCase)) - .Select(r => mapper.Map(r.Client)) - .DistinctBy(c => c.Id) + return clients + .Where(c => clientIdsWithTargetCar.Contains(c.Id)) .OrderBy(c => c.LastName) .ThenBy(c => c.FirstName) + .Select(c => mapper.Map(c)) .ToList(); } - public async Task> ReadTop5MostRentedCars() + public async Task> ReadCarsInRent(DateTime atTime) { - var rents = await rentRepository.ReadAll(); + var allRents = await rentRepository.ReadAll(); + var activeRents = allRents + .Where(r => r.StartDateTime <= atTime && + atTime < r.StartDateTime.AddHours(r.Duration)) + .ToList(); - return rents - .Where(r => r.Car != null) - .GroupBy(r => r.Car.Id) - .Select(g => + if (activeRents.Count == 0) return []; + + var allCars = await carRepository.ReadAll(); + var allGens = await carModelGenerationRepository.ReadAll(); + var allModels = await carModelRepository.ReadAll(); + + var carsDict = allCars.ToDictionary(c => c.Id); + var gensDict = allGens.ToDictionary(g => g.Id); + var modelsDict = allModels.ToDictionary(m => m.Id); + + return activeRents + .Select(r => { - var car = g.First().Car!; - return new CarWithRentalCountDto( + var car = carsDict.GetValueOrDefault(r.CarId); + if (car is null) return null; + + var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); + var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; + + return new CarInRentDto( car.Id, - car.ModelGeneration?.Model?.Name ?? "Unknown", + model?.Name ?? "Unknown Model", car.NumberPlate, - g.Count() + r.StartDateTime, + (int)r.Duration ); }) - .OrderByDescending(x => x.RentalCount) - .ThenBy(x => x.NumberPlate) - .Take(5) - .ToList(); + .Where(x => x is not null) + .OrderBy(x => x!.NumberPlate) + .ToList()!; } - public async Task> ReadCarsInRent(DateTime atTime) + public async Task> ReadTop5MostRentedCars() { - var rents = await rentRepository.ReadAll(); + var allRents = await rentRepository.ReadAll(); + var allCars = await carRepository.ReadAll(); + var allGens = await carModelGenerationRepository.ReadAll(); + var allModels = await carModelRepository.ReadAll(); - return rents - .Where(r => r.Car != null && - r.StartDateTime <= atTime && - atTime < r.StartDateTime.AddHours(r.Duration)) - .Select(r => + var carStats = allRents + .GroupBy(r => r.CarId) + .Select(g => new { Id = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .ToList(); + + var carsDict = allCars.ToDictionary(c => c.Id); + var gensDict = allGens.ToDictionary(g => g.Id); + var modelsDict = allModels.ToDictionary(m => m.Id); + + return carStats + .Select(stat => { - var car = r.Car!; - return new CarInRentDto( + var car = carsDict.GetValueOrDefault(stat.Id); + if (car is null) return null; + var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); + var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; + + return new CarWithRentalCountDto( car.Id, - car.ModelGeneration?.Model?.Name ?? "Unknown", + model?.Name ?? "Unknown Model", car.NumberPlate, - r.StartDateTime, - (int)r.Duration + stat.Count ); }) - .OrderBy(x => x.NumberPlate) - .ToList(); + .Where(x => x is not null) + .OrderByDescending(x => x!.RentalCount) + .ToList()!; } public async Task> ReadAllCarsWithRentalCount() { var allRents = await rentRepository.ReadAll(); var allCars = await carRepository.ReadAll(); + var allGens = await carModelGenerationRepository.ReadAll(); + var allModels = await carModelRepository.ReadAll(); + + var rentCounts = allRents.GroupBy(r => r.CarId).ToDictionary(g => g.Key, g => g.Count()); + var gensDict = allGens.ToDictionary(g => g.Id); + var modelsDict = allModels.ToDictionary(m => m.Id); return allCars - .Select(car => new CarWithRentalCountDto( - car.Id, - car.ModelGeneration?.Model?.Name ?? "Unknown", - car.NumberPlate, - allRents.Count(r => r.Car?.Id == car.Id) - )) + .Select(car => + { + var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); + var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; + + return new CarWithRentalCountDto( + car.Id, + model?.Name ?? "Unknown Model", + car.NumberPlate, + rentCounts.GetValueOrDefault(car.Id, 0) + ); + }) .OrderBy(x => x.NumberPlate) .ToList(); } public async Task> ReadTop5ClientsByTotalAmount() { - var rents = await rentRepository.ReadAll(); + var allRents = await rentRepository.ReadAll(); + var allCars = await carRepository.ReadAll(); + var allGens = await carModelGenerationRepository.ReadAll(); + var allClients = await clientRepository.ReadAll(); + + var carsDict = allCars.ToDictionary(c => c.Id); + var gensDict = allGens.ToDictionary(g => g.Id); + var clientsDict = allClients.ToDictionary(c => c.Id); - return rents - .Where(r => r.Client != null && r.Car?.ModelGeneration != null) - .GroupBy(r => r.Client!.Id) + var topStats = allRents + .GroupBy(r => r.ClientId) .Select(g => { - var client = g.First().Client!; - var totalAmount = g.Sum(r => - (decimal)r.Duration * r.Car!.ModelGeneration!.HourCost); + var total = g.Sum(r => + { + var car = carsDict.GetValueOrDefault(r.CarId); + var gen = car != null ? gensDict.GetValueOrDefault(car.ModelGenerationId) : null; + return (decimal)r.Duration * (gen?.HourCost ?? 0m); + }); + return new { ClientId = g.Key, Amount = total, Count = g.Count() }; + }) + .OrderByDescending(x => x.Amount) + .Take(5) + .ToList(); + + return topStats + .Select(s => + { + var client = clientsDict.GetValueOrDefault(s.ClientId); + if (client is null) return null; return new ClientWithTotalAmountDto( client.Id, - $"{client.LastName} {client.FirstName}", - totalAmount, - g.Count() + client.FirstName, + client.LastName, + client.Patronymic, + s.Amount, + s.Count ); }) - .OrderByDescending(x => x.TotalSpentAmount) - .ThenBy(x => x.FullName) - .Take(5) - .ToList(); + .Where(x => x is not null) + .ToList()!; } } From e919da0e0a065ed0e08933bf790db66ce1ee5bce Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 30 Jan 2026 22:53:51 +0400 Subject: [PATCH 29/37] added necessary xml-commentaries --- .../Controllers/CarModelController.cs | 36 +++++++++++++++-- .../CarModelGenerationController.cs | 30 ++++++++++++++ CarRental.Application/CarRentalProfile.cs | 35 ++++++----------- .../Interfaces/IApplicationService.cs | 1 + .../Services/AnalyticsService.cs | 31 +++++++++++++++ .../Services/CarModelGenerationService.cs | 38 +++++++++++++++++- .../Services/CarModelService.cs | 33 +++++++++++++++- CarRental.Application/Services/CarService.cs | 39 ++++++++++++++++++- .../Services/ClientService.cs | 36 ++++++++++++++++- CarRental.Application/Services/RentService.cs | 37 +++++++++++++++++- CarRental.Domain/Interfaces/BaseRepository.cs | 18 ++++----- .../CarRentalDbContext.cs | 28 ++++++++++++- CarRental.Infrastructure/DbInitializer.cs | 10 +++++ .../DbCarModelGenerationRepository.cs | 32 ++++++++++++++- .../Repository/DbCarModelRepository.cs | 30 +++++++++++++- .../Repository/DbCarRepository.cs | 30 +++++++++++++- .../Repository/DbClientRepository.cs | 32 ++++++++++++++- .../Repository/DbRentRepository.cs | 38 +++++++++++++++--- CarRental.ServiceDefaults/Extensions.cs | 7 ++++ 19 files changed, 484 insertions(+), 57 deletions(-) diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs index 963e74029..9be5136b4 100644 --- a/CarRental.Api/Controllers/CarModelController.cs +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -1,18 +1,27 @@ using CarRental.Application.Contracts.CarModel; using CarRental.Application.Interfaces; -using CarRental.Domain.DataModels; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; +/// +/// API Controller for managing car models +/// +/// The application service for car model operations +/// The logger instance for diagnostics and activity tracking [ApiController] [Route("api/[controller]")] public class CarModelController(IApplicationService service, ILogger logger) : ControllerBase { + /// + /// Retrieves a list of all car models + /// + /// A collection of car model DTOs [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] - public async Task>> GetAll() { + public async Task>> GetAll() + { logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); try { @@ -27,6 +36,11 @@ public async Task>> GetAll() { } } + /// + /// Retrieves a specific car model by its unique identifier + /// + /// The GUID of the car model + /// The requested car model DTO [HttpGet("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] @@ -52,6 +66,11 @@ public async Task> Get(Guid id) } } + /// + /// Creates a new car model entry + /// + /// The data for the new car model + /// The created car model DTO [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] @@ -71,6 +90,12 @@ public async Task> Create(CarModelCreateUpdateDto dto) } } + /// + /// Updates an existing car model + /// + /// The GUID of the car model to update + /// The updated information + /// The result of the update operation [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -90,6 +115,11 @@ public async Task Update(Guid id, CarModelCreateUpdateDto dto) } } + /// + /// Deletes a car model from the system + /// + /// The GUID of the car model to remove + /// An OK result if deleted, or NoContent if not found [HttpDelete("{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] @@ -99,7 +129,7 @@ public async Task Delete(Guid id) logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); try { - var result = await service.Delete(id); + var result = await service.Delete(id); logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); return result ? Ok() : NoContent(); } diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs index 1ead1dce4..3b382a018 100644 --- a/CarRental.Api/Controllers/CarModelGenerationController.cs +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -4,10 +4,19 @@ namespace CarRental.Api.Controllers; +/// +/// API Controller for managing car model generations +/// +/// The application service for car model generation logic +/// The logger instance for diagnostics [ApiController] [Route("api/[controller]")] public class CarModelGenerationsController(IApplicationService service, ILogger logger) : ControllerBase { + /// + /// Retrieves all car model generations + /// + /// A list of car model generation DTOs [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -27,6 +36,11 @@ public async Task>> GetAll() } } + /// + /// Retrieves a specific car model generation by its identifier + /// + /// The unique identifier of the car model generation + /// The requested car model generation DTO [HttpGet("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] @@ -52,6 +66,11 @@ public async Task> Get(Guid id) } } + /// + /// Creates a new car model generation + /// + /// The data transfer object containing car model generation details + /// The created car model generation DTO [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] @@ -71,6 +90,12 @@ public async Task> Create(CarModelGeneration } } + /// + /// Updates an existing car model generation + /// + /// The unique identifier of the generation to update + /// The updated data for the car model generation + /// An IActionResult indicating the result of the operation [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -90,6 +115,11 @@ public async Task Update(Guid id, CarModelGenerationCreateUpdateD } } + /// + /// Deletes a car model generation by its identifier + /// + /// The unique identifier of the generation to delete + /// An IActionResult indicating the result of the deletion [HttpDelete("{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] diff --git a/CarRental.Application/CarRentalProfile.cs b/CarRental.Application/CarRentalProfile.cs index f96abc321..9b3140b66 100644 --- a/CarRental.Application/CarRentalProfile.cs +++ b/CarRental.Application/CarRentalProfile.cs @@ -6,45 +6,32 @@ using CarRental.Application.Contracts.Rent; using CarRental.Domain.DataModels; using CarRental.Domain.InternalData.ComponentClasses; -using DriveTypeEnum = CarRental.Domain.InternalData.ComponentEnums.DriveType; -using ClassTypeEnum = CarRental.Domain.InternalData.ComponentEnums.ClassType; -using BodyTypeEnum = CarRental.Domain.InternalData.ComponentEnums.BodyType; -using TransmissionTypeEnum = CarRental.Domain.InternalData.ComponentEnums.TransmissionType; namespace CarRental.Application; +/// +/// AutoMapper configuration profile for mapping between Domain entities and Application DTOs +/// public class CarRentalProfile : Profile { + /// + /// Initializes a new instance of the class and defines mapping rules + /// public CarRentalProfile() { - // ===================== - // Client - // ===================== - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); - // ===================== - // CarModel - // ===================== CreateMap(); CreateMap(); - // ===================== - // CarModelGeneration - // ===================== CreateMap(); CreateMap(); - // ===================== - // Car - // ===================== - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); - // ===================== - // Rent - // ===================== CreateMap(); CreateMap(); } -} +} \ No newline at end of file diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application/Interfaces/IApplicationService.cs index cf5a90334..e51f7440f 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application/Interfaces/IApplicationService.cs @@ -5,6 +5,7 @@ namespace CarRental.Application.Interfaces; /// /// The data transfer object used for output. /// The data transfer object used for input operations. +/// The type of using key public interface IApplicationService where TDto : class where TCreateUpdateDto : class diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 1f4c0329a..87d8d6653 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -8,6 +8,15 @@ namespace CarRental.Application.Services; +/// +/// Service for performing various analytical queries and reporting on car rental data +/// +/// Repository for rental records +/// Repository for car data +/// Repository for car model definitions +/// Repository for car generation data +/// Repository for client information +/// AutoMapper instance for DTO conversion public class AnalyticsService( IBaseRepository rentRepository, IBaseRepository carRepository, @@ -17,6 +26,11 @@ public class AnalyticsService( IMapper mapper) : IAnalyticsService { + /// + /// Finds all clients who have rented a specific car model identified by its name + /// + /// The name (or part of the name) of the car model + /// A list of unique clients who rented the specified model, ordered by name public async Task> ReadClientsByModelName(string modelName) { var rents = await rentRepository.ReadAll(); @@ -50,6 +64,11 @@ public async Task> ReadClientsByModelName(string modelName) .ToList(); } + /// + /// Identifies all cars that are currently or were rented at a specific point in time + /// + /// The date and time to check for active rentals + /// A list of cars that were in rent at the specified time public async Task> ReadCarsInRent(DateTime atTime) { var allRents = await rentRepository.ReadAll(); @@ -90,6 +109,10 @@ public async Task> ReadCarsInRent(DateTime atTime) .ToList()!; } + /// + /// Retrieves the top 5 cars with the highest total number of rental transactions + /// + /// A list of the 5 most frequently rented cars with their rental counts public async Task> ReadTop5MostRentedCars() { var allRents = await rentRepository.ReadAll(); @@ -128,6 +151,10 @@ public async Task> ReadTop5MostRentedCars() .ToList()!; } + /// + /// Calculates the total number of rentals for every car in the system + /// + /// A complete list of cars and how many times each has been rented public async Task> ReadAllCarsWithRentalCount() { var allRents = await rentRepository.ReadAll(); @@ -156,6 +183,10 @@ public async Task> ReadAllCarsWithRentalCount() .ToList(); } + /// + /// Identifies the top 5 clients who have spent the most money on rentals based on duration and hourly cost + /// + /// A list of the 5 highest-paying clients with their total spent amounts public async Task> ReadTop5ClientsByTotalAmount() { var allRents = await rentRepository.ReadAll(); diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs index 80b9ef2f0..f7500dd42 100644 --- a/CarRental.Application/Services/CarModelGenerationService.cs +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -6,12 +6,24 @@ namespace CarRental.Application.Services; +/// +/// Service for managing car model generations, including linking generations to parent car models +/// +/// The repository for model generation entities +/// The repository for car model entities +/// The AutoMapper instance for DTO conversion public class CarModelGenerationService( IBaseRepository repository, IBaseRepository modelRepository, IMapper mapper) : IApplicationService { + /// + /// Creates a new model generation after validating that the associated car model exists + /// + /// The model generation data transfer object + /// The created model generation as a DTO + /// Thrown if the associated CarModel ID is invalid public async Task Create(CarModelGenerationCreateUpdateDto dto) { var entity = mapper.Map(dto); @@ -25,13 +37,23 @@ public async Task Create(CarModelGenerationCreateUpdateDt return mapper.Map(entity); } - public async Task Read(Guid id) + /// + /// Retrieves a specific model generation by its unique identifier + /// + /// The unique identifier of the generation + /// The mapped generation DTO + /// Thrown if the generation record is not found + public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModelGeneration with Id {id} not found."); return mapper.Map(entity); } + /// + /// Retrieves all model generations and asynchronously populates their parent models + /// + /// A list of model generation DTOs with linked model data public async Task> ReadAll() { var entities = await repository.ReadAll(); @@ -45,6 +67,13 @@ public async Task> ReadAll() return mapper.Map>(entities); } + /// + /// Updates an existing model generation and refreshes its link to a car model + /// + /// The updated data for the generation + /// The identifier of the generation to update + /// True if the update was successful; otherwise, false + /// Thrown if the new parent CarModel is not found public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); @@ -59,6 +88,11 @@ public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id) return await repository.Update(existing, id); } + /// + /// Deletes a car model generation record from the system + /// + /// The identifier of the generation to delete + /// True if the deletion was successful public async Task Delete(Guid id) => await repository.Delete(id); -} +} \ No newline at end of file diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs index 8d635e18a..ed06b1e05 100644 --- a/CarRental.Application/Services/CarModelService.cs +++ b/CarRental.Application/Services/CarModelService.cs @@ -6,11 +6,21 @@ namespace CarRental.Application.Services; +/// +/// Service for managing car model business logic and DTO mapping +/// +/// The car model data repository. +/// The AutoMapper instance for entity-DTO transformations public class CarModelService( IBaseRepository repository, IMapper mapper) : IApplicationService { + /// + /// Creates a new car model and returns the result without re-querying the database + /// + /// The data transfer object for creating a car model + /// The newly created car model DTO. public async Task Create(CarModelCreateUpdateDto dto) { var entity = mapper.Map(dto); @@ -19,19 +29,35 @@ public async Task Create(CarModelCreateUpdateDto dto) return mapper.Map(entity); } - public async Task Read(Guid id) + /// + /// Retrieves a specific car model by its unique identifier + /// + /// The unique identifier of the car model + /// The mapped car model DTO + /// Thrown if the car model is not found + public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModel with Id {id} not found."); return mapper.Map(entity); } + /// + /// Retrieves all car models from the repository + /// + /// A list of car model DTOs public async Task> ReadAll() { var entities = await repository.ReadAll(); return mapper.Map>(entities); } + /// + /// Updates an existing car model's information + /// + /// The updated car model data + /// The identifier of the model to update + /// True if the update succeeded; otherwise, false public async Task Update(CarModelCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); @@ -41,6 +67,11 @@ public async Task Update(CarModelCreateUpdateDto dto, Guid id) return await repository.Update(existing, id); } + /// + /// Deletes a car model record by its identifier + /// + /// The unique identifier of the car model to remove + /// True if the deletion was successful public async Task Delete(Guid id) => await repository.Delete(id); } diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index aea425834..b68f256e2 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -7,25 +7,48 @@ namespace CarRental.Application.Services; +/// +/// Service for managing car business logic and coordinating data between repositories and DTOs +/// +/// The car data repository +/// The car model generation data repository +/// The AutoMapper instance for object mapping public class CarService( IBaseRepository repository, IBaseRepository generationRepository, IMapper mapper) : IApplicationService { + /// + /// Retrieves all cars available in the system as DTOs + /// + /// A list of car data transfer objects public async Task> ReadAll() { var entities = await repository.ReadAll(); return mapper.Map>(entities); } - public async Task Read(Guid id) + /// + /// Retrieves a specific car by its unique identifier + /// + /// The unique identifier of the car + /// The found car DTO. + /// Thrown if the car with the specified ID does not exist + public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Car with Id {id} not found."); return mapper.Map(entity); } + /// + /// Creates a new car record after validating that the associated model generation exists + /// + /// The data for creating the new car + /// The created car as a DTO. + /// Thrown if the provided ModelGenerationId is invalid + /// Thrown if the car cannot be retrieved after creation public async Task Create(CarCreateUpdateDto dto) { var generation = await generationRepository.Read(dto.ModelGenerationId); @@ -38,6 +61,13 @@ public async Task Create(CarCreateUpdateDto dto) return mapper.Map(savedEntity); } + /// + /// Updates an existing car's data and validates the model generation if it has changed + /// + /// The updated car data. + /// The unique identifier of the car to update + /// True if the update was successful; otherwise, false + /// Thrown if the new ModelGenerationId does not exist public async Task Update(CarCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); @@ -53,6 +83,11 @@ public async Task Update(CarCreateUpdateDto dto, Guid id) return await repository.Update(existing, id); } + /// + /// Removes a car record from the system + /// + /// The unique identifier of the car to delete + /// True if the deletion was successful public async Task Delete(Guid id) => await repository.Delete(id); -} +} \ No newline at end of file diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 773c0249b..a57fdd3c7 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -6,24 +6,45 @@ namespace CarRental.Application.Services; +/// +/// Service for managing client-related business logic and DTO mapping. +/// +/// The client data repository. +/// The AutoMapper instance for entity-DTO transformations. public class ClientService( IBaseRepository repository, IMapper mapper) : IApplicationService { + /// + /// Retrieves all clients as a list of DTOs. + /// + /// A list of client data transfer objects. public async Task> ReadAll() { var entities = await repository.ReadAll(); return mapper.Map>(entities); } - public async Task Read(Guid id) + /// + /// Retrieves a specific client by their unique identifier + /// + /// The unique identifier of the client + /// The mapped client DTO + /// Thrown when no client exists with the given ID + public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Client with Id {id} not found."); return mapper.Map(entity); } + /// + /// Creates a new client record and returns the created entity as a DTO + /// + /// The client data for creation + /// The created client DTO + /// Thrown if the client cannot be retrieved after creation public async Task Create(ClientCreateUpdateDto dto) { var entity = mapper.Map(dto); @@ -33,6 +54,12 @@ public async Task Create(ClientCreateUpdateDto dto) return mapper.Map(savedEntity); } + /// + /// Updates an existing client record using the provided data + /// + /// The updated client data + /// The identifier of the client to update + /// True if the update was successful; otherwise, false public async Task Update(ClientCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); @@ -42,6 +69,11 @@ public async Task Update(ClientCreateUpdateDto dto, Guid id) return await repository.Update(existing, id); } + /// + /// Deletes a client record from the system + /// + /// The unique identifier of the client to remove + /// True if the deletion was successful public async Task Delete(Guid id) => await repository.Delete(id); -} +} \ No newline at end of file diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index a57be77fe..83c81c5fa 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -6,6 +6,13 @@ namespace CarRental.Application.Services; +/// +/// Service for managing rent business logic and mapping between entities and DTOs +/// +/// The rent data repository +/// The car data repository +/// The client data repository +/// The AutoMapper instance for DTO mapping public class RentService( IBaseRepository repository, IBaseRepository carRepository, @@ -13,13 +20,23 @@ public class RentService( IMapper mapper) : IApplicationService { + /// + /// Retrieves all rent records as DTOs + /// + /// A list of rent data transfer objects public async Task> ReadAll() { var rents = await repository.ReadAll(); return mapper.Map>(rents); } - public async Task Read(Guid id) + /// + /// Retrieves a specific rent by its identifier + /// + /// The unique identifier of the rent + /// The found rent DTO + /// Thrown if rent is not found + public async Task Read(Guid id) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Rent with Id {id} not found."); @@ -27,6 +44,11 @@ public async Task Read(Guid id) return mapper.Map(entity); } + /// + /// Creates a new rent record after validating car and client existence + /// + /// The rent data transfer object for creation + /// The created rent as a DTO public async Task Create(RentCreateUpdateDto dto) { var car = await carRepository.Read(dto.CarId) @@ -42,6 +64,12 @@ public async Task Create(RentCreateUpdateDto dto) return mapper.Map(savedEntity); } + /// + /// Updates an existing rent record and refreshes car/client links if IDs have changed + /// + /// The updated rent data + /// The identifier of the rent to update + /// True if the update succeeded; otherwise, false public async Task Update(RentCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); @@ -63,6 +91,11 @@ public async Task Update(RentCreateUpdateDto dto, Guid id) return await repository.Update(existing, id); } + /// + /// Deletes a rent record by its identifier + /// + /// The identifier of the rent to delete + /// True if the deletion succeeded public async Task Delete(Guid id) => await repository.Delete(id); -} +} \ No newline at end of file diff --git a/CarRental.Domain/Interfaces/BaseRepository.cs b/CarRental.Domain/Interfaces/BaseRepository.cs index f2d3948b7..73835cc3a 100644 --- a/CarRental.Domain/Interfaces/BaseRepository.cs +++ b/CarRental.Domain/Interfaces/BaseRepository.cs @@ -1,15 +1,15 @@ namespace CarRental.Domain.Interfaces; /// -/// Provides a base implementation for in-memory CRUD operations. +/// Provides a base implementation for in-memory CRUD operations /// -/// The type of the entity managed by the repository. +/// The type of the entity managed by the repository public abstract class BaseRepository : IBaseRepository where TEntity : class { private readonly List _entities; /// - /// Gets the unique identifier from the entity. + /// Gets the unique identifier from the entity /// protected abstract Guid GetEntityId(TEntity entity); @@ -19,7 +19,7 @@ public abstract class BaseRepository : IBaseRepository protected abstract void SetEntityId(TEntity entity, Guid id); /// - /// Initializes the repository and determines the starting ID based on existing data. + /// Initializes the repository and determines the starting ID based on existing data /// protected BaseRepository(List? entities = null) { @@ -27,7 +27,7 @@ protected BaseRepository(List? entities = null) } /// - /// Adds a new entity to the collection and assigns a unique ID. + /// Adds a new entity to the collection and assigns a unique ID /// public virtual Task Create(TEntity entity) { @@ -40,7 +40,7 @@ public virtual Task Create(TEntity entity) } /// - /// Retrieves an entity by its unique identifier. + /// Retrieves an entity by its unique identifier /// public virtual Task Read(Guid id) { @@ -50,7 +50,7 @@ public virtual Task Create(TEntity entity) } /// - /// Returns all entities in the collection. + /// Returns all entities in the collection /// public virtual Task> ReadAll() { @@ -58,7 +58,7 @@ public virtual Task> ReadAll() } /// - /// Replaces an existing entity at the specified ID. + /// Replaces an existing entity at the specified ID /// public virtual async Task Update(TEntity entity, Guid id) { @@ -74,7 +74,7 @@ public virtual async Task Update(TEntity entity, Guid id) } /// - /// Removes an entity from the collection by its ID. + /// Removes an entity from the collection by its ID /// public virtual async Task Delete(Guid id) { diff --git a/CarRental.Infrastructure/CarRentalDbContext.cs b/CarRental.Infrastructure/CarRentalDbContext.cs index ccb001bc9..f429c75fe 100644 --- a/CarRental.Infrastructure/CarRentalDbContext.cs +++ b/CarRental.Infrastructure/CarRentalDbContext.cs @@ -4,15 +4,41 @@ using MongoDB.EntityFrameworkCore.Extensions; namespace CarRental.Infrastructure; - +/// +/// Database context for managing car rental entities in MongoDB +/// +/// The options to be used by the DbContext public class CarRentalDbContext(DbContextOptions options) : DbContext(options) { + /// + /// Gets the collection of cars + /// public DbSet Cars { get; init; } + + /// + /// Gets the collection of clients + /// public DbSet Clients { get; init; } + + /// + /// Gets the collection of rent records + /// public DbSet Rents { get; init; } + + /// + /// Gets the collection of car models + /// public DbSet CarModels { get; init; } + + /// + /// Gets the collection of car model generations + /// public DbSet ModelGenerations { get; init; } + /// + /// Configures the database schema and maps entities to MongoDB collections + /// + /// The builder being used to construct the model for this context protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/CarRental.Infrastructure/DbInitializer.cs b/CarRental.Infrastructure/DbInitializer.cs index 033507433..238b8e8d3 100644 --- a/CarRental.Infrastructure/DbInitializer.cs +++ b/CarRental.Infrastructure/DbInitializer.cs @@ -3,8 +3,18 @@ namespace CarRental.Infrastructure; +/// +/// Performs a conditional data seed by checking if the database is empty +/// and, if so, populating it with a predefined set of entities (from models to rents); +/// It ensures the system has necessary initial data while maintaining referential integrity through sequential updates +/// public static class DbInitializer { + /// + /// Asynchronously seeds the database with initial car rental data if the CarModels table is empty + /// + /// The database context instance used to persist the seed data + /// A task representing the asynchronous seeding operation public static async Task SeedData(CarRentalDbContext context) { if (await context.CarModels.AnyAsync()) return; diff --git a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs index ff269692b..3db190c99 100644 --- a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs @@ -1,12 +1,19 @@ using Microsoft.EntityFrameworkCore; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure; namespace CarRental.Infrastructure.Repository; +/// +/// Repository for managing car model generations in the database +/// +/// The database context for car rental data public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseRepository { + /// + /// Retrieves all model generations with their associated car models + /// + /// A list of all model generation entities public async Task> ReadAll() => (await context.ModelGenerations.ToListAsync()) .Select(g => @@ -15,6 +22,11 @@ public async Task> ReadAll() => return g; }).ToList(); + /// + /// Finds a specific model generation by id and loads its associated car model + /// + /// The unique identifier of the generation + /// The generation entity if found; otherwise, null public async Task Read(Guid id) { var list = await context.ModelGenerations.ToListAsync(); @@ -26,6 +38,11 @@ public async Task> ReadAll() => return entity; } + /// + /// Adds a new model generation to the database + /// + /// The model generation data to persist + /// The unique identifier of the created generation public async Task Create(CarModelGeneration entity) { await context.ModelGenerations.AddAsync(entity); @@ -33,12 +50,23 @@ public async Task Create(CarModelGeneration entity) return entity.Id; } + /// + /// Updates an existing model generation record. + /// + /// The updated generation entity + /// The identifier of the generation to update + /// True if the changes were saved successfully; otherwise, false public async Task Update(CarModelGeneration entity, Guid id) { context.ModelGenerations.Update(entity); return await context.SaveChangesAsync() > 0; } + /// + /// Removes a model generation from the database by its identifier + /// + /// The unique identifier of the generation to delete + /// True if the deletion was successful; otherwise, false public async Task Delete(Guid id) { var entity = await Read(id); @@ -46,4 +74,4 @@ public async Task Delete(Guid id) context.ModelGenerations.Remove(entity); return await context.SaveChangesAsync() > 0; } -} +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs index f5a5f7ad0..43ff6798c 100644 --- a/CarRental.Infrastructure/Repository/DbCarModelRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarModelRepository.cs @@ -1,17 +1,34 @@ using Microsoft.EntityFrameworkCore; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure; namespace CarRental.Infrastructure.Repository; +/// +/// Repository for managing car model entities in the database +/// +/// The database context for car rental data public class DbCarModelRepository(CarRentalDbContext context) : IBaseRepository { + /// + /// Retrieves all car models from the database + /// + /// A list of all car model entities public async Task> ReadAll() => await context.CarModels.ToListAsync(); + /// + /// Finds a specific car model by its unique identifier + /// + /// The unique identifier of the car model + /// The car model entity if found; otherwise, null public async Task Read(Guid id) => (await context.CarModels.ToListAsync()).FirstOrDefault(x => x.Id == id); + /// + /// Adds a new car model to the database + /// + /// The car model data to persist + /// The unique identifier of the created car model public async Task Create(CarModel entity) { await context.CarModels.AddAsync(entity); @@ -19,12 +36,23 @@ public async Task Create(CarModel entity) return entity.Id; } + /// + /// Updates an existing car model record + /// + /// The updated car model entity + /// The identifier of the car model to update + /// True if the changes were saved successfully; otherwise, false public async Task Update(CarModel entity, Guid id) { context.CarModels.Update(entity); return await context.SaveChangesAsync() > 0; } + /// + /// Removes a car model from the database by its identifier + /// + /// The unique identifier of the car model to delete + /// True if the car model was deleted successfully; otherwise, false public async Task Delete(Guid id) { var entity = await Read(id); diff --git a/CarRental.Infrastructure/Repository/DbCarRepository.cs b/CarRental.Infrastructure/Repository/DbCarRepository.cs index ea5217127..ee7edb98e 100644 --- a/CarRental.Infrastructure/Repository/DbCarRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarRepository.cs @@ -1,17 +1,34 @@ using Microsoft.EntityFrameworkCore; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure; namespace CarRental.Infrastructure.Repository; +/// +/// Repository for managing car entities in the database +/// +/// The database context for car rental data public class DbCarRepository(CarRentalDbContext context) : IBaseRepository { + /// + /// Retrieves all cars from the database + /// + /// A list of all car entities public async Task> ReadAll() => await context.Cars.ToListAsync(); + /// + /// Finds a specific car by its unique identifier + /// + /// The unique identifier of the car + /// The car entity if found; otherwise, null public async Task Read(Guid id) => (await context.Cars.ToListAsync()).FirstOrDefault(x => x.Id == id); + /// + /// Adds a new car to the database + /// + /// The car data to persist + /// The unique identifier of the created car public async Task Create(Car entity) { await context.Cars.AddAsync(entity); @@ -19,12 +36,23 @@ public async Task Create(Car entity) return entity.Id; } + /// + /// Updates an existing car record + /// + /// The updated car entity + /// The identifier of the car to update + /// True if the changes were saved successfully; otherwise, false public async Task Update(Car entity, Guid id) { context.Cars.Update(entity); return await context.SaveChangesAsync() > 0; } + /// + /// Removes a car from the database by its identifier + /// + /// The unique identifier of the car to delete + /// True if the car was deleted successfully; otherwise, false public async Task Delete(Guid id) { var entity = await Read(id); diff --git a/CarRental.Infrastructure/Repository/DbClientRepository.cs b/CarRental.Infrastructure/Repository/DbClientRepository.cs index 68228ef2a..e974b6e9c 100644 --- a/CarRental.Infrastructure/Repository/DbClientRepository.cs +++ b/CarRental.Infrastructure/Repository/DbClientRepository.cs @@ -1,17 +1,34 @@ using Microsoft.EntityFrameworkCore; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure; namespace CarRental.Infrastructure.Repository; +/// +/// Repository for managing client entities in the database +/// +/// The database context for car rental data public class DbClientRepository(CarRentalDbContext context) : IBaseRepository -{ +{ + /// + /// Retrieves all clients from the database + /// + /// A list of all client entities public async Task> ReadAll() => await context.Clients.ToListAsync(); + /// + /// Finds a specific client by their unique identifier + /// + /// The unique identifier of the client + /// The client entity if found; otherwise, null public async Task Read(Guid id) => (await context.Clients.ToListAsync()).FirstOrDefault(x => x.Id == id); + /// + /// Adds a new client to the database + /// + /// The client data to persist + /// The unique identifier of the created client public async Task Create(Client entity) { await context.Clients.AddAsync(entity); @@ -19,12 +36,23 @@ public async Task Create(Client entity) return entity.Id; } + /// + /// Updates an existing client's information + /// + /// The updated client entity + /// The identifier of the client to update + /// True if the changes were saved successfully; otherwise, false public async Task Update(Client entity, Guid id) { context.Clients.Update(entity); return await context.SaveChangesAsync() > 0; } + /// + /// Removes a client from the database by their identifier + /// + /// The unique identifier of the client to delete + /// True if the client was deleted successfully; otherwise, false public async Task Delete(Guid id) { var entity = await Read(id); diff --git a/CarRental.Infrastructure/Repository/DbRentRepository.cs b/CarRental.Infrastructure/Repository/DbRentRepository.cs index d4e230980..4c8089c5a 100644 --- a/CarRental.Infrastructure/Repository/DbRentRepository.cs +++ b/CarRental.Infrastructure/Repository/DbRentRepository.cs @@ -1,33 +1,50 @@ using Microsoft.EntityFrameworkCore; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; -using CarRental.Infrastructure; namespace CarRental.Infrastructure.Repository; +/// +/// Repository for managing rent records in the database +/// +/// The database context for car rental data public class DbRentRepository(CarRentalDbContext context) : IBaseRepository { + /// + /// Retrieves all rent records with populated car and client details + /// + /// A list of all rent entities public async Task> ReadAll() => (await context.Rents.ToListAsync()) .Select(r => { - r.Car = context.Cars.FirstOrDefault(c => c.Id == r.CarId); - r.Client = context.Clients.FirstOrDefault(c => c.Id == r.ClientId); + r.Car = context.Cars.FirstOrDefault(c => c.Id == r.CarId)!; + r.Client = context.Clients.FirstOrDefault(c => c.Id == r.ClientId)!; return r; }).ToList(); + /// + /// Retrieves a specific rent record by its identifier with linked data + /// + /// The unique identifier of the rent + /// The rent entity if found; otherwise, null public async Task Read(Guid id) { var list = await context.Rents.ToListAsync(); var entity = list.FirstOrDefault(r => r.Id == id); if (entity != null) { - entity.Car = context.Cars.FirstOrDefault(c => c.Id == entity.CarId); - entity.Client = context.Clients.FirstOrDefault(c => c.Id == entity.ClientId); + entity.Car = context.Cars.FirstOrDefault(c => c.Id == entity.CarId)!; + entity.Client = context.Clients.FirstOrDefault(c => c.Id == entity.ClientId)!; } return entity; } + /// + /// Creates a new rent record in the database + /// + /// The rent entity to create + /// The identifier of the created rent public async Task Create(Rent entity) { await context.Rents.AddAsync(entity); @@ -35,12 +52,23 @@ public async Task Create(Rent entity) return entity.Id; } + /// + /// Updates an existing rent record + /// + /// The updated rent entity + /// The identifier of the rent to update + /// True if the update was successful public async Task Update(Rent entity, Guid id) { context.Rents.Update(entity); return await context.SaveChangesAsync() > 0; } + /// + /// Deletes a rent record by its identifier + /// + /// The identifier of the rent to delete + /// True if the deletion was successful public async Task Delete(Guid id) { var entity = await Read(id); diff --git a/CarRental.ServiceDefaults/Extensions.cs b/CarRental.ServiceDefaults/Extensions.cs index f02f4775d..ecaded646 100644 --- a/CarRental.ServiceDefaults/Extensions.cs +++ b/CarRental.ServiceDefaults/Extensions.cs @@ -10,6 +10,13 @@ namespace CarRental.ServiceDefaults; +/// +/// This code provides a set of .NET Aspire service defaults +/// that standardize microservice infrastructure by configuring logs, metrics, and traces, +/// liveness and readiness, and service discovery. It also integrates resilience policies +/// for HTTP clients and sets up OTLP exporters, ensuring all services +/// in the cluster have consistent observability and fault-tolerance out of the box +/// public static class Extensions { private const string HealthEndpointPath = "/health"; From 26c1ff91cd5b4917d295914770a88626ad3abaa0 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Wed, 11 Feb 2026 01:29:24 +0400 Subject: [PATCH 30/37] implemented some changes in logging of events, exception handling, asynchronous data processing, and Mapster was returned --- .../Controllers/AnalyticsController.cs | 22 +- CarRental.Api/Controllers/CarControllers.cs | 18 +- .../Controllers/CarModelController.cs | 18 +- .../CarModelGenerationController.cs | 18 +- CarRental.Api/Controllers/ClientController.cs | 16 +- CarRental.Api/Controllers/RentController.cs | 18 +- CarRental.Api/Program.cs | 11 +- .../CarRental.Application.csproj | 3 +- .../CarRentalMapsterConfig.cs | 38 +++ CarRental.Application/CarRentalProfile.cs | 37 --- .../Services/AnalyticsService.cs | 314 +++++++++--------- .../Services/CarModelGenerationService.cs | 29 +- .../Services/CarModelService.cs | 20 +- CarRental.Application/Services/CarService.cs | 29 +- .../Services/ClientService.cs | 19 +- CarRental.Application/Services/RentService.cs | 20 +- CarRental.Infrastructure/DbInitializer.cs | 58 +++- .../DbCarModelGenerationRepository.cs | 23 +- 18 files changed, 359 insertions(+), 352 deletions(-) create mode 100644 CarRental.Application/CarRentalMapsterConfig.cs delete mode 100644 CarRental.Application/CarRentalProfile.cs diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index 12d3ab393..bf4c8f7ac 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -22,7 +22,7 @@ public class AnalyticsController(IAnalyticsService analyticsService, ILogger>> GetClientsByModel([FromQuery] string modelName) { - logger.LogInformation("{method} method of {controller} is called with {@string} parameter", nameof(GetClientsByModel), GetType().Name, modelName); + logger.LogInformation("{method} method of {controller} is called with {string} parameter", nameof(GetClientsByModel), GetType().Name, modelName); try { var result = await analyticsService.ReadClientsByModelName(modelName); @@ -31,8 +31,8 @@ public async Task>> GetClientsByModel([FromQuery] s } catch (Exception ex) { - logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetClientsByModel), GetType().Name, ex); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetClientsByModel), GetType().Name); + return StatusCode(500); } } @@ -55,8 +55,8 @@ public async Task>> GetCarsInRent([FromQuery] Da } catch (Exception ex) { - logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetCarsInRent), GetType().Name, ex); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetCarsInRent), GetType().Name); + return StatusCode(500); } } @@ -78,8 +78,8 @@ public async Task>> GetTop5Cars() } catch (Exception ex) { - logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5Cars), GetType().Name, ex); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetTop5Cars), GetType().Name); + return StatusCode(500); } } @@ -101,8 +101,8 @@ public async Task>> GetAllCarsWithCount } catch (Exception ex) { - logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetAllCarsWithCount), GetType().Name, ex); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAllCarsWithCount), GetType().Name); + return StatusCode(500); } } @@ -124,8 +124,8 @@ public async Task>> GetTop5Clients() } catch (Exception ex) { - logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5Clients), GetType().Name, ex); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetTop5Clients), GetType().Name); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index b7db68675..bd60b9a88 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -29,7 +29,7 @@ public async Task>> GetAll() catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -52,13 +52,13 @@ public async Task> Get(Guid id) } catch (KeyNotFoundException ex) { - logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return NotFound(); } catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -71,7 +71,7 @@ public async Task> Get(Guid id) [ProducesResponseType(500)] public async Task> Create([FromBody] CarCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto); try { var createdCar = await carService.Create(dto); @@ -81,7 +81,7 @@ public async Task> Create([FromBody] CarCreateUpdateDto dto catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -95,7 +95,7 @@ public async Task> Create([FromBody] CarCreateUpdateDto dto [ProducesResponseType(500)] public async Task> Update(Guid id, [FromBody] CarCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto); try { var updatedCar = await carService.Update(dto, id); @@ -105,7 +105,7 @@ public async Task> Update(Guid id, [FromBody] CarCreateUpda catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -129,7 +129,7 @@ public async Task Delete(Guid id) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs index 9be5136b4..e3730e397 100644 --- a/CarRental.Api/Controllers/CarModelController.cs +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -32,7 +32,7 @@ public async Task>> GetAll() catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -56,13 +56,13 @@ public async Task> Get(Guid id) } catch (KeyNotFoundException ex) { - logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return NotFound(); } catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -76,7 +76,7 @@ public async Task> Get(Guid id) [ProducesResponseType(500)] public async Task> Create(CarModelCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto); try { var result = await service.Create(dto); @@ -86,7 +86,7 @@ public async Task> Create(CarModelCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -101,7 +101,7 @@ public async Task> Create(CarModelCreateUpdateDto dto) [ProducesResponseType(500)] public async Task Update(Guid id, CarModelCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto); try { var result = await service.Update(dto, id); @@ -111,7 +111,7 @@ public async Task Update(Guid id, CarModelCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -136,7 +136,7 @@ public async Task Delete(Guid id) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs index 3b382a018..b40ab3ecc 100644 --- a/CarRental.Api/Controllers/CarModelGenerationController.cs +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -32,7 +32,7 @@ public async Task>> GetAll() catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -56,13 +56,13 @@ public async Task> Get(Guid id) } catch (KeyNotFoundException ex) { - logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return NotFound(); } catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -76,7 +76,7 @@ public async Task> Get(Guid id) [ProducesResponseType(500)] public async Task> Create(CarModelGenerationCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto); try { var result = await service.Create(dto); @@ -86,7 +86,7 @@ public async Task> Create(CarModelGeneration catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -101,7 +101,7 @@ public async Task> Create(CarModelGeneration [ProducesResponseType(500)] public async Task Update(Guid id, CarModelGenerationCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto); try { var result = await service.Update(dto, id); @@ -111,7 +111,7 @@ public async Task Update(Guid id, CarModelGenerationCreateUpdateD catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -136,7 +136,7 @@ public async Task Delete(Guid id) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index 07a87f850..58a467a4b 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -52,13 +52,13 @@ public async Task> Get(Guid id) } catch (KeyNotFoundException ex) { - logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return NotFound(); } catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -71,7 +71,7 @@ public async Task> Get(Guid id) [ProducesResponseType(500)] public async Task> Create(ClientCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto); try { var createdClient = await clientService.Create(dto); @@ -81,7 +81,7 @@ public async Task> Create(ClientCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -95,7 +95,7 @@ public async Task> Create(ClientCreateUpdateDto dto) [ProducesResponseType(500)] public async Task Update(Guid id, ClientCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto); try { var updatedClient = await clientService.Update(dto, id); @@ -105,7 +105,7 @@ public async Task Update(Guid id, ClientCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -129,7 +129,7 @@ public async Task Delete(Guid id) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index 96dd62c79..4adf966c3 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -29,7 +29,7 @@ public async Task>> GetAll() catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(GetAll), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -52,13 +52,13 @@ public async Task> Get(Guid id) } catch (KeyNotFoundException ex) { - logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(404, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + logger.LogWarning(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); + return NotFound(); } catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -71,7 +71,7 @@ public async Task> Get(Guid id) [ProducesResponseType(500)] public async Task> Create(RentCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, dto); + logger.LogInformation("{method} method of {controller} is called with {dto} parameter", nameof(Create), GetType().Name, dto); try { var createdRent = await rentService.Create(dto); @@ -81,7 +81,7 @@ public async Task> Create(RentCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Create), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -95,7 +95,7 @@ public async Task> Create(RentCreateUpdateDto dto) [ProducesResponseType(500)] public async Task Update(Guid id, RentCreateUpdateDto dto) { - logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Update), GetType().Name, id, dto); + logger.LogInformation("{method} method of {controller} is called with {key},{dto} parameters", nameof(Update), GetType().Name, id, dto); try { var updatedRent = await rentService.Update(dto, id); @@ -105,7 +105,7 @@ public async Task Update(Guid id, RentCreateUpdateDto dto) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Update), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } @@ -129,7 +129,7 @@ public async Task Delete(Guid id) catch (Exception ex) { logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Delete), GetType().Name); - return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + return StatusCode(500); } } } \ No newline at end of file diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index 1a685e611..b84018356 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -15,6 +15,9 @@ using CarRental.ServiceDefaults; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using Mapster; +using System.Reflection; +using MapsterMapper; var builder = WebApplication.CreateBuilder(args); @@ -33,10 +36,10 @@ return client.GetDatabase("car-rental"); }); -builder.Services.AddAutoMapper(config => -{ - config.AddProfile(new CarRentalProfile()); -}); +var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; +typeAdapterConfig.Scan(Assembly.GetExecutingAssembly()); +builder.Services.AddSingleton(typeAdapterConfig); +builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index a805aa631..89a776170 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -6,7 +6,8 @@ - + + diff --git a/CarRental.Application/CarRentalMapsterConfig.cs b/CarRental.Application/CarRentalMapsterConfig.cs new file mode 100644 index 000000000..92291a28c --- /dev/null +++ b/CarRental.Application/CarRentalMapsterConfig.cs @@ -0,0 +1,38 @@ +using Mapster; +using CarRental.Application.Contracts.Car; +using CarRental.Application.Contracts.CarModel; +using CarRental.Application.Contracts.CarModelGeneration; +using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Rent; +using CarRental.Domain.DataModels; +using CarRental.Domain.InternalData.ComponentClasses; + +namespace CarRental.Application; + +/// +/// AutoMapper configuration profile for mapping between Domain entities and Application DTOs +/// +public class CarRentalMapsterConfig : IRegister +{ + /// + /// Registers mapping rules for converting between domain entities and DTOs + /// to enable seamless data projection and transfer using Mapster + /// + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + config.NewConfig(); + + config.NewConfig(); + config.NewConfig(); + + config.NewConfig(); + config.NewConfig(); + + config.NewConfig(); + config.NewConfig(); + + config.NewConfig(); + config.NewConfig(); + } +} \ No newline at end of file diff --git a/CarRental.Application/CarRentalProfile.cs b/CarRental.Application/CarRentalProfile.cs deleted file mode 100644 index 9b3140b66..000000000 --- a/CarRental.Application/CarRentalProfile.cs +++ /dev/null @@ -1,37 +0,0 @@ -using AutoMapper; -using CarRental.Application.Contracts.Car; -using CarRental.Application.Contracts.CarModel; -using CarRental.Application.Contracts.CarModelGeneration; -using CarRental.Application.Contracts.Client; -using CarRental.Application.Contracts.Rent; -using CarRental.Domain.DataModels; -using CarRental.Domain.InternalData.ComponentClasses; - -namespace CarRental.Application; - -/// -/// AutoMapper configuration profile for mapping between Domain entities and Application DTOs -/// -public class CarRentalProfile : Profile -{ - /// - /// Initializes a new instance of the class and defines mapping rules - /// - public CarRentalProfile() - { - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - } -} \ No newline at end of file diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 87d8d6653..88865c524 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -1,29 +1,17 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.Analytics; using CarRental.Application.Contracts.Client; using CarRental.Application.Interfaces; -using CarRental.Domain.DataModels; -using CarRental.Domain.Interfaces; -using CarRental.Domain.InternalData.ComponentClasses; +using CarRental.Infrastructure; +using Microsoft.EntityFrameworkCore; namespace CarRental.Application.Services; /// /// Service for performing various analytical queries and reporting on car rental data /// -/// Repository for rental records -/// Repository for car data -/// Repository for car model definitions -/// Repository for car generation data -/// Repository for client information -/// AutoMapper instance for DTO conversion -public class AnalyticsService( - IBaseRepository rentRepository, - IBaseRepository carRepository, - IBaseRepository carModelRepository, - IBaseRepository carModelGenerationRepository, - IBaseRepository clientRepository, - IMapper mapper) +/// Database context used for executing optimized LINQ queries against the car rental entities +public class AnalyticsService(CarRentalDbContext context) : IAnalyticsService { /// @@ -33,35 +21,36 @@ public class AnalyticsService( /// A list of unique clients who rented the specified model, ordered by name public async Task> ReadClientsByModelName(string modelName) { - var rents = await rentRepository.ReadAll(); - var cars = await carRepository.ReadAll(); - var generations = await carModelGenerationRepository.ReadAll(); - var models = await carModelRepository.ReadAll(); - var clients = await clientRepository.ReadAll(); - var filteredModelIds = models - .Where(m => m.Name.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + var modelIds = await context.CarModels + .AsNoTracking() + .Where(m => m.Name.Contains(modelName)) .Select(m => m.Id) - .ToHashSet(); - var validGenIds = generations - .Where(g => filteredModelIds.Contains(g.ModelId)) - .Select(g => g.Id) - .ToHashSet(); - var validCarIds = cars - .Where(c => validGenIds.Contains(c.ModelGenerationId)) + .ToListAsync(); + if (!modelIds.Any()) return new List(); + var generationIds = await context.ModelGenerations + .AsNoTracking() + .Where(mg => modelIds.Contains(mg.ModelId)) + .Select(mg => mg.Id) + .ToListAsync(); + var carIds = await context.Cars + .AsNoTracking() + .Where(c => generationIds.Contains(c.ModelGenerationId)) .Select(c => c.Id) - .ToHashSet(); - var clientIdsWithTargetCar = rents - .Where(r => validCarIds.Contains(r.CarId)) + .ToListAsync(); + var clientIds = await context.Rents + .AsNoTracking() + .Where(r => carIds.Contains(r.CarId)) .Select(r => r.ClientId) .Distinct() - .ToHashSet(); - - return clients - .Where(c => clientIdsWithTargetCar.Contains(c.Id)) - .OrderBy(c => c.LastName) - .ThenBy(c => c.FirstName) - .Select(c => mapper.Map(c)) - .ToList(); + .ToListAsync(); + var clients = await context.Clients + .AsNoTracking() + .Where(cl => clientIds.Contains(cl.Id)) + .OrderBy(cl => cl.LastName) + .ThenBy(cl => cl.FirstName) + .ToListAsync(); + + return clients.Adapt>(); } /// @@ -71,42 +60,47 @@ public async Task> ReadClientsByModelName(string modelName) /// A list of cars that were in rent at the specified time public async Task> ReadCarsInRent(DateTime atTime) { - var allRents = await rentRepository.ReadAll(); - var activeRents = allRents - .Where(r => r.StartDateTime <= atTime && - atTime < r.StartDateTime.AddHours(r.Duration)) + var activeRents = await context.Rents + .AsNoTracking() + .Where(r => r.StartDateTime <= atTime) + .ToListAsync(); + var filteredRents = activeRents + .Where(r => r.StartDateTime.AddHours(r.Duration) > atTime) .ToList(); - - if (activeRents.Count == 0) return []; - - var allCars = await carRepository.ReadAll(); - var allGens = await carModelGenerationRepository.ReadAll(); - var allModels = await carModelRepository.ReadAll(); - - var carsDict = allCars.ToDictionary(c => c.Id); - var gensDict = allGens.ToDictionary(g => g.Id); - var modelsDict = allModels.ToDictionary(m => m.Id); - - return activeRents - .Select(r => - { - var car = carsDict.GetValueOrDefault(r.CarId); - if (car is null) return null; - - var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); - var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; - - return new CarInRentDto( - car.Id, - model?.Name ?? "Unknown Model", - car.NumberPlate, - r.StartDateTime, - (int)r.Duration - ); - }) - .Where(x => x is not null) - .OrderBy(x => x!.NumberPlate) - .ToList()!; + if (!filteredRents.Any()) return new List(); + var carIds = filteredRents.Select(r => r.CarId).Distinct().ToList(); + var cars = await context.Cars + .AsNoTracking() + .Where(c => carIds.Contains(c.Id)) + .ToListAsync(); + var generationIds = cars.Select(c => c.ModelGenerationId).Distinct().ToList(); + var generations = await context.ModelGenerations + .AsNoTracking() + .Where(mg => generationIds.Contains(mg.Id)) + .ToListAsync(); + var modelIds = generations.Select(mg => mg.ModelId).Distinct().ToList(); + var models = await context.CarModels + .AsNoTracking() + .Where(m => modelIds.Contains(m.Id)) + .ToListAsync(); + var result = filteredRents.Select(r => + { + var car = cars.First(c => c.Id == r.CarId); + var gen = generations.First(g => g.Id == car.ModelGenerationId); + var model = models.First(m => m.Id == gen.ModelId); + + return new CarInRentDto( + car.Id, + model.Name, + car.NumberPlate, + r.StartDateTime, + (int)r.Duration + ); + }) + .OrderBy(x => x.NumberPlate) + .ToList(); + + return result; } /// @@ -115,40 +109,31 @@ public async Task> ReadCarsInRent(DateTime atTime) /// A list of the 5 most frequently rented cars with their rental counts public async Task> ReadTop5MostRentedCars() { - var allRents = await rentRepository.ReadAll(); - var allCars = await carRepository.ReadAll(); - var allGens = await carModelGenerationRepository.ReadAll(); - var allModels = await carModelRepository.ReadAll(); - - var carStats = allRents - .GroupBy(r => r.CarId) - .Select(g => new { Id = g.Key, Count = g.Count() }) + var allRentCarIds = await context.Rents + .AsNoTracking() + .Select(r => r.CarId) + .ToListAsync(); + if (!allRentCarIds.Any()) return new List(); + var topStats = allRentCarIds + .GroupBy(id => id) + .Select(g => new { CarId = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count) .Take(5) .ToList(); - - var carsDict = allCars.ToDictionary(c => c.Id); - var gensDict = allGens.ToDictionary(g => g.Id); - var modelsDict = allModels.ToDictionary(m => m.Id); - - return carStats - .Select(stat => - { - var car = carsDict.GetValueOrDefault(stat.Id); - if (car is null) return null; - var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); - var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; - - return new CarWithRentalCountDto( - car.Id, - model?.Name ?? "Unknown Model", - car.NumberPlate, - stat.Count - ); - }) - .Where(x => x is not null) - .OrderByDescending(x => x!.RentalCount) - .ToList()!; + var topCarIds = topStats.Select(x => x.CarId).ToList(); + var cars = await context.Cars.AsNoTracking().Where(c => topCarIds.Contains(c.Id)).ToListAsync(); + var generationIds = cars.Select(c => c.ModelGenerationId).Distinct().ToList(); + var generations = await context.ModelGenerations.AsNoTracking().Where(mg => generationIds.Contains(mg.Id)).ToListAsync(); + var modelIds = generations.Select(mg => mg.ModelId).Distinct().ToList(); + var models = await context.CarModels.AsNoTracking().Where(m => modelIds.Contains(m.Id)).ToListAsync(); + + return topStats.Select(stat => + { + var car = cars.First(c => c.Id == stat.CarId); + var gen = generations.First(g => g.Id == car.ModelGenerationId); + var model = models.First(m => m.Id == gen.ModelId); + return new CarWithRentalCountDto(car.Id, model.Name, car.NumberPlate, stat.Count); + }).ToList(); } /// @@ -157,30 +142,27 @@ public async Task> ReadTop5MostRentedCars() /// A complete list of cars and how many times each has been rented public async Task> ReadAllCarsWithRentalCount() { - var allRents = await rentRepository.ReadAll(); - var allCars = await carRepository.ReadAll(); - var allGens = await carModelGenerationRepository.ReadAll(); - var allModels = await carModelRepository.ReadAll(); - - var rentCounts = allRents.GroupBy(r => r.CarId).ToDictionary(g => g.Key, g => g.Count()); - var gensDict = allGens.ToDictionary(g => g.Id); - var modelsDict = allModels.ToDictionary(m => m.Id); - - return allCars - .Select(car => - { - var gen = gensDict.GetValueOrDefault(car.ModelGenerationId); - var model = gen != null ? modelsDict.GetValueOrDefault(gen.ModelId) : null; - - return new CarWithRentalCountDto( - car.Id, - model?.Name ?? "Unknown Model", - car.NumberPlate, - rentCounts.GetValueOrDefault(car.Id, 0) - ); - }) - .OrderBy(x => x.NumberPlate) - .ToList(); + var allRentCarIds = await context.Rents + .AsNoTracking() + .Select(r => r.CarId) + .ToListAsync(); + var rentDict = allRentCarIds + .GroupBy(id => id) + .ToDictionary(g => g.Key, g => g.Count()); + var cars = await context.Cars.AsNoTracking().ToListAsync(); + var generations = await context.ModelGenerations.AsNoTracking().ToListAsync(); + var models = await context.CarModels.AsNoTracking().ToListAsync(); + + return cars.Select(car => + { + var gen = generations.First(g => g.Id == car.ModelGenerationId); + var model = models.First(m => m.Id == gen.ModelId); + rentDict.TryGetValue(car.Id, out var count); + + return new CarWithRentalCountDto(car.Id, model.Name, car.NumberPlate, count); + }) + .OrderBy(x => x.NumberPlate) + .ToList(); } /// @@ -189,47 +171,49 @@ public async Task> ReadAllCarsWithRentalCount() /// A list of the 5 highest-paying clients with their total spent amounts public async Task> ReadTop5ClientsByTotalAmount() { - var allRents = await rentRepository.ReadAll(); - var allCars = await carRepository.ReadAll(); - var allGens = await carModelGenerationRepository.ReadAll(); - var allClients = await clientRepository.ReadAll(); - - var carsDict = allCars.ToDictionary(c => c.Id); - var gensDict = allGens.ToDictionary(g => g.Id); - var clientsDict = allClients.ToDictionary(c => c.Id); - - var topStats = allRents + var rents = await context.Rents.AsNoTracking().ToListAsync(); + var cars = await context.Cars.AsNoTracking().ToListAsync(); + var generations = await context.ModelGenerations.AsNoTracking().ToListAsync(); + var clientStats = rents .GroupBy(r => r.ClientId) .Select(g => { - var total = g.Sum(r => + var totalAmount = g.Sum(r => { - var car = carsDict.GetValueOrDefault(r.CarId); - var gen = car != null ? gensDict.GetValueOrDefault(car.ModelGenerationId) : null; - return (decimal)r.Duration * (gen?.HourCost ?? 0m); + var car = cars.FirstOrDefault(c => c.Id == r.CarId); + var gen = generations.FirstOrDefault(gn => gn.Id == car?.ModelGenerationId); + return (decimal)r.Duration * (gen?.HourCost ?? 0); }); - return new { ClientId = g.Key, Amount = total, Count = g.Count() }; + + return new + { + ClientId = g.Key, + Amount = totalAmount, + Count = g.Count() + }; }) .OrderByDescending(x => x.Amount) .Take(5) .ToList(); - - return topStats - .Select(s => - { - var client = clientsDict.GetValueOrDefault(s.ClientId); - if (client is null) return null; - - return new ClientWithTotalAmountDto( - client.Id, - client.FirstName, - client.LastName, - client.Patronymic, - s.Amount, - s.Count - ); - }) - .Where(x => x is not null) - .ToList()!; + if (!clientStats.Any()) return new List(); + var topClientIds = clientStats.Select(x => x.ClientId).ToList(); + var clients = await context.Clients + .AsNoTracking() + .Where(c => topClientIds.Contains(c.Id)) + .ToListAsync(); + var result = clientStats.Select(stat => + { + var client = clients.First(c => c.Id == stat.ClientId); + return new ClientWithTotalAmountDto( + client.Id, + client.FirstName, + client.LastName, + client.Patronymic, + stat.Amount, + stat.Count + ); + }).ToList(); + + return result; } } diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs index f7500dd42..21c285b7f 100644 --- a/CarRental.Application/Services/CarModelGenerationService.cs +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.CarModelGeneration; using CarRental.Application.Interfaces; using CarRental.Domain.Interfaces; @@ -11,11 +11,9 @@ namespace CarRental.Application.Services; /// /// The repository for model generation entities /// The repository for car model entities -/// The AutoMapper instance for DTO conversion public class CarModelGenerationService( IBaseRepository repository, - IBaseRepository modelRepository, - IMapper mapper) + IBaseRepository modelRepository) : IApplicationService { /// @@ -26,15 +24,14 @@ public class CarModelGenerationService( /// Thrown if the associated CarModel ID is invalid public async Task Create(CarModelGenerationCreateUpdateDto dto) { - var entity = mapper.Map(dto); - var model = await modelRepository.Read(dto.ModelId); - if (model is null) - throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); + var entity = dto.Adapt(); + var model = await modelRepository.Read(dto.ModelId) + ?? throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); entity.Model = model; entity.ModelId = model.Id; var id = await repository.Create(entity); entity.Id = id; - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -47,7 +44,7 @@ public async Task Create(CarModelGenerationCreateUpdateDt { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModelGeneration with Id {id} not found."); - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -64,7 +61,7 @@ public async Task> ReadAll() generation.Model = await modelRepository.Read(generation.ModelId); } } - return mapper.Map>(entities); + return entities.Adapt>(); } /// @@ -77,12 +74,10 @@ public async Task> ReadAll() public async Task Update(CarModelGenerationCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) - return false; - mapper.Map(dto, existing); - var model = await modelRepository.Read(dto.ModelId); - if (model is null) - throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); + if (existing is null) return false; + dto.Adapt(existing); + var model = await modelRepository.Read(dto.ModelId) + ?? throw new KeyNotFoundException($"CarModel with Id {dto.ModelId} not found."); existing.Model = model; existing.ModelId = model.Id; return await repository.Update(existing, id); diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs index ed06b1e05..e0f20c2cb 100644 --- a/CarRental.Application/Services/CarModelService.cs +++ b/CarRental.Application/Services/CarModelService.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.CarModel; using CarRental.Application.Interfaces; using CarRental.Domain.Interfaces; @@ -10,10 +10,8 @@ namespace CarRental.Application.Services; /// Service for managing car model business logic and DTO mapping /// /// The car model data repository. -/// The AutoMapper instance for entity-DTO transformations public class CarModelService( - IBaseRepository repository, - IMapper mapper) + IBaseRepository repository) : IApplicationService { /// @@ -23,10 +21,10 @@ public class CarModelService( /// The newly created car model DTO. public async Task Create(CarModelCreateUpdateDto dto) { - var entity = mapper.Map(dto); + var entity = dto.Adapt(); var id = await repository.Create(entity); entity.Id = id; - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -39,7 +37,7 @@ public async Task Create(CarModelCreateUpdateDto dto) { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"CarModel with Id {id} not found."); - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -49,7 +47,7 @@ public async Task Create(CarModelCreateUpdateDto dto) public async Task> ReadAll() { var entities = await repository.ReadAll(); - return mapper.Map>(entities); + return entities.Adapt>(); } /// @@ -61,9 +59,9 @@ public async Task> ReadAll() public async Task Update(CarModelCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) - return false; - mapper.Map(dto, existing); + if (existing is null) return false; + + dto.Adapt(existing); return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index b68f256e2..2d7d3aa5c 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.Car; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; @@ -12,11 +12,9 @@ namespace CarRental.Application.Services; /// /// The car data repository /// The car model generation data repository -/// The AutoMapper instance for object mapping public class CarService( IBaseRepository repository, - IBaseRepository generationRepository, - IMapper mapper) + IBaseRepository generationRepository) : IApplicationService { /// @@ -26,7 +24,7 @@ public class CarService( public async Task> ReadAll() { var entities = await repository.ReadAll(); - return mapper.Map>(entities); + return entities.Adapt>(); } /// @@ -39,7 +37,7 @@ public async Task> ReadAll() { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Car with Id {id} not found."); - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -51,14 +49,13 @@ public async Task> ReadAll() /// Thrown if the car cannot be retrieved after creation public async Task Create(CarCreateUpdateDto dto) { - var generation = await generationRepository.Read(dto.ModelGenerationId); - if (generation is null) - throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); - var entity = mapper.Map(dto); + var generation = await generationRepository.Read(dto.ModelGenerationId) + ?? throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); + var entity = dto.Adapt(); var id = await repository.Create(entity); var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created car was not found."); - return mapper.Map(savedEntity); + return savedEntity.Adapt(); } /// @@ -71,15 +68,13 @@ public async Task Create(CarCreateUpdateDto dto) public async Task Update(CarCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) - return false; + if (existing is null) return false; if (dto.ModelGenerationId != existing.ModelGenerationId) { - var generation = await generationRepository.Read(dto.ModelGenerationId); - if (generation is null) - throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); + var generation = await generationRepository.Read(dto.ModelGenerationId) + ?? throw new KeyNotFoundException($"ModelGeneration with Id {dto.ModelGenerationId} not found."); } - mapper.Map(dto, existing); + dto.Adapt(existing); return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index a57fdd3c7..3bdc5d9cb 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.Client; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; @@ -10,10 +10,8 @@ namespace CarRental.Application.Services; /// Service for managing client-related business logic and DTO mapping. /// /// The client data repository. -/// The AutoMapper instance for entity-DTO transformations. public class ClientService( - IBaseRepository repository, - IMapper mapper) + IBaseRepository repository) : IApplicationService { /// @@ -23,7 +21,7 @@ public class ClientService( public async Task> ReadAll() { var entities = await repository.ReadAll(); - return mapper.Map>(entities); + return entities.Adapt>(); } /// @@ -36,7 +34,7 @@ public async Task> ReadAll() { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Client with Id {id} not found."); - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -47,11 +45,11 @@ public async Task> ReadAll() /// Thrown if the client cannot be retrieved after creation public async Task Create(ClientCreateUpdateDto dto) { - var entity = mapper.Map(dto); + var entity = dto.Adapt(); var id = await repository.Create(entity); var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created client was not found."); - return mapper.Map(savedEntity); + return savedEntity.Adapt(); } /// @@ -63,9 +61,8 @@ public async Task Create(ClientCreateUpdateDto dto) public async Task Update(ClientCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) - return false; - mapper.Map(dto, existing); + if (existing is null) return false; + dto.Adapt(existing); return await repository.Update(existing, id); } diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 83c81c5fa..5e31035e6 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using Mapster; using CarRental.Application.Contracts.Rent; using CarRental.Application.Interfaces; using CarRental.Domain.DataModels; @@ -12,12 +12,10 @@ namespace CarRental.Application.Services; /// The rent data repository /// The car data repository /// The client data repository -/// The AutoMapper instance for DTO mapping public class RentService( IBaseRepository repository, IBaseRepository carRepository, - IBaseRepository clientRepository, - IMapper mapper) + IBaseRepository clientRepository) : IApplicationService { /// @@ -27,7 +25,7 @@ public class RentService( public async Task> ReadAll() { var rents = await repository.ReadAll(); - return mapper.Map>(rents); + return rents.Adapt>(); } /// @@ -40,8 +38,7 @@ public async Task> ReadAll() { var entity = await repository.Read(id) ?? throw new KeyNotFoundException($"Rent with Id {id} not found."); - - return mapper.Map(entity); + return entity.Adapt(); } /// @@ -55,13 +52,13 @@ public async Task Create(RentCreateUpdateDto dto) ?? throw new KeyNotFoundException($"Car with Id {dto.CarId} not found."); var client = await clientRepository.Read(dto.ClientId) ?? throw new KeyNotFoundException($"Client with Id {dto.ClientId} not found."); - var entity = mapper.Map(dto); + var entity = dto.Adapt(); entity.Car = car; entity.Client = client; var id = await repository.Create(entity); var savedEntity = await repository.Read(id) ?? throw new InvalidOperationException("Created rent was not found."); - return mapper.Map(savedEntity); + return savedEntity.Adapt(); } /// @@ -73,9 +70,8 @@ public async Task Create(RentCreateUpdateDto dto) public async Task Update(RentCreateUpdateDto dto, Guid id) { var existing = await repository.Read(id); - if (existing is null) - return false; - mapper.Map(dto, existing); + if (existing is null) return false; + dto.Adapt(existing); if (dto.CarId != existing.Car?.Id) { var car = await carRepository.Read(dto.CarId) diff --git a/CarRental.Infrastructure/DbInitializer.cs b/CarRental.Infrastructure/DbInitializer.cs index 238b8e8d3..5925fa8d2 100644 --- a/CarRental.Infrastructure/DbInitializer.cs +++ b/CarRental.Infrastructure/DbInitializer.cs @@ -1,5 +1,6 @@ using CarRental.Domain.DataSeed; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace CarRental.Infrastructure; @@ -14,26 +15,57 @@ public static class DbInitializer /// Asynchronously seeds the database with initial car rental data if the CarModels table is empty /// /// The database context instance used to persist the seed data + /// The logger instance for capturing diagnostic information /// A task representing the asynchronous seeding operation - public static async Task SeedData(CarRentalDbContext context) + public static async Task SeedData(CarRentalDbContext context, ILogger logger) { - if (await context.CarModels.AnyAsync()) return; + try + { + if (await context.CarModels.AnyAsync()) + { + logger.LogInformation("The database if already filled"); + return; + } + } + catch (Exception ex) + { + logger.LogCritical(ex, "Connection with database is not establised"); + } - var data = new DataSeed(); + using var transaction = await context.Database.BeginTransactionAsync(); + try + { + var data = new DataSeed(); + logger.LogInformation("The process of database's filling is starting..."); - await context.CarModels.AddRangeAsync(data.Models); - await context.SaveChangesAsync(); + await context.CarModels.AddRangeAsync(data.Models); + await context.SaveChangesAsync(); + logger.LogInformation("Car models were successfully uploaded"); - await context.ModelGenerations.AddRangeAsync(data.Generations); - await context.SaveChangesAsync(); + await context.ModelGenerations.AddRangeAsync(data.Generations); + await context.SaveChangesAsync(); + logger.LogInformation("Model's generations were successfully uploaded"); - await context.Cars.AddRangeAsync(data.Cars); - await context.SaveChangesAsync(); + await context.Cars.AddRangeAsync(data.Cars); + await context.SaveChangesAsync(); + logger.LogInformation("Cars were successfully uploaded"); - await context.Clients.AddRangeAsync(data.Clients); - await context.SaveChangesAsync(); + await context.Clients.AddRangeAsync(data.Clients); + await context.SaveChangesAsync(); + logger.LogInformation("Clients were successfully uploaded"); - await context.Rents.AddRangeAsync(data.Rents); - await context.SaveChangesAsync(); + await context.Rents.AddRangeAsync(data.Rents); + await context.SaveChangesAsync(); + logger.LogInformation("Rents were successfully uploaded"); + + await transaction.CommitAsync(); + logger.LogInformation("Database was successfully initialized!"); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + logger.LogError(ex, "The problem with filling database. Check logs for more information"); + throw; + } } } \ No newline at end of file diff --git a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs index 3db190c99..0b6fea4ec 100644 --- a/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs +++ b/CarRental.Infrastructure/Repository/DbCarModelGenerationRepository.cs @@ -14,13 +14,19 @@ public class DbCarModelGenerationRepository(CarRentalDbContext context) : IBaseR /// Retrieves all model generations with their associated car models /// /// A list of all model generation entities - public async Task> ReadAll() => - (await context.ModelGenerations.ToListAsync()) - .Select(g => + public async Task> ReadAll() + { + var generations = await context.ModelGenerations.ToListAsync(); + var modelIds = generations.Select(g => g.ModelId).Distinct().ToList(); + var models = await context.CarModels + .Where(m => modelIds.Contains(m.Id)) + .ToListAsync(); + foreach (var generation in generations) { - g.Model = context.CarModels.FirstOrDefault(m => m.Id == g.ModelId); - return g; - }).ToList(); + generation.Model = models.FirstOrDefault(m => m.Id == generation.ModelId); + } + return generations; + } /// /// Finds a specific model generation by id and loads its associated car model @@ -29,11 +35,10 @@ public async Task> ReadAll() => /// The generation entity if found; otherwise, null public async Task Read(Guid id) { - var list = await context.ModelGenerations.ToListAsync(); - var entity = list.FirstOrDefault(x => x.Id == id); + var entity = await context.ModelGenerations.FirstOrDefaultAsync(x => x.Id == id); if (entity != null) { - entity.Model = context.CarModels.FirstOrDefault(m => m.Id == entity.ModelId); + entity.Model = await context.CarModels.FirstOrDefaultAsync(m => m.Id == entity.ModelId); } return entity; } From 4d2ef492099f8d308c0c99d8dbf4309dd4b8af8c Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 12 Feb 2026 18:46:25 +0400 Subject: [PATCH 31/37] changed summary for Mapster config, changed response codes for analytics tasks --- CarRental.Api/Controllers/AnalyticsController.cs | 15 +++++---------- CarRental.Application/CarRentalMapsterConfig.cs | 3 ++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index bf4c8f7ac..0f278652d 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -18,7 +18,6 @@ public class AnalyticsController(IAnalyticsService analyticsService, ILoggerThe name of the car model to filter by [HttpGet("clients-by-model")] [ProducesResponseType(200)] - [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task>> GetClientsByModel([FromQuery] string modelName) { @@ -27,7 +26,7 @@ public async Task>> GetClientsByModel([FromQuery] s { var result = await analyticsService.ReadClientsByModelName(modelName); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetClientsByModel), GetType().Name); - return result != null ? Ok(result) : NoContent(); + return Ok(result); } catch (Exception ex) { @@ -42,7 +41,6 @@ public async Task>> GetClientsByModel([FromQuery] s /// The point in time to check for active rentals [HttpGet("cars-in-rent")] [ProducesResponseType(200)] - [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task>> GetCarsInRent([FromQuery] DateTime atTime) { @@ -51,7 +49,7 @@ public async Task>> GetCarsInRent([FromQuery] Da { var result = await analyticsService.ReadCarsInRent(atTime); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetCarsInRent), GetType().Name); - return result != null ? Ok(result) : NoContent(); + return Ok(result); } catch (Exception ex) { @@ -65,7 +63,6 @@ public async Task>> GetCarsInRent([FromQuery] Da /// [HttpGet("top-5-rented-cars")] [ProducesResponseType(200)] - [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task>> GetTop5Cars() { @@ -74,7 +71,7 @@ public async Task>> GetTop5Cars() { var result = await analyticsService.ReadTop5MostRentedCars(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Cars), GetType().Name); - return result != null ? Ok(result) : NoContent(); + return Ok(result); } catch (Exception ex) { @@ -88,7 +85,6 @@ public async Task>> GetTop5Cars() /// [HttpGet("all-cars-with-rental-count")] [ProducesResponseType(200)] - [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task>> GetAllCarsWithCount() { @@ -97,7 +93,7 @@ public async Task>> GetAllCarsWithCount { var result = await analyticsService.ReadAllCarsWithRentalCount(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAllCarsWithCount), GetType().Name); - return result != null ? Ok(result) : NoContent(); + return Ok(result); } catch (Exception ex) { @@ -111,7 +107,6 @@ public async Task>> GetAllCarsWithCount /// [HttpGet("top-5-clients-by-money")] [ProducesResponseType(200)] - [ProducesResponseType(204)] [ProducesResponseType(500)] public async Task>> GetTop5Clients() { @@ -120,7 +115,7 @@ public async Task>> GetTop5Clients() { var result = await analyticsService.ReadTop5ClientsByTotalAmount(); logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5Clients), GetType().Name); - return result != null ? Ok(result) : NoContent(); + return Ok(result); } catch (Exception ex) { diff --git a/CarRental.Application/CarRentalMapsterConfig.cs b/CarRental.Application/CarRentalMapsterConfig.cs index 92291a28c..f07f700ad 100644 --- a/CarRental.Application/CarRentalMapsterConfig.cs +++ b/CarRental.Application/CarRentalMapsterConfig.cs @@ -10,7 +10,8 @@ namespace CarRental.Application; /// -/// AutoMapper configuration profile for mapping between Domain entities and Application DTOs +/// This code defines a Mapster configuration class +/// used to automate object-to-object mapping within a Car Rental application /// public class CarRentalMapsterConfig : IRegister { From d64858dd96667da32659c3f2ae1b3d0f92e21c15 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Sun, 15 Feb 2026 22:16:56 +0400 Subject: [PATCH 32/37] added a service for generating fake rents --- CarRental.AppHost/CarRental.AppHost.csproj | 1 + CarRental.AppHost/Program.cs | 2 + CarRental.Domain/CarRental.Domain.csproj | 3 + CarRental.Domain/DataSeed/DataSeed.cs | 80 +++++++++---------- .../CarRental.Generator.csproj | 19 +++++ .../Generation/GeneratorOptions.cs | 7 ++ .../Generation/RentGeneratorService.cs | 29 +++++++ CarRental.Generator/Program.cs | 33 ++++++++ .../Properties/launchSettings.json | 38 +++++++++ .../appsettings.Development.json | 9 +++ CarRental.Generator/appsettings.json | 55 +++++++++++++ CarRental.sln | 14 ++++ 12 files changed, 250 insertions(+), 40 deletions(-) create mode 100644 CarRental.Generator/CarRental.Generator.csproj create mode 100644 CarRental.Generator/Generation/GeneratorOptions.cs create mode 100644 CarRental.Generator/Generation/RentGeneratorService.cs create mode 100644 CarRental.Generator/Program.cs create mode 100644 CarRental.Generator/Properties/launchSettings.json create mode 100644 CarRental.Generator/appsettings.Development.json create mode 100644 CarRental.Generator/appsettings.json diff --git a/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental.AppHost/CarRental.AppHost.csproj index e310f8e4b..adadd8abe 100644 --- a/CarRental.AppHost/CarRental.AppHost.csproj +++ b/CarRental.AppHost/CarRental.AppHost.csproj @@ -17,6 +17,7 @@ + diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs index b2d0b91c6..201674b8d 100644 --- a/CarRental.AppHost/Program.cs +++ b/CarRental.AppHost/Program.cs @@ -7,4 +7,6 @@ .WithReference(mongodb, "CarRentalDb") .WaitFor(mongodb); +builder.AddProject("carrental-generator"); + builder.Build().Run(); diff --git a/CarRental.Domain/CarRental.Domain.csproj b/CarRental.Domain/CarRental.Domain.csproj index 5e7c51be3..bd71603f5 100644 --- a/CarRental.Domain/CarRental.Domain.csproj +++ b/CarRental.Domain/CarRental.Domain.csproj @@ -7,4 +7,7 @@ True + + + diff --git a/CarRental.Domain/DataSeed/DataSeed.cs b/CarRental.Domain/DataSeed/DataSeed.cs index 9e8f06e51..4ab5d7485 100644 --- a/CarRental.Domain/DataSeed/DataSeed.cs +++ b/CarRental.Domain/DataSeed/DataSeed.cs @@ -89,50 +89,50 @@ public DataSeed() Cars = new List { - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[5].Id, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[14].Id, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[0].Id, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[19].Id, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[6].Id, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[16].Id, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[7].Id, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[13].Id, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[3].Id, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[18].Id, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[10].Id, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[11].Id, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[8].Id, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[9].Id, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[1].Id, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[15].Id, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[2].Id, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[17].Id, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[4].Id, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" }, - new() { Id = Guid.NewGuid(), ModelGenerationId = Generations[12].Id, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" } + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440001"), ModelGenerationId = Generations[5].Id, ModelGeneration = Generations[5], NumberPlate = "T890NO96", Colour = "Gray" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440002"), ModelGenerationId = Generations[14].Id, ModelGeneration = Generations[14], NumberPlate = "A123BC77", Colour = "Black" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440003"), ModelGenerationId = Generations[0].Id, ModelGeneration = Generations[0], NumberPlate = "M789ZA89", Colour = "Yellow" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440004"), ModelGenerationId = Generations[19].Id, ModelGeneration = Generations[19], NumberPlate = "D012HI80", Colour = "Blue" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440005"), ModelGenerationId = Generations[6].Id, ModelGeneration = Generations[6], NumberPlate = "E345JK81", Colour = "Red" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440006"), ModelGenerationId = Generations[16].Id, ModelGeneration = Generations[16], NumberPlate = "F678LM82", Colour = "Gray" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440007"), ModelGenerationId = Generations[7].Id, ModelGeneration = Generations[7], NumberPlate = "G901NO83", Colour = "Green" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440008"), ModelGenerationId = Generations[13].Id, ModelGeneration = Generations[13], NumberPlate = "H234PQ84", Colour = "Black" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440009"), ModelGenerationId = Generations[3].Id, ModelGeneration = Generations[3], NumberPlate = "I567RS85", Colour = "White" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440010"), ModelGenerationId = Generations[18].Id, ModelGeneration = Generations[18], NumberPlate = "J890TU86", Colour = "Silver" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440011"), ModelGenerationId = Generations[10].Id, ModelGeneration = Generations[10], NumberPlate = "K123VW87", Colour = "Blue" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440012"), ModelGenerationId = Generations[11].Id, ModelGeneration = Generations[11], NumberPlate = "L456XY88", Colour = "Red" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440013"), ModelGenerationId = Generations[8].Id, ModelGeneration = Generations[8], NumberPlate = "R234JK94", Colour = "Blue" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440014"), ModelGenerationId = Generations[9].Id, ModelGeneration = Generations[9], NumberPlate = "N012BC90", Colour = "White" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440015"), ModelGenerationId = Generations[1].Id, ModelGeneration = Generations[1], NumberPlate = "Q901HI93", Colour = "Red" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440016"), ModelGenerationId = Generations[15].Id, ModelGeneration = Generations[15], NumberPlate = "P678FG92", Colour = "Silver" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440017"), ModelGenerationId = Generations[2].Id, ModelGeneration = Generations[2], NumberPlate = "O345DE91", Colour = "Black" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440018"), ModelGenerationId = Generations[17].Id, ModelGeneration = Generations[17], NumberPlate = "S567LM95", Colour = "Green" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440019"), ModelGenerationId = Generations[4].Id, ModelGeneration = Generations[4], NumberPlate = "C789FG79", Colour = "Silver" }, + new() { Id = Guid.Parse("550e8400-e29b-41d4-a716-446655440020"), ModelGenerationId = Generations[12].Id, ModelGeneration = Generations[12], NumberPlate = "B456DE78", Colour = "White" } }; Clients = new List { - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, - new() { Id = Guid.NewGuid(), DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440001"), DriverLicenseId = "DL990011223", LastName = "Belov", FirstName = "Roman", Patronymic = "Evgenievich", BirthDate = new DateOnly(1984, 9, 13) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440002"), DriverLicenseId = "DL112233445", LastName = "Lebedev", FirstName = "Artem", Patronymic = "Olegovich", BirthDate = new DateOnly(1994, 10, 21) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440003"), DriverLicenseId = "DL001122334", LastName = "Efimova", FirstName = "Daria", Patronymic = "Mikhailovna", BirthDate = new DateOnly(1999, 6, 22) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440004"), DriverLicenseId = "DL445566778", LastName = "Vinogradova", FirstName = "Polina", Patronymic = "Sergeevna", BirthDate = new DateOnly(1996, 12, 19) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440005"), DriverLicenseId = "DL567890123", LastName = "Smirnov", FirstName = "Dmitry", Patronymic = "Alexandrovich", BirthDate = new DateOnly(1985, 7, 12) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440006"), DriverLicenseId = "DL234567890", LastName = "Petrova", FirstName = "Maria", Patronymic = "Dmitrievna", BirthDate = new DateOnly(1988, 11, 3) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440007"), DriverLicenseId = "DL789012345", LastName = "Vasiliev", FirstName = "Sergey", Patronymic = "Nikolaevich", BirthDate = new DateOnly(1980, 12, 5) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440008"), DriverLicenseId = "DL890123456", LastName = "Fedorov", FirstName = "Andrey", Patronymic = null, BirthDate = new DateOnly(1993, 9, 27) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440009"), DriverLicenseId = "DL334455667", LastName = "Orlov", FirstName = "Maxim", Patronymic = "Igorevich", BirthDate = new DateOnly(1986, 8, 3) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440010"), DriverLicenseId = "DL012345678", LastName = "Nikolaev", FirstName = "Nikolay", Patronymic = "Pavlovich", BirthDate = new DateOnly(1987, 6, 9) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440011"), DriverLicenseId = "DL678901234", LastName = "Popova", FirstName = "Anna", Patronymic = "Ivanovna", BirthDate = new DateOnly(1997, 4, 18) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440012"), DriverLicenseId = "DL223344556", LastName = "Sokolova", FirstName = "Tatiana", Patronymic = null, BirthDate = new DateOnly(1989, 2, 11) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440013"), DriverLicenseId = "DL901234567", LastName = "Morozova", FirstName = "Olga", Patronymic = "Viktorovna", BirthDate = new DateOnly(1991, 3, 14) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440014"), DriverLicenseId = "DL123456789", LastName = "Ivanov", FirstName = "Alexey", Patronymic = "Sergeevich", BirthDate = new DateOnly(1990, 5, 15) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440015"), DriverLicenseId = "DL556677889", LastName = "Mikhailov", FirstName = "Kirill", Patronymic = null, BirthDate = new DateOnly(1990, 7, 25) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440016"), DriverLicenseId = "DL667788990", LastName = "Romanova", FirstName = "Victoria", Patronymic = "Andreevna", BirthDate = new DateOnly(1983, 11, 8) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440017"), DriverLicenseId = "DL778899001", LastName = "Karpov", FirstName = "Igor", Patronymic = "Valentinovich", BirthDate = new DateOnly(1982, 4, 17) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440018"), DriverLicenseId = "DL889900112", LastName = "Timofeeva", FirstName = "Natalia", Patronymic = null, BirthDate = new DateOnly(1998, 1, 29) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440019"), DriverLicenseId = "DL345678901", LastName = "Sidorov", FirstName = "Ivan", Patronymic = "Petrovich", BirthDate = new DateOnly(1995, 8, 22) }, + new() { Id = Guid.Parse("c11e4400-e29b-41d4-a716-446655440020"), DriverLicenseId = "DL456789012", LastName = "Kuznetsova", FirstName = "Elena", Patronymic = null, BirthDate = new DateOnly(1992, 1, 30) } }; var baseTime = new DateTime(2025, 1, 1, 10, 0, 0, DateTimeKind.Utc); diff --git a/CarRental.Generator/CarRental.Generator.csproj b/CarRental.Generator/CarRental.Generator.csproj new file mode 100644 index 000000000..5cc1e3e76 --- /dev/null +++ b/CarRental.Generator/CarRental.Generator.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/CarRental.Generator/Generation/GeneratorOptions.cs b/CarRental.Generator/Generation/GeneratorOptions.cs new file mode 100644 index 000000000..a035c0ef6 --- /dev/null +++ b/CarRental.Generator/Generation/GeneratorOptions.cs @@ -0,0 +1,7 @@ +namespace CarRental.Generator.Generation; + +public class GeneratorOptions +{ + public List CarIds { get; set; } = new(); + public List ClientIds { get; set; } = new(); +} diff --git a/CarRental.Generator/Generation/RentGeneratorService.cs b/CarRental.Generator/Generation/RentGeneratorService.cs new file mode 100644 index 000000000..2357efac9 --- /dev/null +++ b/CarRental.Generator/Generation/RentGeneratorService.cs @@ -0,0 +1,29 @@ +using CarRental.Application.Contracts.Rent; +using Microsoft.Extensions.Options; +using Bogus; +using CarRental.Generator.Generation; + +namespace CarRental.Generator; + +public static class RentGeneratorService +{ + private readonly GeneratorOptions _options; + + public RentGeneratorService(IOptions options) + { + _options = options.Value; + } + + public static IList GenerateContract(int count) + { + var generatedRents = new Faker().CustomInstantiator(f => new RentCreateUpdateDto( + StartDateTime: f.Date.Soon(1), + Duration: f.Random.Double(1, 100), + CarId: f.PickRandom(_options.CarIds), + ClientId: f.PickRandom(_options.ClientIds) + ) + ); + + return generatedRents.Generate(count); + } +} diff --git a/CarRental.Generator/Program.cs b/CarRental.Generator/Program.cs new file mode 100644 index 000000000..a620b52e3 --- /dev/null +++ b/CarRental.Generator/Program.cs @@ -0,0 +1,33 @@ +namespace CarRental.Generator.Generation; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.Configure(builder.Configuration.GetSection("Generator")); + +// Add services to the container. +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthorization(); + +app.MapRazorPages(); + +app.Run(); diff --git a/CarRental.Generator/Properties/launchSettings.json b/CarRental.Generator/Properties/launchSettings.json new file mode 100644 index 000000000..8df699d1b --- /dev/null +++ b/CarRental.Generator/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52170", + "sslPort": 44384 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5112", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7160;http://localhost:5112", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CarRental.Generator/appsettings.Development.json b/CarRental.Generator/appsettings.Development.json new file mode 100644 index 000000000..770d3e931 --- /dev/null +++ b/CarRental.Generator/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CarRental.Generator/appsettings.json b/CarRental.Generator/appsettings.json new file mode 100644 index 000000000..4a1cde249 --- /dev/null +++ b/CarRental.Generator/appsettings.json @@ -0,0 +1,55 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Generator": { + "CarIds": [ + "550e8400-e29b-41d4-a716-446655440001", + "550e8400-e29b-41d4-a716-446655440002", + "550e8400-e29b-41d4-a716-446655440003", + "550e8400-e29b-41d4-a716-446655440004", + "550e8400-e29b-41d4-a716-446655440005", + "550e8400-e29b-41d4-a716-446655440006", + "550e8400-e29b-41d4-a716-446655440007", + "550e8400-e29b-41d4-a716-446655440008", + "550e8400-e29b-41d4-a716-446655440009", + "550e8400-e29b-41d4-a716-446655440010", + "550e8400-e29b-41d4-a716-446655440011", + "550e8400-e29b-41d4-a716-446655440012", + "550e8400-e29b-41d4-a716-446655440013", + "550e8400-e29b-41d4-a716-446655440014", + "550e8400-e29b-41d4-a716-446655440015", + "550e8400-e29b-41d4-a716-446655440016", + "550e8400-e29b-41d4-a716-446655440017", + "550e8400-e29b-41d4-a716-446655440018", + "550e8400-e29b-41d4-a716-446655440019", + "550e8400-e29b-41d4-a716-446655440020" + ], + "ClientIds": [ + "c11e4400-e29b-41d4-a716-446655440001", + "c11e4400-e29b-41d4-a716-446655440002", + "c11e4400-e29b-41d4-a716-446655440003", + "c11e4400-e29b-41d4-a716-446655440004", + "c11e4400-e29b-41d4-a716-446655440005", + "c11e4400-e29b-41d4-a716-446655440006", + "c11e4400-e29b-41d4-a716-446655440007", + "c11e4400-e29b-41d4-a716-446655440008", + "c11e4400-e29b-41d4-a716-446655440009", + "c11e4400-e29b-41d4-a716-446655440010", + "c11e4400-e29b-41d4-a716-446655440011", + "c11e4400-e29b-41d4-a716-446655440012", + "c11e4400-e29b-41d4-a716-446655440013", + "c11e4400-e29b-41d4-a716-446655440014", + "c11e4400-e29b-41d4-a716-446655440015", + "c11e4400-e29b-41d4-a716-446655440016", + "c11e4400-e29b-41d4-a716-446655440017", + "c11e4400-e29b-41d4-a716-446655440018", + "c11e4400-e29b-41d4-a716-446655440019", + "c11e4400-e29b-41d4-a716-446655440020" + ] + } +} diff --git a/CarRental.sln b/CarRental.sln index 1f0664c5e..6344b38f1 100644 --- a/CarRental.sln +++ b/CarRental.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.AppHost", "CarRen EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.ServiceDefaults", "CarRental.ServiceDefaults\CarRental.ServiceDefaults.csproj", "{0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Generator", "CarRental.Generator\CarRental.Generator.csproj", "{BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +113,18 @@ Global {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x64.Build.0 = Release|Any CPU {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x86.ActiveCfg = Release|Any CPU {0DCB1E82-BC4B-4CFB-A63F-FD0A292AE748}.Release|x86.Build.0 = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|x64.Build.0 = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Debug|x86.Build.0 = Debug|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|Any CPU.Build.0 = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x64.ActiveCfg = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x64.Build.0 = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x86.ActiveCfg = Release|Any CPU + {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b3c57eff148a9f89364665b48dab868afa112692 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Tue, 17 Feb 2026 18:07:04 +0400 Subject: [PATCH 33/37] divided the CarRental.Application structure to avoid circular dependencies, added producer in the generator project and consumer in the infrastructure project --- .../Controllers/AnalyticsController.cs | 2 +- CarRental.Api/Controllers/CarControllers.cs | 2 +- .../Controllers/CarModelController.cs | 2 +- .../CarModelGenerationController.cs | 2 +- CarRental.Api/Controllers/ClientController.cs | 2 +- CarRental.Api/Controllers/RentController.cs | 2 +- CarRental.Api/Program.cs | 33 +++- CarRental.AppHost/CarRental.AppHost.csproj | 1 + CarRental.AppHost/Program.cs | 14 +- .../Analytics/CarInRentDto.cs | 0 .../Analytics/CarWithRentalCountDto.cs | 0 .../Analytics/ClientWithTotalAmountDto.cs | 0 .../Car/CarCreateUpdateDto.cs | 0 .../Car/CarDto.cs | 0 .../CarModel/CarModelCreateUpdateDto.cs | 0 .../CarModel/CarModelDto.cs | 0 .../CarModelGenerationCreateUpdateDto.cs | 0 .../CarModelGenerationDto.cs | 0 .../CarRental.Application.Contracts.csproj | 9 ++ .../Client/ClientCreateUpdateDto.cs | 0 .../Client/ClientDto.cs | 0 .../Interfaces/IAnalyticsService.cs | 2 +- .../Interfaces/IApplicationService.cs | 2 +- .../Rent/RentCreateUpdateDto.cs | 0 .../Rent/RentDto.cs | 0 .../CarRental.Application.csproj | 11 +- .../Services/AnalyticsService.cs | 2 +- .../Services/CarModelGenerationService.cs | 2 +- .../Services/CarModelService.cs | 2 +- CarRental.Application/Services/CarService.cs | 2 +- .../Services/ClientService.cs | 2 +- CarRental.Application/Services/RentService.cs | 2 +- .../CarRental.Generator.csproj | 5 +- .../Controller/GeneratorController.cs | 102 ++++++++++++ .../Generation/GeneratorOptions.cs | 14 +- .../Generation/RentGeneratorService.cs | 30 +++- CarRental.Generator/KafkaProducer.cs | 88 ++++++++++ CarRental.Generator/KafkaProducerSettings.cs | 22 +++ CarRental.Generator/Program.cs | 66 +++++--- .../Properties/launchSettings.json | 3 + .../CarRental.Infrastructure.csproj | 21 +-- CarRental.Infrastructure/Kafka/Consumer.cs | 152 ++++++++++++++++++ .../Kafka/ConsumerSettings.cs | 33 ++++ CarRental.sln | 14 ++ 44 files changed, 581 insertions(+), 65 deletions(-) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Analytics/CarInRentDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Analytics/CarWithRentalCountDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Analytics/ClientWithTotalAmountDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Car/CarCreateUpdateDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Car/CarDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/CarModel/CarModelCreateUpdateDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/CarModel/CarModelDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/CarModelGeneration/CarModelGenerationDto.cs (100%) create mode 100644 CarRental.Application.Contracts/CarRental.Application.Contracts.csproj rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Client/ClientCreateUpdateDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Client/ClientDto.cs (100%) rename {CarRental.Application => CarRental.Application.Contracts}/Interfaces/IAnalyticsService.cs (95%) rename {CarRental.Application => CarRental.Application.Contracts}/Interfaces/IApplicationService.cs (96%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Rent/RentCreateUpdateDto.cs (100%) rename {CarRental.Application/Contracts => CarRental.Application.Contracts}/Rent/RentDto.cs (100%) create mode 100644 CarRental.Generator/Controller/GeneratorController.cs create mode 100644 CarRental.Generator/KafkaProducer.cs create mode 100644 CarRental.Generator/KafkaProducerSettings.cs create mode 100644 CarRental.Infrastructure/Kafka/Consumer.cs create mode 100644 CarRental.Infrastructure/Kafka/ConsumerSettings.cs diff --git a/CarRental.Api/Controllers/AnalyticsController.cs b/CarRental.Api/Controllers/AnalyticsController.cs index 0f278652d..894cebba2 100644 --- a/CarRental.Api/Controllers/AnalyticsController.cs +++ b/CarRental.Api/Controllers/AnalyticsController.cs @@ -1,6 +1,6 @@ using CarRental.Application.Contracts.Client; using CarRental.Application.Contracts.Analytics; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Controllers/CarControllers.cs b/CarRental.Api/Controllers/CarControllers.cs index bd60b9a88..e48fc0769 100644 --- a/CarRental.Api/Controllers/CarControllers.cs +++ b/CarRental.Api/Controllers/CarControllers.cs @@ -1,5 +1,5 @@ using CarRental.Application.Contracts.Car; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Controllers/CarModelController.cs b/CarRental.Api/Controllers/CarModelController.cs index e3730e397..e82727799 100644 --- a/CarRental.Api/Controllers/CarModelController.cs +++ b/CarRental.Api/Controllers/CarModelController.cs @@ -1,5 +1,5 @@ using CarRental.Application.Contracts.CarModel; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Controllers/CarModelGenerationController.cs b/CarRental.Api/Controllers/CarModelGenerationController.cs index b40ab3ecc..3a8668890 100644 --- a/CarRental.Api/Controllers/CarModelGenerationController.cs +++ b/CarRental.Api/Controllers/CarModelGenerationController.cs @@ -1,5 +1,5 @@ using CarRental.Application.Contracts.CarModelGeneration; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Controllers/ClientController.cs b/CarRental.Api/Controllers/ClientController.cs index 58a467a4b..f78a440a7 100644 --- a/CarRental.Api/Controllers/ClientController.cs +++ b/CarRental.Api/Controllers/ClientController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Application.Contracts.Client; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Controllers/RentController.cs b/CarRental.Api/Controllers/RentController.cs index 4adf966c3..13767876d 100644 --- a/CarRental.Api/Controllers/RentController.cs +++ b/CarRental.Api/Controllers/RentController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Application.Contracts.Rent; namespace CarRental.Api.Controllers; diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index b84018356..c0b7d70d3 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -1,23 +1,25 @@ -using CarRental.Application; using CarRental.Application.Contracts.Car; using CarRental.Application.Contracts.CarModel; using CarRental.Application.Contracts.CarModelGeneration; using CarRental.Application.Contracts.Client; +using CarRental.Application.Contracts.Interfaces; using CarRental.Application.Contracts.Rent; -using CarRental.Application.Interfaces; using CarRental.Application.Services; using CarRental.Domain.DataModels; -using CarRental.Domain.Interfaces; using CarRental.Domain.DataSeed; +using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Infrastructure; +using CarRental.Infrastructure.Kafka; using CarRental.Infrastructure.Repository; using CarRental.ServiceDefaults; +using Confluent.Kafka; +using Mapster; +using MapsterMapper; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; using MongoDB.Driver; -using Mapster; using System.Reflection; -using MapsterMapper; var builder = WebApplication.CreateBuilder(args); @@ -61,6 +63,27 @@ builder.Services.AddScoped(); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("KafkaConsumer")); +builder.Services.AddSingleton>(sp => +{ + var settings = sp.GetRequiredService>().Value; + var bootstrapServers = builder.Configuration.GetConnectionString("car-rental-kafka"); + if (string.IsNullOrWhiteSpace(bootstrapServers)) + throw new InvalidOperationException("Kafka connection string 'car-rental-kafka' is missing."); + var config = new ConsumerConfig + { + BootstrapServers = bootstrapServers, + GroupId = settings.GroupId, + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = settings.AutoCommitEnabled + }; + + return new ConsumerBuilder(config).Build(); +}); + +builder.Services.AddHostedService(); + builder.Services.AddControllers() .AddJsonOptions(options => { diff --git a/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental.AppHost/CarRental.AppHost.csproj index adadd8abe..1ab807259 100644 --- a/CarRental.AppHost/CarRental.AppHost.csproj +++ b/CarRental.AppHost/CarRental.AppHost.csproj @@ -13,6 +13,7 @@ + diff --git a/CarRental.AppHost/Program.cs b/CarRental.AppHost/Program.cs index 201674b8d..21691b555 100644 --- a/CarRental.AppHost/Program.cs +++ b/CarRental.AppHost/Program.cs @@ -3,10 +3,18 @@ var mongodb = builder.AddMongoDB("mongodb"); mongodb.AddDatabase("car-rental"); +var kafka = builder.AddKafka("car-rental-kafka") + .WithKafkaUI() + .WithEnvironment("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true"); + builder.AddProject("carrental-api") .WithReference(mongodb, "CarRentalDb") - .WaitFor(mongodb); + .WithReference(kafka) + .WaitFor(mongodb) + .WaitFor(kafka); -builder.AddProject("carrental-generator"); +builder.AddProject("carrental-generator") + .WithReference(kafka) + .WaitFor(kafka); -builder.Build().Run(); +builder.Build().Run(); \ No newline at end of file diff --git a/CarRental.Application/Contracts/Analytics/CarInRentDto.cs b/CarRental.Application.Contracts/Analytics/CarInRentDto.cs similarity index 100% rename from CarRental.Application/Contracts/Analytics/CarInRentDto.cs rename to CarRental.Application.Contracts/Analytics/CarInRentDto.cs diff --git a/CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs b/CarRental.Application.Contracts/Analytics/CarWithRentalCountDto.cs similarity index 100% rename from CarRental.Application/Contracts/Analytics/CarWithRentalCountDto.cs rename to CarRental.Application.Contracts/Analytics/CarWithRentalCountDto.cs diff --git a/CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs b/CarRental.Application.Contracts/Analytics/ClientWithTotalAmountDto.cs similarity index 100% rename from CarRental.Application/Contracts/Analytics/ClientWithTotalAmountDto.cs rename to CarRental.Application.Contracts/Analytics/ClientWithTotalAmountDto.cs diff --git a/CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs b/CarRental.Application.Contracts/Car/CarCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/Car/CarCreateUpdateDto.cs rename to CarRental.Application.Contracts/Car/CarCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/Car/CarDto.cs b/CarRental.Application.Contracts/Car/CarDto.cs similarity index 100% rename from CarRental.Application/Contracts/Car/CarDto.cs rename to CarRental.Application.Contracts/Car/CarDto.cs diff --git a/CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs b/CarRental.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/CarModel/CarModelCreateUpdateDto.cs rename to CarRental.Application.Contracts/CarModel/CarModelCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/CarModel/CarModelDto.cs b/CarRental.Application.Contracts/CarModel/CarModelDto.cs similarity index 100% rename from CarRental.Application/Contracts/CarModel/CarModelDto.cs rename to CarRental.Application.Contracts/CarModel/CarModelDto.cs diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs rename to CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs b/CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs similarity index 100% rename from CarRental.Application/Contracts/CarModelGeneration/CarModelGenerationDto.cs rename to CarRental.Application.Contracts/CarModelGeneration/CarModelGenerationDto.cs diff --git a/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs b/CarRental.Application.Contracts/Client/ClientCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/Client/ClientCreateUpdateDto.cs rename to CarRental.Application.Contracts/Client/ClientCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/Client/ClientDto.cs b/CarRental.Application.Contracts/Client/ClientDto.cs similarity index 100% rename from CarRental.Application/Contracts/Client/ClientDto.cs rename to CarRental.Application.Contracts/Client/ClientDto.cs diff --git a/CarRental.Application/Interfaces/IAnalyticsService.cs b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs similarity index 95% rename from CarRental.Application/Interfaces/IAnalyticsService.cs rename to CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs index 501301c9a..0ce7e6dd4 100644 --- a/CarRental.Application/Interfaces/IAnalyticsService.cs +++ b/CarRental.Application.Contracts/Interfaces/IAnalyticsService.cs @@ -1,7 +1,7 @@ using CarRental.Application.Contracts.Client; using CarRental.Application.Contracts.Analytics; -namespace CarRental.Application.Interfaces; +namespace CarRental.Application.Contracts.Interfaces; /// /// Defines methods for business intelligence and data analysis across cars, clients, and rentals. diff --git a/CarRental.Application/Interfaces/IApplicationService.cs b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs similarity index 96% rename from CarRental.Application/Interfaces/IApplicationService.cs rename to CarRental.Application.Contracts/Interfaces/IApplicationService.cs index e51f7440f..f61d49e1c 100644 --- a/CarRental.Application/Interfaces/IApplicationService.cs +++ b/CarRental.Application.Contracts/Interfaces/IApplicationService.cs @@ -1,4 +1,4 @@ -namespace CarRental.Application.Interfaces; +namespace CarRental.Application.Contracts.Interfaces; /// /// Defines a generic contract for application services handling mapping between entities and DTOs. diff --git a/CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs b/CarRental.Application.Contracts/Rent/RentCreateUpdateDto.cs similarity index 100% rename from CarRental.Application/Contracts/Rent/RentCreateUpdateDto.cs rename to CarRental.Application.Contracts/Rent/RentCreateUpdateDto.cs diff --git a/CarRental.Application/Contracts/Rent/RentDto.cs b/CarRental.Application.Contracts/Rent/RentDto.cs similarity index 100% rename from CarRental.Application/Contracts/Rent/RentDto.cs rename to CarRental.Application.Contracts/Rent/RentDto.cs diff --git a/CarRental.Application/CarRental.Application.csproj b/CarRental.Application/CarRental.Application.csproj index 89a776170..1b16d887b 100644 --- a/CarRental.Application/CarRental.Application.csproj +++ b/CarRental.Application/CarRental.Application.csproj @@ -1,10 +1,5 @@  - - - - - @@ -17,4 +12,10 @@ True + + + + + + diff --git a/CarRental.Application/Services/AnalyticsService.cs b/CarRental.Application/Services/AnalyticsService.cs index 88865c524..4bfdf2056 100644 --- a/CarRental.Application/Services/AnalyticsService.cs +++ b/CarRental.Application/Services/AnalyticsService.cs @@ -1,7 +1,7 @@ using Mapster; using CarRental.Application.Contracts.Analytics; using CarRental.Application.Contracts.Client; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Infrastructure; using Microsoft.EntityFrameworkCore; diff --git a/CarRental.Application/Services/CarModelGenerationService.cs b/CarRental.Application/Services/CarModelGenerationService.cs index 21c285b7f..c6786ea1c 100644 --- a/CarRental.Application/Services/CarModelGenerationService.cs +++ b/CarRental.Application/Services/CarModelGenerationService.cs @@ -1,6 +1,6 @@ using Mapster; using CarRental.Application.Contracts.CarModelGeneration; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; diff --git a/CarRental.Application/Services/CarModelService.cs b/CarRental.Application/Services/CarModelService.cs index e0f20c2cb..30249e455 100644 --- a/CarRental.Application/Services/CarModelService.cs +++ b/CarRental.Application/Services/CarModelService.cs @@ -1,6 +1,6 @@ using Mapster; using CarRental.Application.Contracts.CarModel; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; diff --git a/CarRental.Application/Services/CarService.cs b/CarRental.Application/Services/CarService.cs index 2d7d3aa5c..e7a934e7c 100644 --- a/CarRental.Application/Services/CarService.cs +++ b/CarRental.Application/Services/CarService.cs @@ -1,6 +1,6 @@ using Mapster; using CarRental.Application.Contracts.Car; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; diff --git a/CarRental.Application/Services/ClientService.cs b/CarRental.Application/Services/ClientService.cs index 3bdc5d9cb..51948bd7c 100644 --- a/CarRental.Application/Services/ClientService.cs +++ b/CarRental.Application/Services/ClientService.cs @@ -1,6 +1,6 @@ using Mapster; using CarRental.Application.Contracts.Client; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; diff --git a/CarRental.Application/Services/RentService.cs b/CarRental.Application/Services/RentService.cs index 5e31035e6..a299f7b1b 100644 --- a/CarRental.Application/Services/RentService.cs +++ b/CarRental.Application/Services/RentService.cs @@ -1,6 +1,6 @@ using Mapster; using CarRental.Application.Contracts.Rent; -using CarRental.Application.Interfaces; +using CarRental.Application.Contracts.Interfaces; using CarRental.Domain.DataModels; using CarRental.Domain.Interfaces; diff --git a/CarRental.Generator/CarRental.Generator.csproj b/CarRental.Generator/CarRental.Generator.csproj index 5cc1e3e76..4d982f91f 100644 --- a/CarRental.Generator/CarRental.Generator.csproj +++ b/CarRental.Generator/CarRental.Generator.csproj @@ -9,10 +9,11 @@ + - + - + diff --git a/CarRental.Generator/Controller/GeneratorController.cs b/CarRental.Generator/Controller/GeneratorController.cs new file mode 100644 index 000000000..feebe5a8e --- /dev/null +++ b/CarRental.Generator/Controller/GeneratorController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Mvc; +using CarRental.Generator.Generation; +using CarRental.Application.Contracts.Rent; + +namespace CarRental.Generator.Controller; + +/// +/// Controller for generating and publishing rental data to Kafka +/// +/// Kafka producer for sending messages +/// Service for generating rental contracts +/// Logger instance +[ApiController] +[Route("api/[controller]")] +public class GeneratorController( + KafkaProducer producer, + RentGeneratorService rentGenerator, + ILogger logger) : ControllerBase +{ + /// + /// Generates and publishes rental contracts to Kafka in batches + /// + /// Total number of rentals to generate + /// Number of rentals per batch + /// Delay between batches in milliseconds + /// Cancellation token + /// Result with generation statistics + [HttpPost("rentals")] + public async Task GenerateRentals( + [FromQuery] int totalCount, + [FromQuery] int batchSize, + [FromQuery] int delayMs, + CancellationToken cancellationToken) + { + if (totalCount <= 0) + return BadRequest("totalCount must be greater than 0."); + + if (batchSize <= 0) + return BadRequest("batchSize must be greater than 0."); + + if (delayMs < 0) + return BadRequest("delayMs must be greater than or equal to 0."); + + logger.LogInformation("Rental generation requested. TotalCount={TotalCount}, BatchSize={BatchSize}, DelayMs={DelayMs}", + totalCount, batchSize, delayMs); + + var sent = 0; + var batches = 0; + + try + { + while (sent < totalCount && !cancellationToken.IsCancellationRequested) + { + var remaining = totalCount - sent; + var currentBatchSize = Math.Min(batchSize, remaining); + + IList batch = rentGenerator.GenerateContract(currentBatchSize); + + await producer.ProduceMany(batch, cancellationToken); + + sent += currentBatchSize; + batches++; + + if (sent < totalCount && delayMs > 0) + { + await Task.Delay(delayMs, cancellationToken); + } + } + + logger.LogInformation("Generation finished. TotalSent={TotalSent}, Batches={Batches}", sent, batches); + + return Ok(new + { + TotalRequested = totalCount, + TotalSent = sent, + BatchSize = batchSize, + DelayMs = delayMs, + Batches = batches, + Canceled = false + }); + } + catch (OperationCanceledException) + { + logger.LogInformation("Generation was canceled. TotalSent={TotalSent}/{TotalCount}", sent, totalCount); + + return Ok(new + { + TotalRequested = totalCount, + TotalSent = sent, + BatchSize = batchSize, + DelayMs = delayMs, + Batches = batches, + Canceled = true + }); + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error during generation/publishing. TotalSent={TotalSent}/{TotalCount}", sent, totalCount); + return StatusCode(500, "An error occurred while generating and sending rentals"); + } + } +} \ No newline at end of file diff --git a/CarRental.Generator/Generation/GeneratorOptions.cs b/CarRental.Generator/Generation/GeneratorOptions.cs index a035c0ef6..867e485e4 100644 --- a/CarRental.Generator/Generation/GeneratorOptions.cs +++ b/CarRental.Generator/Generation/GeneratorOptions.cs @@ -1,7 +1,17 @@ namespace CarRental.Generator.Generation; +/// +/// Configuration options for the rental data generator +/// public class GeneratorOptions { - public List CarIds { get; set; } = new(); - public List ClientIds { get; set; } = new(); + /// + /// List of available car IDs to randomly select from + /// + public List CarIds { get; set; } = new(); + + /// + /// List of available client IDs to randomly select from + /// + public List ClientIds { get; set; } = new(); } diff --git a/CarRental.Generator/Generation/RentGeneratorService.cs b/CarRental.Generator/Generation/RentGeneratorService.cs index 2357efac9..a6f10430c 100644 --- a/CarRental.Generator/Generation/RentGeneratorService.cs +++ b/CarRental.Generator/Generation/RentGeneratorService.cs @@ -1,26 +1,44 @@ using CarRental.Application.Contracts.Rent; using Microsoft.Extensions.Options; using Bogus; -using CarRental.Generator.Generation; -namespace CarRental.Generator; +namespace CarRental.Generator.Generation; -public static class RentGeneratorService +/// +/// Service for generating fake rental contract data +/// +public class RentGeneratorService { private readonly GeneratorOptions _options; + /// + /// Initializes a new instance of the class + /// + /// Generator configuration options public RentGeneratorService(IOptions options) { _options = options.Value; } - public static IList GenerateContract(int count) + /// + /// Generates a specified number of fake rental contracts + /// + /// Number of contracts to generate + /// List of generated rental DTOs + /// Thrown when CarIds or ClientIds lists are empty + public IList GenerateContract(int count) { + if (_options.CarIds == null || !_options.CarIds.Any()) + throw new InvalidOperationException("CarIds list is empty. Check appsettings.json Generator section."); + + if (_options.ClientIds == null || !_options.ClientIds.Any()) + throw new InvalidOperationException("ClientIds list is empty. Check appsettings.json Generator section."); + var generatedRents = new Faker().CustomInstantiator(f => new RentCreateUpdateDto( StartDateTime: f.Date.Soon(1), Duration: f.Random.Double(1, 100), - CarId: f.PickRandom(_options.CarIds), - ClientId: f.PickRandom(_options.ClientIds) + CarId: Guid.Parse(f.PickRandom(_options.CarIds)), + ClientId: Guid.Parse(f.PickRandom(_options.ClientIds)) ) ); diff --git a/CarRental.Generator/KafkaProducer.cs b/CarRental.Generator/KafkaProducer.cs new file mode 100644 index 000000000..2c67edaf6 --- /dev/null +++ b/CarRental.Generator/KafkaProducer.cs @@ -0,0 +1,88 @@ +using Confluent.Kafka; +using Microsoft.Extensions.Options; +using CarRental.Application.Contracts.Rent; +using System.Text.Json; + +namespace CarRental.Generator; + +/// +/// Kafka producer that serializes into JSON +/// and publishes messages to a configured topic +/// +/// Logger instance +/// Kafka producer +/// Producer settings +public class KafkaProducer( + ILogger logger, + IProducer producer, + IOptions options) +{ + private readonly KafkaProducerSettings _settings = options.Value; + + /// + /// Sends a rent DTO as a JSON message to Kafka + /// + /// Rent DTO to send + /// Cancellation token + public async Task Produce(RentCreateUpdateDto dto, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(_settings.TopicName)) + throw new InvalidOperationException("KafkaProducerSettings.TopicName must be configured."); + + var payload = JsonSerializer.Serialize(dto); + + for (var attempt = 1; attempt <= _settings.MaxProduceAttempts; attempt++) + { + try + { + var result = await producer.ProduceAsync(_settings.TopicName, new Message { Value = payload }, cancellationToken); + + logger.LogInformation( + "Kafka message produced successfully. Topic={Topic}, Partition={Partition}, Offset={Offset}, " + + "CarId={CarId}, ClientId={ClientId}, StartDateTime={StartDateTime}, Duration={Duration}", + result.Topic, result.Partition.Value, result.Offset.Value, + dto.CarId, dto.ClientId, dto.StartDateTime, dto.Duration); + + return; + } + catch (ProduceException ex) when (attempt < _settings.MaxProduceAttempts) + { + logger.LogWarning(ex, + "Kafka produce attempt {Attempt}/{MaxAttempts} failed. Reason={Reason}. Retrying in {Delay}ms...", + attempt, _settings.MaxProduceAttempts, ex.Error.Reason, _settings.RetryDelayMs); + + await Task.Delay(_settings.RetryDelayMs, cancellationToken); + } + catch (ProduceException ex) + { + logger.LogError(ex, + "Kafka produce failed after {MaxAttempts} attempts. Reason={Reason}. CarId={CarId}, ClientId={ClientId}", + _settings.MaxProduceAttempts, ex.Error.Reason, dto.CarId, dto.ClientId); + + throw; + } + } + } + + /// + /// Sends a batch of rent DTOs as JSON messages to Kafka in parallel + /// + /// Rent DTOs to send + /// Cancellation token + public async Task ProduceMany(IList dtos, CancellationToken cancellationToken = default) + { + if (!dtos.Any()) + { + logger.LogWarning("No rent DTOs to produce to Kafka."); + return; + } + + logger.LogInformation("Starting to produce {Count} rent messages to Kafka topic: {Topic}", + dtos.Count, _settings.TopicName); + + var tasks = dtos.Select(dto => Produce(dto, cancellationToken)); + await Task.WhenAll(tasks); + + logger.LogInformation("Successfully produced all {Count} rent messages to Kafka", dtos.Count); + } +} \ No newline at end of file diff --git a/CarRental.Generator/KafkaProducerSettings.cs b/CarRental.Generator/KafkaProducerSettings.cs new file mode 100644 index 000000000..0e51834cb --- /dev/null +++ b/CarRental.Generator/KafkaProducerSettings.cs @@ -0,0 +1,22 @@ +namespace CarRental.Generator; + +/// +/// Kafka settings used by the CarRental Kafka producer host +/// +public class KafkaProducerSettings +{ + /// + /// Kafka topic name used for producing messages + /// + public string TopicName { get; init; } = "car-rentals"; + + /// + /// Maximum number of attempts to send a message + /// + public int MaxProduceAttempts { get; init; } = 5; + + /// + /// Delay between produce retries in milliseconds + /// + public int RetryDelayMs { get; init; } = 1000; +} \ No newline at end of file diff --git a/CarRental.Generator/Program.cs b/CarRental.Generator/Program.cs index a620b52e3..c4e0c3f2b 100644 --- a/CarRental.Generator/Program.cs +++ b/CarRental.Generator/Program.cs @@ -1,33 +1,63 @@ -namespace CarRental.Generator.Generation; +using CarRental.Generator; +using CarRental.Generator.Generation; +using CarRental.ServiceDefaults; +using Confluent.Kafka; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure(builder.Configuration.GetSection("Generator")); + builder.AddServiceDefaults(); -builder.Services.Configure(builder.Configuration.GetSection("Generator")); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => +{ + var cfg = sp.GetRequiredService(); + var bootstrapServers = cfg.GetConnectionString("car-rental-kafka"); + if (string.IsNullOrWhiteSpace(bootstrapServers)) + throw new InvalidOperationException("Kafka connection string 'car-rental-kafka' is missing."); + var producerConfig = new ProducerConfig + { + BootstrapServers = bootstrapServers, + Acks = Acks.All + }; + + return new ProducerBuilder(producerConfig).Build(); +}); + +builder.Services.AddSingleton(); +builder.Services.AddControllers().AddJsonOptions(o => +{ + o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); -// Add services to the container. -builder.Services.AddRazorPages(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("CarRental")) + .Distinct(); + + foreach (var assembly in assemblies) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } +}); var app = builder.Build(); - app.MapDefaultEndpoints(); - -// Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) +if (app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); -app.UseStaticFiles(); - -app.UseRouting(); - app.UseAuthorization(); +app.MapControllers(); -app.MapRazorPages(); - -app.Run(); +app.Run(); \ No newline at end of file diff --git a/CarRental.Generator/Properties/launchSettings.json b/CarRental.Generator/Properties/launchSettings.json index 8df699d1b..fe877566f 100644 --- a/CarRental.Generator/Properties/launchSettings.json +++ b/CarRental.Generator/Properties/launchSettings.json @@ -13,6 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:5112", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,6 +23,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "https://localhost:7160;http://localhost:5112", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -30,6 +32,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental.Infrastructure/CarRental.Infrastructure.csproj index 151629217..81047227d 100644 --- a/CarRental.Infrastructure/CarRental.Infrastructure.csproj +++ b/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -1,18 +1,19 @@  - - - - - - - - - - + net8.0 enable enable True + + + + + + + + + + diff --git a/CarRental.Infrastructure/Kafka/Consumer.cs b/CarRental.Infrastructure/Kafka/Consumer.cs new file mode 100644 index 000000000..ebc411299 --- /dev/null +++ b/CarRental.Infrastructure/Kafka/Consumer.cs @@ -0,0 +1,152 @@ +using Confluent.Kafka; +using CarRental.Application.Contracts.Rent; +using CarRental.Application.Contracts.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace CarRental.Infrastructure.Kafka; + +/// +/// Background service that consumes rent messages from Kafka and saves them to database +/// +/// Logger for recording operations +/// Kafka consumer instance +/// Factory for creating service scopes +/// Consumer configuration settings +public class Consumer( + ILogger logger, + IConsumer consumer, + IServiceScopeFactory scopeFactory, + IOptions options) : BackgroundService +{ + private readonly ConsumerSettings _settings = options.Value; + + /// + /// Main execution loop that continuously consumes and processes messages from Kafka + /// + /// Cancellation token to stop the consumer + /// Task representing the asynchronous operation + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + consumer.Subscribe(_settings.TopicName); + logger.LogInformation("KafkaConsumer started on topic {TopicName}", _settings.TopicName); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to subscribe to topic {TopicName}", _settings.TopicName); + return; + } + + try + { + while (!stoppingToken.IsCancellationRequested) + { + ConsumeResult? message = null; + + try + { + message = consumer.Consume(TimeSpan.FromMilliseconds(_settings.ConsumeTimeoutMs)); + + if (message is null) + continue; + + var payload = message.Message?.Value; + + if (string.IsNullOrWhiteSpace(payload)) + { + logger.LogWarning("Empty payload. Topic={Topic}, Offset={Offset}", + message.Topic, message.Offset.Value); + CommitIfNeeded(message); + continue; + } + + RentCreateUpdateDto? dto = null; + + for (var attempt = 1; attempt <= _settings.MaxDeserializeAttempts; attempt++) + { + try + { + dto = JsonSerializer.Deserialize(payload); + break; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Deserialization attempt {Attempt} failed", attempt); + } + } + + if (dto == null) + { + logger.LogError("Failed to deserialize after {MaxAttempts} attempts", + _settings.MaxDeserializeAttempts); + CommitIfNeeded(message); + continue; + } + + using var scope = scopeFactory.CreateScope(); + var rentService = scope.ServiceProvider.GetRequiredService>(); + + var savedRent = await rentService.Create(dto); + + CommitIfNeeded(message); + logger.LogInformation("Saved rent with Id: {RentId} from Kafka", savedRent.Id); + } + catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) + { + logger.LogWarning("Topic not available, retrying..."); + await Task.Delay(1000, stoppingToken); + } + catch (ConsumeException ex) + { + logger.LogError(ex, "Consume error: {Reason}", ex.Error.Reason); + } + catch (OperationCanceledException) + { + logger.LogInformation("KafkaConsumer stopping"); + break; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error"); + } + } + } + finally + { + try + { + consumer.Unsubscribe(); + consumer.Close(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error closing consumer"); + } + + logger.LogInformation("KafkaConsumer stopped"); + } + } + + /// + /// Commits the message offset if auto-commit is disabled + /// + /// The consumed message to commit + private void CommitIfNeeded(ConsumeResult message) + { + if (_settings.AutoCommitEnabled) + return; + try + { + consumer.Commit(message); + } + catch (KafkaException ex) + { + logger.LogWarning(ex, "Commit failed. Offset={Offset}", message.Offset.Value); + } + } +} \ No newline at end of file diff --git a/CarRental.Infrastructure/Kafka/ConsumerSettings.cs b/CarRental.Infrastructure/Kafka/ConsumerSettings.cs new file mode 100644 index 000000000..033cdacaf --- /dev/null +++ b/CarRental.Infrastructure/Kafka/ConsumerSettings.cs @@ -0,0 +1,33 @@ +namespace CarRental.Infrastructure.Kafka; + +/// +/// Kafka settings used by CarRental Kafka consumer +/// +public class ConsumerSettings +{ + /// + /// Kafka topic name used for consuming messages + /// + public string TopicName { get; init; } = "car-rentals"; + + /// + /// Consumer group ID for Kafka + /// + public string GroupId { get; init; } = "car-rental-consumer-group"; + + /// + /// Enables Kafka auto-commit for the consumer. + /// If false, the consumer commits offsets manually after successful processing + /// + public bool AutoCommitEnabled { get; init; } = false; + + /// + /// Poll timeout for consuming messages in milliseconds + /// + public int ConsumeTimeoutMs { get; init; } = 5000; + + /// + /// Maximum number of attempts to deserialize a message payload + /// + public int MaxDeserializeAttempts { get; init; } = 3; +} diff --git a/CarRental.sln b/CarRental.sln index 6344b38f1..4af6600c6 100644 --- a/CarRental.sln +++ b/CarRental.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.ServiceDefaults", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Generator", "CarRental.Generator\CarRental.Generator.csproj", "{BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarRental.Application.Contracts", "CarRental.Application.Contracts\CarRental.Application.Contracts.csproj", "{F4DCE45C-696E-49E2-957B-97F6D57AC5F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -125,6 +127,18 @@ Global {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x64.Build.0 = Release|Any CPU {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x86.ActiveCfg = Release|Any CPU {BE76D53D-F0C3-E6B6-15A8-337EEF8BC9EC}.Release|x86.Build.0 = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|x64.Build.0 = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Debug|x86.Build.0 = Debug|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|Any CPU.Build.0 = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|x64.ActiveCfg = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|x64.Build.0 = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|x86.ActiveCfg = Release|Any CPU + {F4DCE45C-696E-49E2-957B-97F6D57AC5F5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From bade9a15a12e0afe9cfea858be9465fe4445b2f9 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 19 Feb 2026 03:03:06 +0400 Subject: [PATCH 34/37] optimized work with Guid in Lists, added control for the number of records, the number of batches, and the delay time and other optimizations --- CarRental.Api/Program.cs | 25 +-------- .../WebApplicationBuilderExtensions.cs | 32 +++++++++++ .../Generator/GenerateRentalsRequest.cs | 30 +++++++++++ .../Generator/GenerateRentalsResponse.cs | 37 +++++++++++++ .../Controller/GeneratorController.cs | 53 ++++++++----------- .../Generation/GeneratorOptions.cs | 41 ++++++++++++-- .../Generation/RentGeneratorService.cs | 36 +++++-------- CarRental.Generator/KafkaProducer.cs | 30 +++++++++-- CarRental.Generator/KafkaProducerSettings.cs | 5 ++ CarRental.Generator/Program.cs | 29 +++------- CarRental.Infrastructure/Kafka/Consumer.cs | 17 ++++-- 11 files changed, 223 insertions(+), 112 deletions(-) create mode 100644 CarRental.Api/WebApplicationBuilderExtensions.cs create mode 100644 CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs create mode 100644 CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs diff --git a/CarRental.Api/Program.cs b/CarRental.Api/Program.cs index c0b7d70d3..d34aa4d3a 100644 --- a/CarRental.Api/Program.cs +++ b/CarRental.Api/Program.cs @@ -1,3 +1,4 @@ +using CarRental.Api; using CarRental.Application.Contracts.Car; using CarRental.Application.Contracts.CarModel; using CarRental.Application.Contracts.CarModelGeneration; @@ -10,14 +11,11 @@ using CarRental.Domain.Interfaces; using CarRental.Domain.InternalData.ComponentClasses; using CarRental.Infrastructure; -using CarRental.Infrastructure.Kafka; using CarRental.Infrastructure.Repository; using CarRental.ServiceDefaults; -using Confluent.Kafka; using Mapster; using MapsterMapper; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using MongoDB.Driver; using System.Reflection; @@ -63,26 +61,7 @@ builder.Services.AddScoped(); -builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection("KafkaConsumer")); -builder.Services.AddSingleton>(sp => -{ - var settings = sp.GetRequiredService>().Value; - var bootstrapServers = builder.Configuration.GetConnectionString("car-rental-kafka"); - if (string.IsNullOrWhiteSpace(bootstrapServers)) - throw new InvalidOperationException("Kafka connection string 'car-rental-kafka' is missing."); - var config = new ConsumerConfig - { - BootstrapServers = bootstrapServers, - GroupId = settings.GroupId, - AutoOffsetReset = AutoOffsetReset.Earliest, - EnableAutoCommit = settings.AutoCommitEnabled - }; - - return new ConsumerBuilder(config).Build(); -}); - -builder.Services.AddHostedService(); +builder.AddGeneratorService(); builder.Services.AddControllers() .AddJsonOptions(options => diff --git a/CarRental.Api/WebApplicationBuilderExtensions.cs b/CarRental.Api/WebApplicationBuilderExtensions.cs new file mode 100644 index 000000000..3e4987e76 --- /dev/null +++ b/CarRental.Api/WebApplicationBuilderExtensions.cs @@ -0,0 +1,32 @@ +using CarRental.Infrastructure.Kafka; +using Confluent.Kafka; + +namespace CarRental.Api; + +/// +/// Extension methods for registering generator service client in DI container +/// +internal static class WebApplicationBuilderExtensions +{ + /// + /// Registers Kafka consumer client for interacting with the data generator service + /// + /// Web application builder + /// Web application builder with registered Kafka services + public static WebApplicationBuilder AddGeneratorService(this WebApplicationBuilder builder) + { + builder.Services.AddHostedService(); + + builder.AddKafkaConsumer( + "car-rental-kafka", + configureSettings: settings => + { + settings.Config.GroupId = "car-rental-consumer-group"; + settings.Config.AutoOffsetReset = Confluent.Kafka.AutoOffsetReset.Earliest; + settings.Config.EnableAutoCommit = false; + } + ); + + return builder; + } +} \ No newline at end of file diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs new file mode 100644 index 000000000..335614224 --- /dev/null +++ b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace CarRental.Application.Contracts.Generator; + +/// +/// Request model for generating rental contracts +/// +public class GenerateRentalsRequest +{ + /// + /// Total number of rentals to generate + /// + [Required] + [Range(1, 1000, ErrorMessage = "TotalCount must be between 1 and 1000.")] + public int TotalCount { get; set; } + + /// + /// Number of rentals per batch + /// + [Required] + [Range(1, 100, ErrorMessage = "BatchSize must be between 1 and 100.")] + public int BatchSize { get; set; } + + /// + /// Delay between batches in milliseconds + /// + [Required] + [Range(100, 60000, ErrorMessage = "DelayMs must be between 0 and 60000.")] + public int DelayMs { get; set; } +} \ No newline at end of file diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs new file mode 100644 index 000000000..dc51d7042 --- /dev/null +++ b/CarRental.Application.Contracts/Generator/GenerateRentalsResponse.cs @@ -0,0 +1,37 @@ +namespace CarRental.Application.Contracts.Generator; + +/// +/// Response model for rental generation operation +/// +public class GenerateRentalsResponse +{ + /// + /// Total number of items requested + /// + public int TotalRequested { get; set; } + + /// + /// Total number of items successfully sent + /// + public int TotalSent { get; set; } + + /// + /// Size of each batch + /// + public int BatchSize { get; set; } + + /// + /// Delay between batches in milliseconds + /// + public int DelayMs { get; set; } + + /// + /// Number of batches sent + /// + public int Batches { get; set; } + + /// + /// Whether the operation was canceled + /// + public bool Canceled { get; set; } +} \ No newline at end of file diff --git a/CarRental.Generator/Controller/GeneratorController.cs b/CarRental.Generator/Controller/GeneratorController.cs index feebe5a8e..c14e04bba 100644 --- a/CarRental.Generator/Controller/GeneratorController.cs +++ b/CarRental.Generator/Controller/GeneratorController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using CarRental.Generator.Generation; using CarRental.Application.Contracts.Rent; +using CarRental.Application.Contracts.Generator; namespace CarRental.Generator.Controller; @@ -20,39 +21,29 @@ public class GeneratorController( /// /// Generates and publishes rental contracts to Kafka in batches /// - /// Total number of rentals to generate - /// Number of rentals per batch - /// Delay between batches in milliseconds + /// Generation parameters /// Cancellation token /// Result with generation statistics [HttpPost("rentals")] - public async Task GenerateRentals( - [FromQuery] int totalCount, - [FromQuery] int batchSize, - [FromQuery] int delayMs, + public async Task> GenerateRentals( + [FromQuery] GenerateRentalsRequest request, CancellationToken cancellationToken) { - if (totalCount <= 0) - return BadRequest("totalCount must be greater than 0."); - - if (batchSize <= 0) - return BadRequest("batchSize must be greater than 0."); - - if (delayMs < 0) - return BadRequest("delayMs must be greater than or equal to 0."); + if (!ModelState.IsValid) + return BadRequest(ModelState); logger.LogInformation("Rental generation requested. TotalCount={TotalCount}, BatchSize={BatchSize}, DelayMs={DelayMs}", - totalCount, batchSize, delayMs); + request.TotalCount, request.BatchSize, request.DelayMs); var sent = 0; var batches = 0; try { - while (sent < totalCount && !cancellationToken.IsCancellationRequested) + while (sent < request.TotalCount && !cancellationToken.IsCancellationRequested) { - var remaining = totalCount - sent; - var currentBatchSize = Math.Min(batchSize, remaining); + var remaining = request.TotalCount - sent; + var currentBatchSize = Math.Min(request.BatchSize, remaining); IList batch = rentGenerator.GenerateContract(currentBatchSize); @@ -61,41 +52,41 @@ public async Task GenerateRentals( sent += currentBatchSize; batches++; - if (sent < totalCount && delayMs > 0) + if (sent < request.TotalCount && request.DelayMs > 0) { - await Task.Delay(delayMs, cancellationToken); + await Task.Delay(request.DelayMs, cancellationToken); } } logger.LogInformation("Generation finished. TotalSent={TotalSent}, Batches={Batches}", sent, batches); - return Ok(new + return Ok(new GenerateRentalsResponse { - TotalRequested = totalCount, + TotalRequested = request.TotalCount, TotalSent = sent, - BatchSize = batchSize, - DelayMs = delayMs, + BatchSize = request.BatchSize, + DelayMs = request.DelayMs, Batches = batches, Canceled = false }); } catch (OperationCanceledException) { - logger.LogInformation("Generation was canceled. TotalSent={TotalSent}/{TotalCount}", sent, totalCount); + logger.LogInformation("Generation was canceled. TotalSent={TotalSent}/{TotalCount}", sent, request.TotalCount); - return Ok(new + return Ok(new GenerateRentalsResponse { - TotalRequested = totalCount, + TotalRequested = request.TotalCount, TotalSent = sent, - BatchSize = batchSize, - DelayMs = delayMs, + BatchSize = request.BatchSize, + DelayMs = request.DelayMs, Batches = batches, Canceled = true }); } catch (Exception ex) { - logger.LogError(ex, "Unexpected error during generation/publishing. TotalSent={TotalSent}/{TotalCount}", sent, totalCount); + logger.LogError(ex, "Unexpected error during generation/publishing. TotalSent={TotalSent}/{TotalCount}", sent, request.TotalCount); return StatusCode(500, "An error occurred while generating and sending rentals"); } } diff --git a/CarRental.Generator/Generation/GeneratorOptions.cs b/CarRental.Generator/Generation/GeneratorOptions.cs index 867e485e4..4a1e04637 100644 --- a/CarRental.Generator/Generation/GeneratorOptions.cs +++ b/CarRental.Generator/Generation/GeneratorOptions.cs @@ -5,13 +5,44 @@ /// public class GeneratorOptions { + private List? _carIdStrings; + private List? _clientIdStrings; + private List? _carIds; + private List? _clientIds; + + /// + /// List of available car IDs as strings + /// + public List CarIds + { + get => _carIdStrings ?? new(); + set + { + _carIdStrings = value; + _carIds = value?.Select(Guid.Parse).ToList(); + } + } + + /// + /// List of available client IDs as strings + /// + public List ClientIds + { + get => _clientIdStrings ?? new(); + set + { + _clientIdStrings = value; + _clientIds = value?.Select(Guid.Parse).ToList(); + } + } + /// - /// List of available car IDs to randomly select from + /// List of available car IDs as GUIDs /// - public List CarIds { get; set; } = new(); + public List CarIdGuids => _carIds ?? new(); /// - /// List of available client IDs to randomly select from + /// List of available client IDs as GUIDs /// - public List ClientIds { get; set; } = new(); -} + public List ClientIdGuids => _clientIds ?? new(); +} \ No newline at end of file diff --git a/CarRental.Generator/Generation/RentGeneratorService.cs b/CarRental.Generator/Generation/RentGeneratorService.cs index a6f10430c..dd5280ef7 100644 --- a/CarRental.Generator/Generation/RentGeneratorService.cs +++ b/CarRental.Generator/Generation/RentGeneratorService.cs @@ -7,41 +7,31 @@ namespace CarRental.Generator.Generation; /// /// Service for generating fake rental contract data /// -public class RentGeneratorService +/// Generator configuration options monitor +public class RentGeneratorService(IOptionsMonitor optionsMonitor) { - private readonly GeneratorOptions _options; - - /// - /// Initializes a new instance of the class - /// - /// Generator configuration options - public RentGeneratorService(IOptions options) - { - _options = options.Value; - } - /// /// Generates a specified number of fake rental contracts /// /// Number of contracts to generate /// List of generated rental DTOs - /// Thrown when CarIds or ClientIds lists are empty public IList GenerateContract(int count) { - if (_options.CarIds == null || !_options.CarIds.Any()) - throw new InvalidOperationException("CarIds list is empty. Check appsettings.json Generator section."); + var options = optionsMonitor.CurrentValue; + + if (!options.CarIdGuids.Any()) + throw new InvalidOperationException("CarIds list is empty. Check configuration."); - if (_options.ClientIds == null || !_options.ClientIds.Any()) - throw new InvalidOperationException("ClientIds list is empty. Check appsettings.json Generator section."); + if (!options.ClientIdGuids.Any()) + throw new InvalidOperationException("ClientIds list is empty. Check configuration."); var generatedRents = new Faker().CustomInstantiator(f => new RentCreateUpdateDto( StartDateTime: f.Date.Soon(1), Duration: f.Random.Double(1, 100), - CarId: Guid.Parse(f.PickRandom(_options.CarIds)), - ClientId: Guid.Parse(f.PickRandom(_options.ClientIds)) - ) - ); + CarId: f.PickRandom(options.CarIdGuids), + ClientId: f.PickRandom(options.ClientIdGuids) + )); - return generatedRents.Generate(count); + return generatedRents.Generate(count); } -} +} \ No newline at end of file diff --git a/CarRental.Generator/KafkaProducer.cs b/CarRental.Generator/KafkaProducer.cs index 2c67edaf6..d3f928e55 100644 --- a/CarRental.Generator/KafkaProducer.cs +++ b/CarRental.Generator/KafkaProducer.cs @@ -17,7 +17,8 @@ public class KafkaProducer( IProducer producer, IOptions options) { - private readonly KafkaProducerSettings _settings = options.Value; + private readonly KafkaProducerSettings _settings = options.Value + ?? throw new InvalidOperationException("KafkaProducerSettings must be configured."); /// /// Sends a rent DTO as a JSON message to Kafka @@ -65,7 +66,7 @@ public async Task Produce(RentCreateUpdateDto dto, CancellationToken cancellatio } /// - /// Sends a batch of rent DTOs as JSON messages to Kafka in parallel + /// Sends a batch of rent DTOs as JSON messages to Kafka with controlled parallelism /// /// Rent DTOs to send /// Cancellation token @@ -77,10 +78,29 @@ public async Task ProduceMany(IList dtos, CancellationToken return; } - logger.LogInformation("Starting to produce {Count} rent messages to Kafka topic: {Topic}", - dtos.Count, _settings.TopicName); + logger.LogInformation("Starting to produce {Count} rent messages to Kafka topic: {Topic} with parallelism {MaxParallelism}", + dtos.Count, _settings.TopicName, _settings.MaxParallelism); + + using var semaphore = new SemaphoreSlim(_settings.MaxParallelism); + var tasks = new List(); + + foreach (var dto in dtos) + { + await semaphore.WaitAsync(cancellationToken); + + tasks.Add(Task.Run(async () => + { + try + { + await Produce(dto, cancellationToken); + } + finally + { + semaphore.Release(); + } + }, cancellationToken)); + } - var tasks = dtos.Select(dto => Produce(dto, cancellationToken)); await Task.WhenAll(tasks); logger.LogInformation("Successfully produced all {Count} rent messages to Kafka", dtos.Count); diff --git a/CarRental.Generator/KafkaProducerSettings.cs b/CarRental.Generator/KafkaProducerSettings.cs index 0e51834cb..65160b56c 100644 --- a/CarRental.Generator/KafkaProducerSettings.cs +++ b/CarRental.Generator/KafkaProducerSettings.cs @@ -19,4 +19,9 @@ public class KafkaProducerSettings /// Delay between produce retries in milliseconds /// public int RetryDelayMs { get; init; } = 1000; + + /// + /// Maximum number of parallel produce operations + /// + public int MaxParallelism { get; init; } = 10; } \ No newline at end of file diff --git a/CarRental.Generator/Program.cs b/CarRental.Generator/Program.cs index c4e0c3f2b..12efdeeb7 100644 --- a/CarRental.Generator/Program.cs +++ b/CarRental.Generator/Program.cs @@ -1,8 +1,7 @@ -using CarRental.Generator; +using CarRental.Generator; using CarRental.Generator.Generation; using CarRental.ServiceDefaults; using Confluent.Kafka; -using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); @@ -10,27 +9,13 @@ builder.AddServiceDefaults(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(sp => -{ - var cfg = sp.GetRequiredService(); - var bootstrapServers = cfg.GetConnectionString("car-rental-kafka"); - if (string.IsNullOrWhiteSpace(bootstrapServers)) - throw new InvalidOperationException("Kafka connection string 'car-rental-kafka' is missing."); - var producerConfig = new ProducerConfig - { - BootstrapServers = bootstrapServers, - Acks = Acks.All - }; - - return new ProducerBuilder(producerConfig).Build(); -}); +builder.AddKafkaProducer("car-rental-kafka"); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddControllers().AddJsonOptions(o => -{ - o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); -}); + +builder.Services.AddControllers(); +builder.Services.AddAuthorization(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => @@ -58,6 +43,6 @@ app.UseHttpsRedirection(); app.UseAuthorization(); -app.MapControllers(); +app.MapControllers(); // ← маппит контроллеры app.Run(); \ No newline at end of file diff --git a/CarRental.Infrastructure/Kafka/Consumer.cs b/CarRental.Infrastructure/Kafka/Consumer.cs index ebc411299..482ab5379 100644 --- a/CarRental.Infrastructure/Kafka/Consumer.cs +++ b/CarRental.Infrastructure/Kafka/Consumer.cs @@ -55,13 +55,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (message is null) continue; + if (message.IsPartitionEOF || message.Message == null) + { + logger.LogDebug("Reached end of partition or empty message"); + continue; + } + var payload = message.Message?.Value; if (string.IsNullOrWhiteSpace(payload)) { logger.LogWarning("Empty payload. Topic={Topic}, Offset={Offset}", message.Topic, message.Offset.Value); - CommitIfNeeded(message); continue; } @@ -125,9 +130,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { - logger.LogWarning(ex, "Error closing consumer"); + logger.LogError(ex, "Error closing consumer"); } - logger.LogInformation("KafkaConsumer stopped"); } } @@ -140,6 +144,13 @@ private void CommitIfNeeded(ConsumeResult message) { if (_settings.AutoCommitEnabled) return; + + if (message == null || message.Message == null || message.IsPartitionEOF) + { + logger.LogDebug("Skipping commit for null/EOF message"); + return; + } + try { consumer.Commit(message); From 65c81d49e3eca6cd664bfd1ca23f28bf3c78964e Mon Sep 17 00:00:00 2001 From: Amitroki Date: Thu, 19 Feb 2026 19:24:40 +0400 Subject: [PATCH 35/37] made changes in GeneratorController.cs, KafkaProducer.cs, GenerateRentalsRequest.cs, Program.cs --- .../Generator/GenerateRentalsRequest.cs | 2 +- .../Controller/GeneratorController.cs | 3 -- .../Generation/GeneratorOptions.cs | 33 +++++++++++++++---- CarRental.Generator/KafkaProducer.cs | 3 -- CarRental.Generator/Program.cs | 2 +- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs index 335614224..bd88994d5 100644 --- a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs +++ b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs @@ -25,6 +25,6 @@ public class GenerateRentalsRequest /// Delay between batches in milliseconds /// [Required] - [Range(100, 60000, ErrorMessage = "DelayMs must be between 0 and 60000.")] + [Range(100, 30000, ErrorMessage = "DelayMs must be between 0 and 60000.")] public int DelayMs { get; set; } } \ No newline at end of file diff --git a/CarRental.Generator/Controller/GeneratorController.cs b/CarRental.Generator/Controller/GeneratorController.cs index c14e04bba..a9e418ecb 100644 --- a/CarRental.Generator/Controller/GeneratorController.cs +++ b/CarRental.Generator/Controller/GeneratorController.cs @@ -29,9 +29,6 @@ public async Task> GenerateRentals( [FromQuery] GenerateRentalsRequest request, CancellationToken cancellationToken) { - if (!ModelState.IsValid) - return BadRequest(ModelState); - logger.LogInformation("Rental generation requested. TotalCount={TotalCount}, BatchSize={BatchSize}, DelayMs={DelayMs}", request.TotalCount, request.BatchSize, request.DelayMs); diff --git a/CarRental.Generator/Generation/GeneratorOptions.cs b/CarRental.Generator/Generation/GeneratorOptions.cs index 4a1e04637..977c8d482 100644 --- a/CarRental.Generator/Generation/GeneratorOptions.cs +++ b/CarRental.Generator/Generation/GeneratorOptions.cs @@ -1,7 +1,7 @@ namespace CarRental.Generator.Generation; /// -/// Configuration options for the rental data generator +/// Configuration options for the rental data generator. /// public class GeneratorOptions { @@ -11,7 +11,7 @@ public class GeneratorOptions private List? _clientIds; /// - /// List of available car IDs as strings + /// List of available car IDs as strings (for configuration binding). /// public List CarIds { @@ -19,12 +19,12 @@ public List CarIds set { _carIdStrings = value; - _carIds = value?.Select(Guid.Parse).ToList(); + _carIds = ParseGuids(value, "CarIds"); } } /// - /// List of available client IDs as strings + /// List of available client IDs as strings (for configuration binding). /// public List ClientIds { @@ -32,17 +32,36 @@ public List ClientIds set { _clientIdStrings = value; - _clientIds = value?.Select(Guid.Parse).ToList(); + _clientIds = ParseGuids(value, "ClientIds"); } } /// - /// List of available car IDs as GUIDs + /// List of available car IDs as GUIDs (pre-parsed for performance). /// public List CarIdGuids => _carIds ?? new(); /// - /// List of available client IDs as GUIDs + /// List of available client IDs as GUIDs (pre-parsed for performance). /// public List ClientIdGuids => _clientIds ?? new(); + + private static List ParseGuids(List? values, string fieldName) + { + if (values == null) + return new List(); + var result = new List(); + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + if (string.IsNullOrWhiteSpace(value)) + throw new FormatException($"{fieldName}[{i}] is null or empty"); + if (!Guid.TryParse(value, out var guid)) + throw new FormatException($"{fieldName}[{i}] has invalid GUID format: {value}"); + + result.Add(guid); + } + + return result; + } } \ No newline at end of file diff --git a/CarRental.Generator/KafkaProducer.cs b/CarRental.Generator/KafkaProducer.cs index d3f928e55..224754060 100644 --- a/CarRental.Generator/KafkaProducer.cs +++ b/CarRental.Generator/KafkaProducer.cs @@ -27,9 +27,6 @@ public class KafkaProducer( /// Cancellation token public async Task Produce(RentCreateUpdateDto dto, CancellationToken cancellationToken = default) { - if (string.IsNullOrWhiteSpace(_settings.TopicName)) - throw new InvalidOperationException("KafkaProducerSettings.TopicName must be configured."); - var payload = JsonSerializer.Serialize(dto); for (var attempt = 1; attempt <= _settings.MaxProduceAttempts; attempt++) diff --git a/CarRental.Generator/Program.cs b/CarRental.Generator/Program.cs index 12efdeeb7..572ce8d9a 100644 --- a/CarRental.Generator/Program.cs +++ b/CarRental.Generator/Program.cs @@ -43,6 +43,6 @@ app.UseHttpsRedirection(); app.UseAuthorization(); -app.MapControllers(); // ← маппит контроллеры +app.MapControllers(); app.Run(); \ No newline at end of file From 2f3ef59b84978aa97850ebd94fb83df2971b9b31 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 20 Feb 2026 16:56:39 +0400 Subject: [PATCH 36/37] the error message for incorrect delay time was corrected --- .../Generator/GenerateRentalsRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs index bd88994d5..3c6894ced 100644 --- a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs +++ b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs @@ -25,6 +25,6 @@ public class GenerateRentalsRequest /// Delay between batches in milliseconds /// [Required] - [Range(100, 30000, ErrorMessage = "DelayMs must be between 0 and 60000.")] + [Range(100, 30000, ErrorMessage = "DelayMs must be between 0 and 30000.")] public int DelayMs { get; set; } } \ No newline at end of file From 7c529d128d2f20372690dc2ca6b94c7cce423870 Mon Sep 17 00:00:00 2001 From: Amitroki Date: Fri, 20 Feb 2026 17:00:07 +0400 Subject: [PATCH 37/37] minor changes --- .../Generator/GenerateRentalsRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs index 3c6894ced..9e6104473 100644 --- a/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs +++ b/CarRental.Application.Contracts/Generator/GenerateRentalsRequest.cs @@ -25,6 +25,6 @@ public class GenerateRentalsRequest /// Delay between batches in milliseconds /// [Required] - [Range(100, 30000, ErrorMessage = "DelayMs must be between 0 and 30000.")] + [Range(100, 30000, ErrorMessage = "DelayMs must be between 100 and 30000.")] public int DelayMs { get; set; } } \ No newline at end of file