From a66a81023f6c9b8d0f55672369628c46f3f5f334 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 30 Oct 2025 22:50:22 +0400 Subject: [PATCH 01/48] add classes (models and enum) --- .idea/.gitignore | 13 +++ .idea/indexLayout.xml | 8 ++ .idea/vcs.xml | 7 ++ .../.idea/.idea.BikeRental/.idea/.gitignore | 13 +++ .../.idea.BikeRental/.idea/indexLayout.xml | 8 ++ .../.idea/.idea.BikeRental/.idea/vcs.xml | 6 ++ .../BikeRental.Domain.csproj | 10 +++ BikeRental/BikeRental.Domain/Enum/BikeType.cs | 27 +++++++ BikeRental/BikeRental.Domain/Models/Bike.cs | 27 +++++++ .../BikeRental.Domain/Models/BikeModel.cs | 50 ++++++++++++ BikeRental/BikeRental.Domain/Models/Lease.cs | 32 ++++++++ BikeRental/BikeRental.Domain/Models/Renter.cs | 22 ++++++ BikeRental/BikeRental.Domain/Program.cs | 79 +++++++++++++++++++ .../BikeRental.Tests/BikeRental.Tests.csproj | 10 +++ BikeRental/BikeRental.Tests/Program.cs | 3 + BikeRental/BikeRental.sln | 22 ++++++ 16 files changed, 337 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/indexLayout.xml create mode 100644 .idea/vcs.xml create mode 100644 BikeRental/.idea/.idea.BikeRental/.idea/.gitignore create mode 100644 BikeRental/.idea/.idea.BikeRental/.idea/indexLayout.xml create mode 100644 BikeRental/.idea/.idea.BikeRental/.idea/vcs.xml create mode 100644 BikeRental/BikeRental.Domain/BikeRental.Domain.csproj create mode 100644 BikeRental/BikeRental.Domain/Enum/BikeType.cs create mode 100644 BikeRental/BikeRental.Domain/Models/Bike.cs create mode 100644 BikeRental/BikeRental.Domain/Models/BikeModel.cs create mode 100644 BikeRental/BikeRental.Domain/Models/Lease.cs create mode 100644 BikeRental/BikeRental.Domain/Models/Renter.cs create mode 100644 BikeRental/BikeRental.Domain/Program.cs create mode 100644 BikeRental/BikeRental.Tests/BikeRental.Tests.csproj create mode 100644 BikeRental/BikeRental.Tests/Program.cs create mode 100644 BikeRental/BikeRental.sln diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..a013d3a07 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/modules.xml +/.idea.enterprise-development.iml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml new file mode 100644 index 000000000..7b08163ce --- /dev/null +++ b/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..830674470 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/BikeRental/.idea/.idea.BikeRental/.idea/.gitignore b/BikeRental/.idea/.idea.BikeRental/.idea/.gitignore new file mode 100644 index 000000000..e9332b459 --- /dev/null +++ b/BikeRental/.idea/.idea.BikeRental/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.BikeRental.iml +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/BikeRental/.idea/.idea.BikeRental/.idea/indexLayout.xml b/BikeRental/.idea/.idea.BikeRental/.idea/indexLayout.xml new file mode 100644 index 000000000..7b08163ce --- /dev/null +++ b/BikeRental/.idea/.idea.BikeRental/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/BikeRental/.idea/.idea.BikeRental/.idea/vcs.xml b/BikeRental/.idea/.idea.BikeRental/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/BikeRental/.idea/.idea.BikeRental/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj new file mode 100644 index 000000000..2f4fc7765 --- /dev/null +++ b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/BikeRental/BikeRental.Domain/Enum/BikeType.cs b/BikeRental/BikeRental.Domain/Enum/BikeType.cs new file mode 100644 index 000000000..e5eaceec8 --- /dev/null +++ b/BikeRental/BikeRental.Domain/Enum/BikeType.cs @@ -0,0 +1,27 @@ +namespace BikeRental.Domain.Enum; + +/// +/// A class describing the types of bikes that can be rented +/// +public enum BikeType +{ + /// + /// Road bike + /// + Road, + + /// + /// Sports bike + /// + Sport, + + /// + /// Mountain bike + /// + Mountain, + + /// + /// Hybrid bike - a bicycle that combines the qualities of a mountain bike and a road bike + /// + Hybrid +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs new file mode 100644 index 000000000..767db5c23 --- /dev/null +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -0,0 +1,27 @@ +namespace BikeRental.Domain.Models; + +/// +/// A class describing a bike for rent +/// +public class Bike +{ + /// + /// Bike's unique id + /// + public required Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Bike's serial number + /// + public required string SerialNumber { get; set; } + + /// + /// Bike's color + /// + public required string Color { get; set; } + + /// + /// Bike's model + /// + public required BikeModel Model { get; set; } +} diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs new file mode 100644 index 000000000..dcb11e53c --- /dev/null +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -0,0 +1,50 @@ +namespace BikeRental.Domain.Models; + +using Enum; + +/// +/// A class describing the models of bikes that can be rented +/// +public class BikeModel +{ + /// + /// The unique id for bike model + /// + public required Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The type of bicycle: road, sport, mountain, hybrid + /// + public required BikeType Type { get; set; } + + /// + /// The size of the bicycle's wheels + /// + public required int WheelSize { get; set; } + + /// + /// Maximum permissible cyclist weight + /// + public required int MaxСyclistWeight { get; set; } + + /// + /// Weight of the bike model + /// + public required double Weight { get; set; } + + /// + /// The type of braking system used in this model of bike + /// + public required string BrakeType { get; set; } + + /// + /// Year of manufacture of the bicycle model + /// + public required string YearOfManufacture { get; set; } + + /// + /// Cost per hour rental + /// + public required int RentPrice { get; set; } + +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs new file mode 100644 index 000000000..f13f6e1db --- /dev/null +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -0,0 +1,32 @@ +namespace BikeRental.Domain.Models; + +/// +/// A class describing a lease agreement +/// +public class Lease +{ + /// + /// Person who rents a bike + /// + public required Renter Renter { get; set; } + + /// + /// Bike for rent + /// + public required Bike Bike { get; set; } + + /// + /// Lease ID + /// + public required Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Rental start time + /// + public required DateTime RentalStartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int RentalDuration { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs new file mode 100644 index 000000000..581cdb54b --- /dev/null +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -0,0 +1,22 @@ +namespace BikeRental.Domain.Models; + +/// +/// A class describing a bike for rent +/// +public class Renter +{ + /// + /// Renter's id + /// + public required Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Renter's full name + /// + public required string FullName { get; set; } + + /// + /// Renter's phone number + /// + public required string Number { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Program.cs b/BikeRental/BikeRental.Domain/Program.cs new file mode 100644 index 000000000..c1c1ccd29 --- /dev/null +++ b/BikeRental/BikeRental.Domain/Program.cs @@ -0,0 +1,79 @@ +namespace BikeRental.Domain; +using Models; +using Enum; + +/// +/// A class just for print initialized data +/// +public class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("Bike initialization"); + + BikeModel roadBikeModel = new BikeModel + { + Id = Guid.NewGuid(), + Type = BikeType.Road, + WheelSize = 700, + MaxСyclistWeight = 100, + Weight = 10, + BrakeType = "Disc", + YearOfManufacture = "2023", + RentPrice = 15 + }; + + Bike myBike = new Bike + { + Id = Guid.NewGuid(), + SerialNumber = "SN1234567890", + Color = "Red", + Model = roadBikeModel + }; + + Renter renter1 = new Renter + { + Id = Guid.NewGuid(), + FullName = "Иван Иванов", + Number = "+7 (999) 123-45-67" + }; + + Lease lease1 = new Lease + { + Id = Guid.NewGuid(), + Renter = renter1, + Bike = myBike, + RentalStartTime = DateTime.Now, + RentalDuration = 3 + }; + + Console.WriteLine("\nBike Information"); + Console.WriteLine($"Bike ID: {myBike.Id}"); + Console.WriteLine($"Serial Number: {myBike.SerialNumber}"); + Console.WriteLine($"Color: {myBike.Color}"); + + Console.WriteLine("\nBike Model Information"); + Console.WriteLine($" Model ID: {myBike.Model.Id}"); + Console.WriteLine($" Type: {myBike.Model.Type}"); + Console.WriteLine($" Wheel Size: {myBike.Model.WheelSize}"); + Console.WriteLine($" Max Passenger Weight: {myBike.Model.MaxСyclistWeight}"); + Console.WriteLine($" Weight: {myBike.Model.Weight}"); + Console.WriteLine($" Brake Type: {myBike.Model.BrakeType}"); + Console.WriteLine($" Year: {myBike.Model.YearOfManufacture}"); + Console.WriteLine($" Rent Price (per hour): {myBike.Model.RentPrice}"); + + Console.WriteLine($"Full Name: {renter1.FullName}"); + Console.WriteLine($"Phone Number: {renter1.Number}"); + + Console.WriteLine("\nLease Agreement Information"); + Console.WriteLine($"Lease ID: {lease1.Id}"); + Console.WriteLine($"Renter: {lease1.Renter.FullName} (ID: {lease1.Renter.Id})"); + Console.WriteLine($"Bike Serial Number: {lease1.Bike.SerialNumber} (Color: {lease1.Bike.Color})"); + Console.WriteLine($"Rental Start Time: {lease1.RentalStartTime}"); + Console.WriteLine($"Rental Duration (hours): {lease1.RentalDuration}"); + Console.WriteLine($"Estimated Rental Cost: {lease1.RentalDuration * lease1.Bike.Model.RentPrice} (currency units)"); + + + Console.WriteLine("\nInitialization complete."); + } +} diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj new file mode 100644 index 000000000..2f4fc7765 --- /dev/null +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/BikeRental/BikeRental.Tests/Program.cs b/BikeRental/BikeRental.Tests/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/BikeRental/BikeRental.Tests/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.sln b/BikeRental/BikeRental.sln new file mode 100644 index 000000000..2f5161042 --- /dev/null +++ b/BikeRental/BikeRental.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Domain", "BikeRental.Domain\BikeRental.Domain.csproj", "{1004CD79-296D-41B4-99B7-6D1AEF3000C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Tests", "BikeRental.Tests\BikeRental.Tests.csproj", "{383A3622-AE13-45D6-88D3-E8559613718E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1004CD79-296D-41B4-99B7-6D1AEF3000C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1004CD79-296D-41B4-99B7-6D1AEF3000C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1004CD79-296D-41B4-99B7-6D1AEF3000C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1004CD79-296D-41B4-99B7-6D1AEF3000C5}.Release|Any CPU.Build.0 = Release|Any CPU + {383A3622-AE13-45D6-88D3-E8559613718E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {383A3622-AE13-45D6-88D3-E8559613718E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {383A3622-AE13-45D6-88D3-E8559613718E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {383A3622-AE13-45D6-88D3-E8559613718E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From d3f6302ee30017874e2ef97cbbfdda2bca21fab9 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 00:29:57 +0400 Subject: [PATCH 02/48] added data seeder and fixture --- .../.idea/dictionaries/project.xml | 8 ++ .../BikeRental.Domain/Models/BikeModel.cs | 5 +- BikeRental/BikeRental.Domain/Models/Renter.cs | 2 +- .../{Program.cs => Printer.cs} | 12 +-- .../BikeRental.Tests/BikeRental.Tests.csproj | 4 + BikeRental/BikeRental.Tests/Printer.cs | 70 ++++++++++++++ BikeRental/BikeRental.Tests/Program.cs | 3 - BikeRental/BikeRental.Tests/RentalFixture.cs | 92 +++++++++++++++++++ 8 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 BikeRental/.idea/.idea.BikeRental/.idea/dictionaries/project.xml rename BikeRental/BikeRental.Domain/{Program.cs => Printer.cs} (92%) create mode 100644 BikeRental/BikeRental.Tests/Printer.cs delete mode 100644 BikeRental/BikeRental.Tests/Program.cs create mode 100644 BikeRental/BikeRental.Tests/RentalFixture.cs diff --git a/BikeRental/.idea/.idea.BikeRental/.idea/dictionaries/project.xml b/BikeRental/.idea/.idea.BikeRental/.idea/dictionaries/project.xml new file mode 100644 index 000000000..dfd4ac66a --- /dev/null +++ b/BikeRental/.idea/.idea.BikeRental/.idea/dictionaries/project.xml @@ -0,0 +1,8 @@ + + + + арендатели + сyclist + + + \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs index dcb11e53c..e1a957dee 100644 --- a/BikeRental/BikeRental.Domain/Models/BikeModel.cs +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -1,7 +1,6 @@ -namespace BikeRental.Domain.Models; - -using Enum; +using BikeRental.Domain.Enum; +namespace BikeRental.Domain.Models; /// /// A class describing the models of bikes that can be rented /// diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs index 581cdb54b..4b8c1e387 100644 --- a/BikeRental/BikeRental.Domain/Models/Renter.cs +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -18,5 +18,5 @@ public class Renter /// /// Renter's phone number /// - public required string Number { get; set; } + public required string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Program.cs b/BikeRental/BikeRental.Domain/Printer.cs similarity index 92% rename from BikeRental/BikeRental.Domain/Program.cs rename to BikeRental/BikeRental.Domain/Printer.cs index c1c1ccd29..42a7bc1b9 100644 --- a/BikeRental/BikeRental.Domain/Program.cs +++ b/BikeRental/BikeRental.Domain/Printer.cs @@ -1,11 +1,11 @@ -namespace BikeRental.Domain; -using Models; -using Enum; +using BikeRental.Domain.Models; +using BikeRental.Domain.Enum; +namespace BikeRental.Domain; /// /// A class just for print initialized data /// -public class Program +public class Printer { public static void Main(string[] args) { @@ -35,7 +35,7 @@ public static void Main(string[] args) { Id = Guid.NewGuid(), FullName = "Иван Иванов", - Number = "+7 (999) 123-45-67" + PhoneNumber = "+7 (999) 123-45-67" }; Lease lease1 = new Lease @@ -63,7 +63,7 @@ public static void Main(string[] args) Console.WriteLine($" Rent Price (per hour): {myBike.Model.RentPrice}"); Console.WriteLine($"Full Name: {renter1.FullName}"); - Console.WriteLine($"Phone Number: {renter1.Number}"); + Console.WriteLine($"Phone Number: {renter1.PhoneNumber}"); Console.WriteLine("\nLease Agreement Information"); Console.WriteLine($"Lease ID: {lease1.Id}"); diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj index 2f4fc7765..13016642a 100644 --- a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/BikeRental/BikeRental.Tests/Printer.cs b/BikeRental/BikeRental.Tests/Printer.cs new file mode 100644 index 000000000..cd9622a8d --- /dev/null +++ b/BikeRental/BikeRental.Tests/Printer.cs @@ -0,0 +1,70 @@ +namespace BikeRental.Tests; + +/// +/// A class just for print testing data +/// +public class TestDataPrinter +{ + public static void Main(string[] args) + { + var printer = new TestDataPrinter(); + printer.PrintDataCheck(); + } + + public void PrintDataCheck() + { + var fixture = new RentalFixture(); + + Console.WriteLine("Модели"); + foreach (var model in fixture.Models) + { + Console.WriteLine("| {0,-40} | {1,-10} | {2,-10} | {3,-10} | {4,-10} | {5,-10} | {6,-5} | {7,-5} |", + model.Id, + model.Type, + model.WheelSize, + model.Weight, + model.MaxСyclistWeight, + model.BrakeType, + model.YearOfManufacture, + model.RentPrice); + } + + Console.WriteLine("\n"); + Console.WriteLine("Велики"); + foreach (var bike in fixture.Bikes) + { + Console.WriteLine("| {0,-40} | {1,-10} | {2,-10} | {3,-20} |", + bike.Id, + bike.SerialNumber, + bike.Color, + bike.Model?.Id.ToString()); + } + + Console.WriteLine("\n"); + Console.WriteLine("Арендатели"); + foreach (var renter in fixture.Renters) + { + Console.WriteLine("| {0,-40} | {1,-20} | {2,-20} |", + renter.Id, + renter.FullName, + renter.PhoneNumber); + } + + Console.WriteLine("\n"); + Console.WriteLine("Аренда"); + foreach (var rent in fixture.Lease) + { + Console.WriteLine("| {0,-40} | {1,-20} | {2,-20} | {3,-20} | {4,-5} |", + rent.Id, + rent.Bike?.Id.ToString(), + rent.Renter?.Id.ToString(), + rent.RentalStartTime.ToString("dd.MM.yyyy HH:mm"), + rent.RentalDuration); + } + + Console.WriteLine($"Модели: {fixture.Models.Count} шт."); + Console.WriteLine($"Арендаторы: {fixture.Renters.Count} шт."); + Console.WriteLine($"Велосипеды: {fixture.Bikes.Count} шт."); + Console.WriteLine($"Аренды: {fixture.Lease.Count} шт."); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Tests/Program.cs b/BikeRental/BikeRental.Tests/Program.cs deleted file mode 100644 index e5dff12bc..000000000 --- a/BikeRental/BikeRental.Tests/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs new file mode 100644 index 000000000..60da15134 --- /dev/null +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -0,0 +1,92 @@ +using BikeRental.Domain.Enum; +using BikeRental.Domain.Models; + +namespace BikeRental.Tests; + +public class RentalFixture +{ + /// + /// A list of all bike models + /// + public List Models { get; } + + /// + /// /// A list of all bikes for rent + /// + public List Bikes { get; } + + /// + /// A list of all registered renters + /// + public List Renters { get; } + + /// + /// A list of all leases + /// + public readonly List Lease; + + + public RentalFixture() + { + Models = GetBikeModels(); + Renters = GetRenters(); + Bikes = GetBikes(Models); + Lease = GetRentals(Bikes, Renters); + } + + private List GetBikeModels() => + [ + new() { Id = Guid.NewGuid(), Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 27, MaxСyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 29, MaxСyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Hybrid, WheelSize = 26, MaxСyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Mountain, WheelSize = 27, MaxСyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Hybrid, WheelSize = 29, MaxСyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 26, MaxСyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, + new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 28, MaxСyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, + ]; + + private List GetRenters() => + [ + new() { Id = Guid.NewGuid(), FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, + new() { Id = Guid.NewGuid(), FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, + new() { Id = Guid.NewGuid(), FullName = "Григорьев Григорий", PhoneNumber = "+7 934 567 89 01" }, + new() { Id = Guid.NewGuid(), FullName = "Дмитриева Ольга", PhoneNumber = "+7 945 678 90 12" }, + new() { Id = Guid.NewGuid(), FullName = "Николаева Светлана", PhoneNumber = "+7 956 789 01 23" }, + new() { Id = Guid.NewGuid(), FullName = "Михайлов Сергей", PhoneNumber = "+7 967 890 12 34" }, + new() { Id = Guid.NewGuid(), FullName = "Романова Татьяна", PhoneNumber = "+7 978 901 23 45" }, + new() { Id = Guid.NewGuid(), FullName = "Павлов Дмитрий", PhoneNumber = "+7 989 012 34 56" }, + new() { Id = Guid.NewGuid(), FullName = "Фёдорова Екатерина", PhoneNumber = "+7 990 123 45 67" }, + new() { Id = Guid.NewGuid(), FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" }, + ]; + + private List GetBikes(List models) => + [ + new() { Id = Guid.NewGuid(), SerialNumber = "R001", Color = "Silver", Model = models[0] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R002", Color = "Navy", Model = models[1] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R003", Color = "Charcoal", Model = models[2] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R004", Color = "Beige", Model = models[3] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R005", Color = "Burgundy", Model = models[4] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R006", Color = "Teal", Model = models[5] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R007", Color = "Coral", Model = models[6] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R008", Color = "Indigo", Model = models[7] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R009", Color = "Bronze", Model = models[8] }, + new() { Id = Guid.NewGuid(), SerialNumber = "R010", Color = "Lavender", Model = models[9] }, + ]; + + private List GetRentals(List bikes, List renters) => + [ + new() { Id = Guid.NewGuid(), Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, + new() { Id = Guid.NewGuid(), Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, + new() { Id = Guid.NewGuid(), Bike = bikes[2], Renter = renters[2], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, + new() { Id = Guid.NewGuid(), Bike = bikes[3], Renter = renters[3], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, + new() { Id = Guid.NewGuid(), Bike = bikes[4], Renter = renters[4], RentalStartTime = DateTime.Now.AddHours(-20), RentalDuration = 8 }, + new() { Id = Guid.NewGuid(), Bike = bikes[5], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-3), RentalDuration = 1 }, + new() { Id = Guid.NewGuid(), Bike = bikes[6], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-18), RentalDuration = 5 }, + new() { Id = Guid.NewGuid(), Bike = bikes[7], Renter = renters[7], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, + new() { Id = Guid.NewGuid(), Bike = bikes[8], Renter = renters[8], RentalStartTime = DateTime.Now.AddHours(-10), RentalDuration = 4 }, + new() { Id = Guid.NewGuid(), Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 3 }, + ]; +} \ No newline at end of file From 53b25caac4dfabaa0f3baa14dc3e52bc33739a6e Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:18:02 +0400 Subject: [PATCH 03/48] change the type of id from guid to int, added tests and github action --- .github/workflows/dotnet-tests.yml | 23 ++++ BikeRental/BikeRental.Domain/Models/Bike.cs | 2 +- .../BikeRental.Domain/Models/BikeModel.cs | 2 +- BikeRental/BikeRental.Domain/Models/Lease.cs | 2 +- BikeRental/BikeRental.Domain/Models/Renter.cs | 2 +- BikeRental/BikeRental.Domain/Printer.cs | 79 ------------ .../BikeRental.Tests/BikeRental.Tests.csproj | 9 ++ .../BikeRental.Tests/BikeRentalTests.cs | 122 ++++++++++++++++++ BikeRental/BikeRental.Tests/RentalFixture.cs | 93 ++++++------- 9 files changed, 207 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/dotnet-tests.yml delete mode 100644 BikeRental/BikeRental.Domain/Printer.cs create mode 100644 BikeRental/BikeRental.Tests/BikeRentalTests.cs diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml new file mode 100644 index 000000000..927a854a2 --- /dev/null +++ b/.github/workflows/dotnet-tests.yml @@ -0,0 +1,23 @@ +name: dotnet-tests.yml +on: + push: + branches: [ "main" ] +jobs: + 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: Run tests directly + run: | + cd BikeRental.Tests + dotnet restore + dotnet build + dotnet test --verbosity normal diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs index 767db5c23..b3452202c 100644 --- a/BikeRental/BikeRental.Domain/Models/Bike.cs +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -8,7 +8,7 @@ public class Bike /// /// Bike's unique id /// - public required Guid Id { get; set; } = Guid.NewGuid(); + public required int Id { get; set; } /// /// Bike's serial number diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs index e1a957dee..a53402183 100644 --- a/BikeRental/BikeRental.Domain/Models/BikeModel.cs +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -9,7 +9,7 @@ public class BikeModel /// /// The unique id for bike model /// - public required Guid Id { get; set; } = Guid.NewGuid(); + public required int Id { get; set; } /// /// The type of bicycle: road, sport, mountain, hybrid diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index f13f6e1db..271c76fff 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -18,7 +18,7 @@ public class Lease /// /// Lease ID /// - public required Guid Id { get; set; } = Guid.NewGuid(); + public required int Id { get; set; } /// /// Rental start time diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs index 4b8c1e387..7c89426d8 100644 --- a/BikeRental/BikeRental.Domain/Models/Renter.cs +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -8,7 +8,7 @@ public class Renter /// /// Renter's id /// - public required Guid Id { get; set; } = Guid.NewGuid(); + public required int Id { get; set; } /// /// Renter's full name diff --git a/BikeRental/BikeRental.Domain/Printer.cs b/BikeRental/BikeRental.Domain/Printer.cs deleted file mode 100644 index 42a7bc1b9..000000000 --- a/BikeRental/BikeRental.Domain/Printer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using BikeRental.Domain.Models; -using BikeRental.Domain.Enum; - -namespace BikeRental.Domain; -/// -/// A class just for print initialized data -/// -public class Printer -{ - public static void Main(string[] args) - { - Console.WriteLine("Bike initialization"); - - BikeModel roadBikeModel = new BikeModel - { - Id = Guid.NewGuid(), - Type = BikeType.Road, - WheelSize = 700, - MaxСyclistWeight = 100, - Weight = 10, - BrakeType = "Disc", - YearOfManufacture = "2023", - RentPrice = 15 - }; - - Bike myBike = new Bike - { - Id = Guid.NewGuid(), - SerialNumber = "SN1234567890", - Color = "Red", - Model = roadBikeModel - }; - - Renter renter1 = new Renter - { - Id = Guid.NewGuid(), - FullName = "Иван Иванов", - PhoneNumber = "+7 (999) 123-45-67" - }; - - Lease lease1 = new Lease - { - Id = Guid.NewGuid(), - Renter = renter1, - Bike = myBike, - RentalStartTime = DateTime.Now, - RentalDuration = 3 - }; - - Console.WriteLine("\nBike Information"); - Console.WriteLine($"Bike ID: {myBike.Id}"); - Console.WriteLine($"Serial Number: {myBike.SerialNumber}"); - Console.WriteLine($"Color: {myBike.Color}"); - - Console.WriteLine("\nBike Model Information"); - Console.WriteLine($" Model ID: {myBike.Model.Id}"); - Console.WriteLine($" Type: {myBike.Model.Type}"); - Console.WriteLine($" Wheel Size: {myBike.Model.WheelSize}"); - Console.WriteLine($" Max Passenger Weight: {myBike.Model.MaxСyclistWeight}"); - Console.WriteLine($" Weight: {myBike.Model.Weight}"); - Console.WriteLine($" Brake Type: {myBike.Model.BrakeType}"); - Console.WriteLine($" Year: {myBike.Model.YearOfManufacture}"); - Console.WriteLine($" Rent Price (per hour): {myBike.Model.RentPrice}"); - - Console.WriteLine($"Full Name: {renter1.FullName}"); - Console.WriteLine($"Phone Number: {renter1.PhoneNumber}"); - - Console.WriteLine("\nLease Agreement Information"); - Console.WriteLine($"Lease ID: {lease1.Id}"); - Console.WriteLine($"Renter: {lease1.Renter.FullName} (ID: {lease1.Renter.Id})"); - Console.WriteLine($"Bike Serial Number: {lease1.Bike.SerialNumber} (Color: {lease1.Bike.Color})"); - Console.WriteLine($"Rental Start Time: {lease1.RentalStartTime}"); - Console.WriteLine($"Rental Duration (hours): {lease1.RentalDuration}"); - Console.WriteLine($"Estimated Rental Cost: {lease1.RentalDuration * lease1.Bike.Model.RentPrice} (currency units)"); - - - Console.WriteLine("\nInitialization complete."); - } -} diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj index 13016642a..c360a8f56 100644 --- a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -11,4 +11,13 @@ + + + + + + + + + diff --git a/BikeRental/BikeRental.Tests/BikeRentalTests.cs b/BikeRental/BikeRental.Tests/BikeRentalTests.cs new file mode 100644 index 000000000..4bc2548b6 --- /dev/null +++ b/BikeRental/BikeRental.Tests/BikeRentalTests.cs @@ -0,0 +1,122 @@ +using BikeRental.Domain.Enum; + +namespace BikeRental.Tests; + +/// +/// +public class BikeRentalTests(RentalFixture fixture) : IClassFixture +{ + + ///Вывести информацию обо всех спортивных велосипедах. + /// + /// + [Fact] + public void InfoAboutSportBikes() + { + var expected = new List {3, 6, 9}; + + var actual = fixture.Bikes + .Where(b => b.Model.Type == BikeType.Sport) + .Select(b => b.Id) + .ToList(); + + Assert.Equal(expected, actual); + } + + ///Вывести топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). + /// + /// + [Fact] + public void TopFiveModelsIncome() + { + var expected = new List {5, 8, 6, 2, 9}; + + var actual = fixture.Lease + .GroupBy(lease => lease.Bike.Model.Id) + .Select(modelsGroup => new + { + ModelId = modelsGroup.Key, + SumOfIncomes = modelsGroup.Sum(lease => lease.Bike.Model.RentPrice * lease.RentalDuration) + }) + .OrderByDescending(models => models.SumOfIncomes) + .Select(models => models.ModelId) + .Take(5) + .ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void TopFiveModelsDuration() + { + var expected = new List {5, 8, 6, 2, 9}; + + var actual = fixture.Lease + .GroupBy(lease => lease.Bike.Model.Id) + .Select(modelsGroup => new + { + ModelId = modelsGroup.Key, + SumOfDurations = modelsGroup.Sum(lease => lease.RentalDuration) + }) + .OrderByDescending(models => models.SumOfDurations) + .Select(models => models.ModelId) + .Take(5) + .ToList(); + + Assert.Equal(expected, actual); + } + + + //Вывести информацию о минимальном, максимальном и среднем времени аренды велосипедов. + /// + /// + [Fact] + public void MinMaxAvgRental() + { + var expectedMinimum = 1; + var expectedMaximum = 8; + var expectedAverage = 4.3; + + var durations = fixture.Lease.Select(rent => rent.RentalDuration).ToList(); + + Assert.Equal(expectedMinimum, durations.Min()); + Assert.Equal(expectedMaximum, durations.Max()); + Assert.Equal(expectedAverage, durations.Average()); + } + + + ///Вывести суммарное время аренды велосипедов каждого типа. + /// + /// + [Theory] + [InlineData(BikeType.Road, 11)] + [InlineData(BikeType.Sport, 9)] + [InlineData(BikeType.Mountain, 8)] + [InlineData(BikeType.Hybrid, 15)] + + public void TotalRentalTimeByType(BikeType type, int expected) + { + var actual = fixture.Lease + .Where(lease => lease.Bike.Model.Type == type) + .Sum(lease => lease.RentalDuration); + + Assert.Equal(expected, actual); + } + + /// Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз. + /// + /// + [Fact] + public void TopThreeRenters() + { + var expected = new List {1, 2, 6}; + + var actual = fixture.Lease + .GroupBy(lease => lease.Renter.Id) + .OrderByDescending(group => group.Count()) + .Select(group => group.Key) + .Take(3) + .ToList(); + + Assert.Equal(expected, actual); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index 60da15134..fce97d144 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -25,68 +25,73 @@ public class RentalFixture /// public readonly List Lease; - + /// + /// TODO + /// public RentalFixture() { Models = GetBikeModels(); Renters = GetRenters(); Bikes = GetBikes(Models); - Lease = GetRentals(Bikes, Renters); + Lease = GetLeases(Bikes, Renters); } - + + /// + /// TODO + /// private List GetBikeModels() => [ - new() { Id = Guid.NewGuid(), Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 27, MaxСyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 29, MaxСyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Hybrid, WheelSize = 26, MaxСyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Mountain, WheelSize = 27, MaxСyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Hybrid, WheelSize = 29, MaxСyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Sport, WheelSize = 26, MaxСyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, - new() { Id = Guid.NewGuid(), Type = BikeType.Road, WheelSize = 28, MaxСyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, + new() { Id = 2, Type = BikeType.Road, WheelSize = 27, MaxСyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, + new() { Id = 3, Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, + new() { Id = 4, Type = BikeType.Road, WheelSize = 29, MaxСyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, + new() { Id = 5, Type = BikeType.Hybrid, WheelSize = 26, MaxСyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, + new() { Id = 6, Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, + new() { Id = 7, Type = BikeType.Mountain, WheelSize = 27, MaxСyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, + new() { Id = 8, Type = BikeType.Hybrid, WheelSize = 29, MaxСyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, + new() { Id = 9, Type = BikeType.Sport, WheelSize = 26, MaxСyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, + new() { Id = 10, Type = BikeType.Road, WheelSize = 28, MaxСyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, ]; private List GetRenters() => [ - new() { Id = Guid.NewGuid(), FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, - new() { Id = Guid.NewGuid(), FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, - new() { Id = Guid.NewGuid(), FullName = "Григорьев Григорий", PhoneNumber = "+7 934 567 89 01" }, - new() { Id = Guid.NewGuid(), FullName = "Дмитриева Ольга", PhoneNumber = "+7 945 678 90 12" }, - new() { Id = Guid.NewGuid(), FullName = "Николаева Светлана", PhoneNumber = "+7 956 789 01 23" }, - new() { Id = Guid.NewGuid(), FullName = "Михайлов Сергей", PhoneNumber = "+7 967 890 12 34" }, - new() { Id = Guid.NewGuid(), FullName = "Романова Татьяна", PhoneNumber = "+7 978 901 23 45" }, - new() { Id = Guid.NewGuid(), FullName = "Павлов Дмитрий", PhoneNumber = "+7 989 012 34 56" }, - new() { Id = Guid.NewGuid(), FullName = "Фёдорова Екатерина", PhoneNumber = "+7 990 123 45 67" }, - new() { Id = Guid.NewGuid(), FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" }, + new() { Id = 1, FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, + new() { Id = 2, FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, + new() { Id = 3, FullName = "Григорьев Григорий", PhoneNumber = "+7 934 567 89 01" }, + new() { Id = 4, FullName = "Дмитриева Ольга", PhoneNumber = "+7 945 678 90 12" }, + new() { Id = 5, FullName = "Николаева Светлана", PhoneNumber = "+7 956 789 01 23" }, + new() { Id = 6, FullName = "Михайлов Сергей", PhoneNumber = "+7 967 890 12 34" }, + new() { Id = 7, FullName = "Романова Татьяна", PhoneNumber = "+7 978 901 23 45" }, + new() { Id = 8, FullName = "Павлов Дмитрий", PhoneNumber = "+7 989 012 34 56" }, + new() { Id = 9, FullName = "Фёдорова Екатерина", PhoneNumber = "+7 990 123 45 67" }, + new() { Id = 10, FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" }, ]; private List GetBikes(List models) => [ - new() { Id = Guid.NewGuid(), SerialNumber = "R001", Color = "Silver", Model = models[0] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R002", Color = "Navy", Model = models[1] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R003", Color = "Charcoal", Model = models[2] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R004", Color = "Beige", Model = models[3] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R005", Color = "Burgundy", Model = models[4] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R006", Color = "Teal", Model = models[5] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R007", Color = "Coral", Model = models[6] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R008", Color = "Indigo", Model = models[7] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R009", Color = "Bronze", Model = models[8] }, - new() { Id = Guid.NewGuid(), SerialNumber = "R010", Color = "Lavender", Model = models[9] }, + new() { Id = 1, SerialNumber = "R001", Color = "Silver", Model = models[0] }, + new() { Id = 2, SerialNumber = "R002", Color = "Navy", Model = models[1] }, + new() { Id = 3, SerialNumber = "R003", Color = "Charcoal", Model = models[2] }, + new() { Id = 4, SerialNumber = "R004", Color = "Beige", Model = models[3] }, + new() { Id = 5, SerialNumber = "R005", Color = "Burgundy", Model = models[4] }, + new() { Id = 6, SerialNumber = "R006", Color = "Teal", Model = models[5] }, + new() { Id = 7, SerialNumber = "R007", Color = "Coral", Model = models[6] }, + new() { Id = 8, SerialNumber = "R008", Color = "Indigo", Model = models[7] }, + new() { Id = 9, SerialNumber = "R009", Color = "Bronze", Model = models[8] }, + new() { Id = 10, SerialNumber = "R010", Color = "Lavender", Model = models[9] }, ]; - private List GetRentals(List bikes, List renters) => + private List GetLeases(List bikes, List renters) => [ - new() { Id = Guid.NewGuid(), Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, - new() { Id = Guid.NewGuid(), Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, - new() { Id = Guid.NewGuid(), Bike = bikes[2], Renter = renters[2], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, - new() { Id = Guid.NewGuid(), Bike = bikes[3], Renter = renters[3], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, - new() { Id = Guid.NewGuid(), Bike = bikes[4], Renter = renters[4], RentalStartTime = DateTime.Now.AddHours(-20), RentalDuration = 8 }, - new() { Id = Guid.NewGuid(), Bike = bikes[5], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-3), RentalDuration = 1 }, - new() { Id = Guid.NewGuid(), Bike = bikes[6], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-18), RentalDuration = 5 }, - new() { Id = Guid.NewGuid(), Bike = bikes[7], Renter = renters[7], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, - new() { Id = Guid.NewGuid(), Bike = bikes[8], Renter = renters[8], RentalStartTime = DateTime.Now.AddHours(-10), RentalDuration = 4 }, - new() { Id = Guid.NewGuid(), Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 3 }, + new() { Id = 1, Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, + new() { Id = 2, Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, + new() { Id = 3, Bike = bikes[2], Renter = renters[2], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, + new() { Id = 4, Bike = bikes[3], Renter = renters[3], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, + new() { Id = 5, Bike = bikes[4], Renter = renters[4], RentalStartTime = DateTime.Now.AddHours(-20), RentalDuration = 8 }, + new() { Id = 6, Bike = bikes[5], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-3), RentalDuration = 1 }, + new() { Id = 7, Bike = bikes[6], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-18), RentalDuration = 5 }, + new() { Id = 8, Bike = bikes[7], Renter = renters[7], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, + new() { Id = 9, Bike = bikes[8], Renter = renters[8], RentalStartTime = DateTime.Now.AddHours(-10), RentalDuration = 4 }, + new() { Id = 10, Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 3 }, ]; } \ No newline at end of file From c8836bd37e5845f7e08c0950dce93098f5b351e7 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:23:08 +0400 Subject: [PATCH 04/48] fixed dotnet-tests.yml --- .github/workflows/dotnet-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 927a854a2..f0b2d36dd 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -17,6 +17,7 @@ jobs: - name: Run tests directly run: | + cd BikeRental cd BikeRental.Tests dotnet restore dotnet build From 7805b4ee9458152a39d920216138185142dfa636 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:28:40 +0400 Subject: [PATCH 05/48] fixed dotnet-tests.yml 2 --- .github/workflows/dotnet-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index f0b2d36dd..185a09128 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -17,8 +17,7 @@ jobs: - name: Run tests directly run: | - cd BikeRental - cd BikeRental.Tests + cd BikeRental/BikeRental.Tests dotnet restore dotnet build dotnet test --verbosity normal From e2b3998c01c4879a6bd80d781a0507cc35554c59 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:32:55 +0400 Subject: [PATCH 06/48] fixed dotnet-tests.yml - 3 --- .github/workflows/dotnet-tests.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 185a09128..9d35a2bed 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -14,10 +14,14 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore BikeRental/BikeRental.sln - - name: Run tests directly - run: | - cd BikeRental/BikeRental.Tests - dotnet restore - dotnet build - dotnet test --verbosity normal + - name: Build + run: dotnet build --no-restore --configuration Release BikeRental/BikeRental.sln + + - name: Run tests + run: dotnet test BikeRental/BikeRental.Tests/BikeRental.Tests.csproj --no-build --configuration Release + + \ No newline at end of file From 9b7be37b578b3da2ed35e23225dbc58147274375 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:40:20 +0400 Subject: [PATCH 07/48] delete "Exe" in BikeRental.Domain.csproj --- BikeRental/BikeRental.Domain/BikeRental.Domain.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj index 2f4fc7765..3a6353295 100644 --- a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj +++ b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable From 14ac949b902fd7a632b6a9ac5bf125443cdb33ba Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:52:42 +0400 Subject: [PATCH 08/48] added summary in tests --- .../BikeRental.Tests/BikeRentalTests.cs | 19 ++--- BikeRental/BikeRental.Tests/Printer.cs | 70 ------------------- 2 files changed, 11 insertions(+), 78 deletions(-) delete mode 100644 BikeRental/BikeRental.Tests/Printer.cs diff --git a/BikeRental/BikeRental.Tests/BikeRentalTests.cs b/BikeRental/BikeRental.Tests/BikeRentalTests.cs index 4bc2548b6..4eb321710 100644 --- a/BikeRental/BikeRental.Tests/BikeRentalTests.cs +++ b/BikeRental/BikeRental.Tests/BikeRentalTests.cs @@ -3,12 +3,13 @@ namespace BikeRental.Tests; /// +/// Class for unit-tests /// public class BikeRentalTests(RentalFixture fixture) : IClassFixture { - - ///Вывести информацию обо всех спортивных велосипедах. + /// + /// Displays information about all sports bikes /// [Fact] public void InfoAboutSportBikes() @@ -23,8 +24,8 @@ public void InfoAboutSportBikes() Assert.Equal(expected, actual); } - ///Вывести топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). /// + /// Displays the top 5 bike models ranked by rental revenue /// [Fact] public void TopFiveModelsIncome() @@ -45,6 +46,9 @@ public void TopFiveModelsIncome() Assert.Equal(expected, actual); } + /// + /// Displays the top 5 bike models ranked by rental duration + /// + /// Displays information about the minimum, maximum, and average rental time /// [Fact] public void MinMaxAvgRental() @@ -84,8 +87,8 @@ public void MinMaxAvgRental() } - ///Вывести суммарное время аренды велосипедов каждого типа. /// + /// Displays the total rental time for each bike type /// [Theory] [InlineData(BikeType.Road, 11)] @@ -102,8 +105,8 @@ public void TotalRentalTimeByType(BikeType type, int expected) Assert.Equal(expected, actual); } - /// Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз. /// + /// Displays information about customers who have rented bikes the most times /// [Fact] public void TopThreeRenters() diff --git a/BikeRental/BikeRental.Tests/Printer.cs b/BikeRental/BikeRental.Tests/Printer.cs deleted file mode 100644 index cd9622a8d..000000000 --- a/BikeRental/BikeRental.Tests/Printer.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace BikeRental.Tests; - -/// -/// A class just for print testing data -/// -public class TestDataPrinter -{ - public static void Main(string[] args) - { - var printer = new TestDataPrinter(); - printer.PrintDataCheck(); - } - - public void PrintDataCheck() - { - var fixture = new RentalFixture(); - - Console.WriteLine("Модели"); - foreach (var model in fixture.Models) - { - Console.WriteLine("| {0,-40} | {1,-10} | {2,-10} | {3,-10} | {4,-10} | {5,-10} | {6,-5} | {7,-5} |", - model.Id, - model.Type, - model.WheelSize, - model.Weight, - model.MaxСyclistWeight, - model.BrakeType, - model.YearOfManufacture, - model.RentPrice); - } - - Console.WriteLine("\n"); - Console.WriteLine("Велики"); - foreach (var bike in fixture.Bikes) - { - Console.WriteLine("| {0,-40} | {1,-10} | {2,-10} | {3,-20} |", - bike.Id, - bike.SerialNumber, - bike.Color, - bike.Model?.Id.ToString()); - } - - Console.WriteLine("\n"); - Console.WriteLine("Арендатели"); - foreach (var renter in fixture.Renters) - { - Console.WriteLine("| {0,-40} | {1,-20} | {2,-20} |", - renter.Id, - renter.FullName, - renter.PhoneNumber); - } - - Console.WriteLine("\n"); - Console.WriteLine("Аренда"); - foreach (var rent in fixture.Lease) - { - Console.WriteLine("| {0,-40} | {1,-20} | {2,-20} | {3,-20} | {4,-5} |", - rent.Id, - rent.Bike?.Id.ToString(), - rent.Renter?.Id.ToString(), - rent.RentalStartTime.ToString("dd.MM.yyyy HH:mm"), - rent.RentalDuration); - } - - Console.WriteLine($"Модели: {fixture.Models.Count} шт."); - Console.WriteLine($"Арендаторы: {fixture.Renters.Count} шт."); - Console.WriteLine($"Велосипеды: {fixture.Bikes.Count} шт."); - Console.WriteLine($"Аренды: {fixture.Lease.Count} шт."); - } -} \ No newline at end of file From 2f034bd187b93e0a7f12ec3150cb2dd448584da8 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 03:56:09 +0400 Subject: [PATCH 09/48] delete Exe from BikeRental.Tests --- BikeRental/BikeRental.Tests/BikeRental.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj index c360a8f56..60c5fdcab 100644 --- a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable From 54600ad877fd65d3f8516ccc7c056016dbdf23a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A6=D1=8B=D0=B3=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=94?= =?UTF-8?q?=D0=B0=D1=80=D1=8C=D1=8F?= <135041193+tsda04@users.noreply.github.com> Date: Sat, 1 Nov 2025 04:25:04 +0400 Subject: [PATCH 10/48] Update README.md upd readme file --- README.md | 140 ++++++++---------------------------------------------- 1 file changed, 21 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 39c9a8443..1a8cb8bc2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) ## Задание ### Цель Реализация проекта сервисно-ориентированного приложения. - -### Задачи +### Предметная область «Пункт велопроката» +В базе пункта проката хранятся сведения о велосипедах, их арендаторах и выданных в аренду транспортных средствах. +Велосипед характеризуется серийным номером, моделью, цветом. +Модель велосипеда является справочником, содержащим сведения о типе велосипеда, размере колес, предельно допустимом весе пассажира, весе велосипеда, типе тормозов, модельном годе. Для каждой модели велосипеда указывается цена часа аренды. +Тип велосипеда является перечислением. +Арендатор характеризуется ФИО, телефоном. +При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах. +Используется в качестве контракта. +### Задание на лабораторную 1: * Реализация объектно-ориентированной модели данных, * Изучение реализации серверных приложений на базе WebAPI/OpenAPI, * Изучение работы с брокерами сообщений, @@ -14,123 +20,19 @@ * Повторение основ работы с системами контроля версий, * Unit-тестирование. -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. - -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма - -image1 - -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve -6. Прийти на занятие и защитить работу - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания +В рамках первой лабораторной работы была подготовлена структура классов, описывающая предметную область: +* класс Bike. Велосипед характеризуется серийным номером, моделью, цветом - поля: SerialNumber, Model, Color соответственно, а также уникальный индефикатор велосипеда - поле Id. +* класс BikeModel. Модель велосипеда является справочником, содержащим сведения о типе велосипеда, размере колес, предельно допустимом весе пассажира, весе велосипеда, типе тормозов, модельном годе и цены часа аренды - поля: Type, WheelSize, MaxСyclistWeight, Weight, BrakeType, YearOfManufacture,RentPrice соответственно, а также уникальный индефикатор модели - поле Id. +* класс Renter. Арендатор характеризуется ФИО, телефоном - поля: FullName, PhoneNumber соответственно, а также уникальный индефикатор арендатора - поле Id. +* класс Lease используется в качестве контракта. +В нем указан как адендатор (поле Renter), так и арендованый велосипед (поле Bike), а также уникальный индефикатор контракта аренды - поле Id. При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах - за это отвечают поля RentalStartTime и RentalDuration соответственно. -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл +Было включено 10 экземпляров каждого класса в датасид(RentalFixture) и реализованы **юнит-тесты**: +InfoAboutSportBikes - Выводит информацию обо всех спортивных велосипедах. +TopFiveModelsIncome и TopFiveModelsDuration - Выводят топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). +MinMaxAvgRental - Выводит информацию о минимальном, максимальном и среднем времени аренды велосипедов. +TotalRentalTimeByType - Выводит суммарное время аренды велосипедов каждого типа. +TopThreeRenters - Выводит информацию о клиентах, бравших велосипеды на прокат больше всего раз. -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. -## Вопросы и обратная связь по курсу -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas). From c1c431dbbda11445bb7b33d6bffe1eac03cb981a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A6=D1=8B=D0=B3=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=94?= =?UTF-8?q?=D0=B0=D1=80=D1=8C=D1=8F?= <135041193+tsda04@users.noreply.github.com> Date: Sat, 1 Nov 2025 04:26:52 +0400 Subject: [PATCH 11/48] Update README.md upd readme 2 --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1a8cb8bc2..a92ff9ee8 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,6 @@ При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах. Используется в качестве контракта. ### Задание на лабораторную 1: -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. В рамках первой лабораторной работы была подготовлена структура классов, описывающая предметную область: * класс Bike. Велосипед характеризуется серийным номером, моделью, цветом - поля: SerialNumber, Model, Color соответственно, а также уникальный индефикатор велосипеда - поле Id. @@ -28,11 +21,11 @@ В нем указан как адендатор (поле Renter), так и арендованый велосипед (поле Bike), а также уникальный индефикатор контракта аренды - поле Id. При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах - за это отвечают поля RentalStartTime и RentalDuration соответственно. Было включено 10 экземпляров каждого класса в датасид(RentalFixture) и реализованы **юнит-тесты**: -InfoAboutSportBikes - Выводит информацию обо всех спортивных велосипедах. -TopFiveModelsIncome и TopFiveModelsDuration - Выводят топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). -MinMaxAvgRental - Выводит информацию о минимальном, максимальном и среднем времени аренды велосипедов. -TotalRentalTimeByType - Выводит суммарное время аренды велосипедов каждого типа. -TopThreeRenters - Выводит информацию о клиентах, бравших велосипеды на прокат больше всего раз. +* InfoAboutSportBikes - Выводит информацию обо всех спортивных велосипедах. +* TopFiveModelsIncome и TopFiveModelsDuration - Выводят топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). +* MinMaxAvgRental - Выводит информацию о минимальном, максимальном и среднем времени аренды велосипедов. +* TotalRentalTimeByType - Выводит суммарное время аренды велосипедов каждого типа. +* TopThreeRenters - Выводит информацию о клиентах, бравших велосипеды на прокат больше всего раз. From 4002caac472712b3296a769e301c67f7768334cc Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 16:38:58 +0400 Subject: [PATCH 12/48] change type RentPrice --- BikeRental/BikeRental.Domain/Models/BikeModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs index a53402183..44712e7c7 100644 --- a/BikeRental/BikeRental.Domain/Models/BikeModel.cs +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -44,6 +44,6 @@ public class BikeModel /// /// Cost per hour rental /// - public required int RentPrice { get; set; } + public required decimal RentPrice { get; set; } } \ No newline at end of file From 43566fe539908e724d06e1f2306d4d6797296f11 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 17:12:50 +0400 Subject: [PATCH 13/48] replaced field with property --- BikeRental/BikeRental.Tests/RentalFixture.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index fce97d144..886484e98 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -23,10 +23,10 @@ public class RentalFixture /// /// A list of all leases /// - public readonly List Lease; + public List Lease { get; } /// - /// TODO + /// A class for creating the data for testing /// public RentalFixture() { @@ -36,9 +36,6 @@ public RentalFixture() Lease = GetLeases(Bikes, Renters); } - /// - /// TODO - /// private List GetBikeModels() => [ new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, From aa502c88fbc35356c580ce18164e8e9a686bd606 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 17:13:57 +0400 Subject: [PATCH 14/48] moved the Id to the top of the class --- BikeRental/BikeRental.Domain/Models/Lease.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index 271c76fff..099340117 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -5,6 +5,11 @@ namespace BikeRental.Domain.Models; ///
public class Lease { + /// + /// Lease ID + /// + public required int Id { get; set; } + /// /// Person who rents a bike /// @@ -14,11 +19,6 @@ public class Lease /// Bike for rent ///
public required Bike Bike { get; set; } - - /// - /// Lease ID - /// - public required int Id { get; set; } /// /// Rental start time From c2418d35b0d8b591f95550a3de42d359db108a18 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sat, 1 Nov 2025 17:20:03 +0400 Subject: [PATCH 15/48] made methods static --- BikeRental/BikeRental.Tests/RentalFixture.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index 886484e98..1c5175eaa 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -36,7 +36,7 @@ public RentalFixture() Lease = GetLeases(Bikes, Renters); } - private List GetBikeModels() => + private static List GetBikeModels() => [ new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, new() { Id = 2, Type = BikeType.Road, WheelSize = 27, MaxСyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, @@ -50,7 +50,7 @@ private List GetBikeModels() => new() { Id = 10, Type = BikeType.Road, WheelSize = 28, MaxСyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, ]; - private List GetRenters() => + private static List GetRenters() => [ new() { Id = 1, FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, new() { Id = 2, FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, @@ -64,7 +64,7 @@ private List GetRenters() => new() { Id = 10, FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" }, ]; - private List GetBikes(List models) => + private static List GetBikes(List models) => [ new() { Id = 1, SerialNumber = "R001", Color = "Silver", Model = models[0] }, new() { Id = 2, SerialNumber = "R002", Color = "Navy", Model = models[1] }, @@ -78,7 +78,7 @@ private List GetBikes(List models) => new() { Id = 10, SerialNumber = "R010", Color = "Lavender", Model = models[9] }, ]; - private List GetLeases(List bikes, List renters) => + private static List GetLeases(List bikes, List renters) => [ new() { Id = 1, Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, new() { Id = 2, Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, From 233f25fa51d45d6cb5c76bf2445ff1f06493c566 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 13 Nov 2025 14:36:32 +0400 Subject: [PATCH 16/48] fixed tests and data for tests --- BikeRental/BikeRental.Tests/BikeRentalTests.cs | 10 +++++----- BikeRental/BikeRental.Tests/RentalFixture.cs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BikeRental/BikeRental.Tests/BikeRentalTests.cs b/BikeRental/BikeRental.Tests/BikeRentalTests.cs index 4eb321710..32a3d6ef6 100644 --- a/BikeRental/BikeRental.Tests/BikeRentalTests.cs +++ b/BikeRental/BikeRental.Tests/BikeRentalTests.cs @@ -30,7 +30,7 @@ public void InfoAboutSportBikes() [Fact] public void TopFiveModelsIncome() { - var expected = new List {5, 8, 6, 2, 9}; + var expected = new List {5, 8, 2, 9, 3}; /// 9,7,3 have same result (= 60) var actual = fixture.Lease .GroupBy(lease => lease.Bike.Model.Id) @@ -52,7 +52,7 @@ public void TopFiveModelsIncome() [Fact] public void TopFiveModelsDuration() { - var expected = new List {5, 8, 6, 2, 9}; + var expected = new List {5, 8, 2, 7, 3}; var actual = fixture.Lease .GroupBy(lease => lease.Bike.Model.Id) @@ -77,7 +77,7 @@ public void MinMaxAvgRental() { var expectedMinimum = 1; var expectedMaximum = 8; - var expectedAverage = 4.3; + var expectedAverage = 4.4; var durations = fixture.Lease.Select(rent => rent.RentalDuration).ToList(); @@ -91,7 +91,7 @@ public void MinMaxAvgRental() /// Displays the total rental time for each bike type /// [Theory] - [InlineData(BikeType.Road, 11)] + [InlineData(BikeType.Road, 12)] [InlineData(BikeType.Sport, 9)] [InlineData(BikeType.Mountain, 8)] [InlineData(BikeType.Hybrid, 15)] @@ -111,7 +111,7 @@ public void TotalRentalTimeByType(BikeType type, int expected) [Fact] public void TopThreeRenters() { - var expected = new List {1, 2, 6}; + var expected = new List {6, 7, 1}; var actual = fixture.Lease .GroupBy(lease => lease.Renter.Id) diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index 1c5175eaa..629e44aac 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -82,13 +82,13 @@ private static List GetLeases(List bikes, List renters) => [ new() { Id = 1, Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, new() { Id = 2, Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, - new() { Id = 3, Bike = bikes[2], Renter = renters[2], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, - new() { Id = 4, Bike = bikes[3], Renter = renters[3], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, + new() { Id = 3, Bike = bikes[2], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, + new() { Id = 4, Bike = bikes[3], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, new() { Id = 5, Bike = bikes[4], Renter = renters[4], RentalStartTime = DateTime.Now.AddHours(-20), RentalDuration = 8 }, new() { Id = 6, Bike = bikes[5], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-3), RentalDuration = 1 }, new() { Id = 7, Bike = bikes[6], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-18), RentalDuration = 5 }, - new() { Id = 8, Bike = bikes[7], Renter = renters[7], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, + new() { Id = 8, Bike = bikes[7], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, new() { Id = 9, Bike = bikes[8], Renter = renters[8], RentalStartTime = DateTime.Now.AddHours(-10), RentalDuration = 4 }, - new() { Id = 10, Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 3 }, + new() { Id = 10, Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 4 }, ]; } \ No newline at end of file From ac5681ce31e73c07798da3c9523ec3dd08d44094 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Fri, 14 Nov 2025 13:08:19 +0400 Subject: [PATCH 17/48] changed BikeRental.Tests.csproj to run the test --- BikeRental/BikeRental.Tests/BikeRental.Tests.csproj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj index 60c5fdcab..673dee290 100644 --- a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -15,8 +15,12 @@ - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From 6e278669411f06a7fd3d1b359bf3497deb21e5b7 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Fri, 14 Nov 2025 13:12:54 +0400 Subject: [PATCH 18/48] added PR trigger to test workflow --- .github/workflows/dotnet-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 9d35a2bed..4f359414d 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -2,6 +2,8 @@ name: dotnet-tests.yml on: push: branches: [ "main" ] + pull_request: + branches: [ "main" ] jobs: test: runs-on: ubuntu-latest From 294c609051871ef692ebf28105197ec1e61fb7a6 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Tue, 9 Dec 2025 20:38:29 +0400 Subject: [PATCH 19/48] created the project structure --- .../BikeRental.Api/BikeRental.Api.csproj | 14 +++++++++++ BikeRental/BikeRental.Api/Program.cs | 3 +++ .../BikeRental.Application.Contracts.csproj | 14 +++++++++++ .../Program.cs | 3 +++ .../BikeRental.Application.csproj | 16 +++++++++++++ BikeRental/BikeRental.Application/Program.cs | 3 +++ .../BikeRental.Domain.csproj | 4 ++++ .../BikeRental.Infrastructure.csproj | 14 +++++++++++ .../BikeRental.Infrastructure/Program.cs | 3 +++ BikeRental/BikeRental.sln | 24 +++++++++++++++++++ 10 files changed, 98 insertions(+) create mode 100644 BikeRental/BikeRental.Api/BikeRental.Api.csproj create mode 100644 BikeRental/BikeRental.Api/Program.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj create mode 100644 BikeRental/BikeRental.Application.Contracts/Program.cs create mode 100644 BikeRental/BikeRental.Application/BikeRental.Application.csproj create mode 100644 BikeRental/BikeRental.Application/Program.cs create mode 100644 BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj create mode 100644 BikeRental/BikeRental.Infrastructure/Program.cs diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj new file mode 100644 index 000000000..dad2ab4e6 --- /dev/null +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/BikeRental/BikeRental.Api/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj new file mode 100644 index 000000000..2ec2f2c0b --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/BikeRental/BikeRental.Application.Contracts/Program.cs b/BikeRental/BikeRental.Application.Contracts/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj new file mode 100644 index 000000000..a61c24b06 --- /dev/null +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/BikeRental/BikeRental.Application/Program.cs b/BikeRental/BikeRental.Application/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/BikeRental/BikeRental.Application/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj index 3a6353295..dcb54ff5f 100644 --- a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj +++ b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj new file mode 100644 index 000000000..5d60262f2 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/BikeRental/BikeRental.Infrastructure/Program.cs b/BikeRental/BikeRental.Infrastructure/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.sln b/BikeRental/BikeRental.sln index 2f5161042..61720c520 100644 --- a/BikeRental/BikeRental.sln +++ b/BikeRental/BikeRental.sln @@ -4,6 +4,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Domain", "BikeRe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Tests", "BikeRental.Tests\BikeRental.Tests.csproj", "{383A3622-AE13-45D6-88D3-E8559613718E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Application", "BikeRental.Application\BikeRental.Application.csproj", "{B68A0E95-AEB2-4C87-A527-9F89EB8B0032}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Api", "BikeRental.Api\BikeRental.Api.csproj", "{F6A23387-A682-4FFC-A33F-E6354A4839CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Infrastructure", "BikeRental.Infrastructure\BikeRental.Infrastructure.csproj", "{1EABEEC1-1941-44AC-B46F-42CD2381899C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Application.Contracts", "BikeRental.Application.Contracts\BikeRental.Application.Contracts.csproj", "{A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +26,21 @@ Global {383A3622-AE13-45D6-88D3-E8559613718E}.Debug|Any CPU.Build.0 = Debug|Any CPU {383A3622-AE13-45D6-88D3-E8559613718E}.Release|Any CPU.ActiveCfg = Release|Any CPU {383A3622-AE13-45D6-88D3-E8559613718E}.Release|Any CPU.Build.0 = Release|Any CPU + {B68A0E95-AEB2-4C87-A527-9F89EB8B0032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B68A0E95-AEB2-4C87-A527-9F89EB8B0032}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B68A0E95-AEB2-4C87-A527-9F89EB8B0032}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B68A0E95-AEB2-4C87-A527-9F89EB8B0032}.Release|Any CPU.Build.0 = Release|Any CPU + {F6A23387-A682-4FFC-A33F-E6354A4839CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6A23387-A682-4FFC-A33F-E6354A4839CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6A23387-A682-4FFC-A33F-E6354A4839CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6A23387-A682-4FFC-A33F-E6354A4839CA}.Release|Any CPU.Build.0 = Release|Any CPU + {1EABEEC1-1941-44AC-B46F-42CD2381899C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EABEEC1-1941-44AC-B46F-42CD2381899C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EABEEC1-1941-44AC-B46F-42CD2381899C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EABEEC1-1941-44AC-B46F-42CD2381899C}.Release|Any CPU.Build.0 = Release|Any CPU + {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From cfd5bf5c84f03bdee769e30f468f4b13bfe6577f Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Tue, 9 Dec 2025 22:43:45 +0400 Subject: [PATCH 20/48] created dto --- .../BikeRental.Application.Contracts.csproj | 3 +- .../Dtos/BikeCreateUpdateDto.cs | 20 ++++++++ .../Dtos/BikeDto.cs | 30 ++++++++++++ .../Dtos/BikeModelCreateUpdateDto.cs | 41 ++++++++++++++++ .../Dtos/BikeModelDto.cs | 49 +++++++++++++++++++ .../Dtos/LeaseCreateUpdateDto.cs | 24 +++++++++ .../Dtos/LeaseDto.cs | 36 ++++++++++++++ .../Dtos/RenterCreateUpdateDto.cs | 14 ++++++ .../Dtos/RenterDto.cs | 22 +++++++++ BikeRental/BikeRental.Domain/Models/Renter.cs | 2 +- 10 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs create mode 100644 BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj index 2ec2f2c0b..73d1895c6 100644 --- a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -8,7 +8,6 @@ - + - diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs new file mode 100644 index 000000000..2da728fdf --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs @@ -0,0 +1,20 @@ +namespace BikeRental.Application.Contracts.Dtos; + +public class BikeCreateUpdateDto +{ + /// + /// Bike's serial number + /// + public required string SerialNumber { get; set; } + + /// + /// Bike's color + /// + public required string Color { get; set; } + + /// + /// Bike's model + /// + public required int ModelId { get; set; } + +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs new file mode 100644 index 000000000..18e752396 --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs @@ -0,0 +1,30 @@ +namespace BikeRental.Application.Contracts.Dtos; + +/// +/// A class describing a bike for rent +/// +public class BikeDto +{ + /// + /// Bike's unique id + /// + public required int Id { get; set; } + + /// + /// Bike's serial number + /// + public required string SerialNumber { get; set; } + + /// + /// Bike's color + /// + public required string Color { get; set; } + + /// + /// Bike's model + /// + public required int ModelId { get; set; } + + // model information + //public BikeModelDto? Model { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs new file mode 100644 index 000000000..6023ba20d --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs @@ -0,0 +1,41 @@ +using BikeRental.Domain.Enum; + +namespace BikeRental.Application.Contracts.Dtos; + +public class BikeModelCreateUpdateDto +{ + /// + /// The type of bicycle: road, sport, mountain, hybrid + /// + public required BikeType Type { get; set; } + + /// + /// The size of the bicycle's wheels + /// + public required int WheelSize { get; set; } + + /// + /// Maximum permissible cyclist weight + /// + public required int MaxСyclistWeight { get; set; } + + /// + /// Weight of the bike model + /// + public required double Weight { get; set; } + + /// + /// The type of braking system used in this model of bike + /// + public required string BrakeType { get; set; } + + /// + /// Year of manufacture of the bicycle model + /// + public required string YearOfManufacture { get; set; } + + /// + /// Cost per hour rental + /// + public required decimal RentPrice { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs new file mode 100644 index 000000000..fff1780b3 --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs @@ -0,0 +1,49 @@ +using BikeRental.Domain.Enum; + +namespace BikeRental.Application.Contracts.Dtos; + +/// +/// A class describing the models of bikes that can be rented +/// +public class BikeModelDto +{ + /// + /// The unique id for bike model + /// + public required int Id { get; set; } + + /// + /// The type of bicycle: road, sport, mountain, hybrid + /// + public required BikeType Type { get; set; } + + /// + /// The size of the bicycle's wheels + /// + public required int WheelSize { get; set; } + + /// + /// Maximum permissible cyclist weight + /// + public required int MaxСyclistWeight { get; set; } + + /// + /// Weight of the bike model + /// + public required double Weight { get; set; } + + /// + /// The type of braking system used in this model of bike + /// + public required string BrakeType { get; set; } + + /// + /// Year of manufacture of the bicycle model + /// + public required string YearOfManufacture { get; set; } + + /// + /// Cost per hour rental + /// + public required decimal RentPrice { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs new file mode 100644 index 000000000..5dea42c9b --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs @@ -0,0 +1,24 @@ +namespace BikeRental.Application.Contracts.Dtos; + +public class LeaseCreateUpdateDto +{ + /// + /// Person who rents a bike + /// + public required int RenterId { get; set; } + + /// + /// Bike for rent + /// + public required int BikeId { get; set; } + + /// + /// Rental start time + /// + public required DateTime RentalStartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int RentalDuration { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs new file mode 100644 index 000000000..b8d7ace58 --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs @@ -0,0 +1,36 @@ +namespace BikeRental.Application.Contracts.Dtos; + +/// +/// A class describing a lease agreement +/// +public class LeaseDto +{ + /// + /// Lease ID + /// + public required int Id { get; set; } + + /// + /// Person who rents a bike + /// + public required int RenterId { get; set; } + + /// + /// Bike for rent + /// + public required int BikeId { get; set; } + + /// + /// Rental start time + /// + public required DateTime RentalStartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int RentalDuration { get; set; } + + //public RenterDto? Renter { get; set; } + //public BikeDto? Bike { get; set; } + +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs new file mode 100644 index 000000000..13683a2b4 --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs @@ -0,0 +1,14 @@ +namespace BikeRental.Application.Contracts.Dtos; + +public class RenterCreateUpdateDto +{ + /// + /// Renter's full name + /// + public required string FullName { get; set; } + + /// + /// Renter's phone number + /// + public required string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs new file mode 100644 index 000000000..6fc6ee42b --- /dev/null +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs @@ -0,0 +1,22 @@ +namespace BikeRental.Application.Contracts.Dtos; + +/// +/// A class describing a renter +/// +public class RenterDto +{ + /// + /// Renter's id + /// + public required int Id { get; set; } + + /// + /// Renter's full name + /// + public required string FullName { get; set; } + + /// + /// Renter's phone number + /// + public required string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs index 7c89426d8..53f5ca0b5 100644 --- a/BikeRental/BikeRental.Domain/Models/Renter.cs +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -1,7 +1,7 @@ namespace BikeRental.Domain.Models; /// -/// A class describing a bike for rent +/// A class describing a renter /// public class Renter { From 74f55af62049fd7d07c1a381e3454889a6d71149 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Tue, 9 Dec 2025 23:43:12 +0400 Subject: [PATCH 21/48] created service interfaces --- .../BikeRental.Application.csproj | 7 +++- .../Interfaces/IBikeModelService.cs | 32 +++++++++++++++++++ .../Interfaces/IBikeService.cs | 16 ++++++++++ .../Interfaces/ILeaseService.cs | 15 +++++++++ .../Interfaces/IRenterService.cs | 15 +++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs create mode 100644 BikeRental/BikeRental.Application/Interfaces/IBikeService.cs create mode 100644 BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs create mode 100644 BikeRental/BikeRental.Application/Interfaces/IRenterService.cs diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj index a61c24b06..04ccb6ba7 100644 --- a/BikeRental/BikeRental.Application/BikeRental.Application.csproj +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -8,9 +8,14 @@ - + + + + + + diff --git a/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs b/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs new file mode 100644 index 000000000..1e1e3f4ec --- /dev/null +++ b/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs @@ -0,0 +1,32 @@ +using BikeRental.Application.Contracts.Dtos; + +namespace BikeRental.Application.Interfaces; + +public interface IBikeModelService +{ + /// + /// Returns all bike models. + /// + public Task> GetAll(); + + /// + /// Returns a bike model by id. + /// + public Task GetById(int id); + + /// + /// Creates a new bike model. + /// + public Task Create(BikeModelCreateUpdateDto dto); + + /// + /// Updates an existing bike model. + /// + public Task Update(int id, BikeModelCreateUpdateDto dto); + + /// + /// Deletes a bike model. + /// + public Task Delete(int id); + +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs b/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs new file mode 100644 index 000000000..10b453324 --- /dev/null +++ b/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs @@ -0,0 +1,16 @@ +using BikeRental.Application.Contracts.Dtos; + +namespace BikeRental.Application.Interfaces; + +/// +/// Service for managing bikes. +/// +public interface IBikeService +{ + public Task> GetAll(); + public Task GetById(int id); + public Task Create(BikeCreateUpdateDto dto); + public Task Update(int id, BikeCreateUpdateDto dto); + public Task Delete(int id); + +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs b/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs new file mode 100644 index 000000000..e2e0a6ed6 --- /dev/null +++ b/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs @@ -0,0 +1,15 @@ +using BikeRental.Application.Contracts.Dtos; + +namespace BikeRental.Application.Interfaces; + +/// +/// Service for managing bike leases. +/// +public interface ILeaseService +{ + public Task> GetAll(); + public Task GetById(int id); + public Task Create(LeaseCreateUpdateDto dto); + public Task Update(int id, LeaseCreateUpdateDto dto); + public Task Delete(int id); +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs b/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs new file mode 100644 index 000000000..49d4c19d2 --- /dev/null +++ b/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs @@ -0,0 +1,15 @@ +using BikeRental.Application.Contracts.Dtos; + +namespace BikeRental.Application.Interfaces; + +/// +/// Service for managing renters. +/// +public interface IRenterService +{ + public Task> GetAll(); + public Task GetById(int id); + public Task Create(RenterCreateUpdateDto dto); + public Task Update(int id, RenterCreateUpdateDto dto); + public Task Delete(int id); +} \ No newline at end of file From 34548e8dfc0c6198939fc37130e9567946df761c Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Tue, 9 Dec 2025 23:49:17 +0400 Subject: [PATCH 22/48] cleaned directories --- .../BikeRental.Application.Contracts.csproj | 1 - BikeRental/BikeRental.Application.Contracts/Program.cs | 3 --- .../BikeRental.Application/BikeRental.Application.csproj | 1 - BikeRental/BikeRental.Application/Program.cs | 3 --- .../BikeRental.Infrastructure/BikeRental.Infrastructure.csproj | 1 - BikeRental/BikeRental.Infrastructure/Program.cs | 3 --- 6 files changed, 12 deletions(-) delete mode 100644 BikeRental/BikeRental.Application.Contracts/Program.cs delete mode 100644 BikeRental/BikeRental.Application/Program.cs delete mode 100644 BikeRental/BikeRental.Infrastructure/Program.cs diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj index 73d1895c6..44ea37af1 100644 --- a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable diff --git a/BikeRental/BikeRental.Application.Contracts/Program.cs b/BikeRental/BikeRental.Application.Contracts/Program.cs deleted file mode 100644 index e5dff12bc..000000000 --- a/BikeRental/BikeRental.Application.Contracts/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj index 04ccb6ba7..a8fef2faf 100644 --- a/BikeRental/BikeRental.Application/BikeRental.Application.csproj +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable diff --git a/BikeRental/BikeRental.Application/Program.cs b/BikeRental/BikeRental.Application/Program.cs deleted file mode 100644 index e5dff12bc..000000000 --- a/BikeRental/BikeRental.Application/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj index 5d60262f2..4933dbea9 100644 --- a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable diff --git a/BikeRental/BikeRental.Infrastructure/Program.cs b/BikeRental/BikeRental.Infrastructure/Program.cs deleted file mode 100644 index e5dff12bc..000000000 --- a/BikeRental/BikeRental.Infrastructure/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -Console.WriteLine("Hello, World!"); \ No newline at end of file From faadb4b1691a6e4323a03ca55980491d1b150014 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sun, 14 Dec 2025 23:46:36 +0400 Subject: [PATCH 23/48] created IRepository, changed .csproj --- .../BikeRental.Domain.csproj | 4 --- .../Interfaces/IRepository.cs | 34 +++++++++++++++++++ .../BikeRental.Infrastructure.csproj | 3 +- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 BikeRental/BikeRental.Domain/Interfaces/IRepository.cs diff --git a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj index dcb54ff5f..3a6353295 100644 --- a/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj +++ b/BikeRental/BikeRental.Domain/BikeRental.Domain.csproj @@ -6,8 +6,4 @@ enable - - - - diff --git a/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs new file mode 100644 index 000000000..b9fa0bf4e --- /dev/null +++ b/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs @@ -0,0 +1,34 @@ +namespace BikeRental.Domain.Interfaces; +/// +/// Generic repository interface that defines basic CRUD operations. +/// +public interface IRepository + where TEntity : class +{ + /// + /// Returns all entities. + /// + public Task> GetAll(); + + /// + /// Returns entity by id. + /// + public Task GetById(int id); + + /// + /// Adds a new entity and returns its generated id. + /// + public Task Add(TEntity entity); + + /// + /// Updates existing entity. + /// + public Task Update(TEntity entity); + + /// + /// Deletes existing entity. + /// + public Task Delete(TEntity entity); +} + + \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj index 4933dbea9..407a208ad 100644 --- a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -7,7 +7,8 @@ - + + From 28c1c11c39318c7847f0ac966adb0c29a0b87338 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Wed, 17 Dec 2025 18:00:12 +0400 Subject: [PATCH 24/48] added core infrastructure and application services, several changes in domain entities --- .../Dtos/BikeModelCreateUpdateDto.cs | 2 +- .../BikeRental.Application.csproj | 5 - .../Mappings/BikeMappings.cs | 28 +++++ .../Mappings/BikeModelMappings.cs | 36 ++++++ .../Mappings/LeaseMappings.cs | 30 +++++ .../Mappings/RenterMappings.cs | 26 +++++ .../Services/BikeModelService.cs | 64 ++++++++++ .../Services/BikeService.cs | 63 ++++++++++ .../Services/LeaseService.cs | 88 ++++++++++++++ .../Services/RenterService.cs | 64 ++++++++++ .../Interfaces/IBikeModelRepository.cs | 10 ++ .../Interfaces/IBikeRepository.cs | 10 ++ .../Interfaces/ILeaseRepository.cs | 10 ++ .../Interfaces/IRenterRepository.cs | 10 ++ BikeRental/BikeRental.Domain/Models/Bike.cs | 11 +- .../BikeRental.Domain/Models/BikeModel.cs | 4 +- BikeRental/BikeRental.Domain/Models/Lease.cs | 2 +- BikeRental/BikeRental.Domain/Models/Renter.cs | 2 +- .../BikeRental.Infrastructure.csproj | 11 +- .../Database/ApplicationDbContext.cs | 41 +++++++ .../Configurations/BikeConfiguration.cs | 50 ++++++++ .../Configurations/BikeModelConfiguration.cs | 64 ++++++++++ .../Configurations/LeaseConfiguration.cs | 45 +++++++ .../Configurations/RenterConfiguration.cs | 41 +++++++ .../Repositories/BikeModelRepository.cs | 46 ++++++++ .../Repositories/BikeRepository.cs | 44 +++++++ .../Repositories/LeaseRepository.cs | 43 +++++++ .../Repositories/RenterRepository.cs | 44 +++++++ BikeRental/BikeRental.Tests/RentalFixture.cs | 110 ++++++++++++++---- 29 files changed, 969 insertions(+), 35 deletions(-) create mode 100644 BikeRental/BikeRental.Application/Mappings/BikeMappings.cs create mode 100644 BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs create mode 100644 BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs create mode 100644 BikeRental/BikeRental.Application/Mappings/RenterMappings.cs create mode 100644 BikeRental/BikeRental.Application/Services/BikeModelService.cs create mode 100644 BikeRental/BikeRental.Application/Services/BikeService.cs create mode 100644 BikeRental/BikeRental.Application/Services/LeaseService.cs create mode 100644 BikeRental/BikeRental.Application/Services/RenterService.cs create mode 100644 BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs create mode 100644 BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs create mode 100644 BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs create mode 100644 BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs index 6023ba20d..22b9f4732 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs @@ -17,7 +17,7 @@ public class BikeModelCreateUpdateDto /// /// Maximum permissible cyclist weight /// - public required int MaxСyclistWeight { get; set; } + public required int MaxCyclistWeight { get; set; } /// /// Weight of the bike model diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj index a8fef2faf..f639569fa 100644 --- a/BikeRental/BikeRental.Application/BikeRental.Application.csproj +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -6,11 +6,6 @@ enable - - - - - diff --git a/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs new file mode 100644 index 000000000..4236bfe72 --- /dev/null +++ b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs @@ -0,0 +1,28 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Domain.Models; + +namespace BikeRental.Application.Mappings; + +internal static class BikeMappings +{ + public static BikeDto ToDto(this Bike entity) + { + return new BikeDto + { + Id = entity.Id, + SerialNumber = entity.SerialNumber, + Color = entity.Color, + ModelId = entity.ModelId + }; + } + + public static Bike ToEntity(this BikeCreateUpdateDto dto) + { + return new Bike + { + SerialNumber = dto.SerialNumber, + Color = dto.Color, + ModelId = dto.ModelId + }; + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs b/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs new file mode 100644 index 000000000..25dc3f23b --- /dev/null +++ b/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs @@ -0,0 +1,36 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Domain.Models; + +namespace BikeRental.Application.Mappings; + +internal static class BikeModelMappings +{ + public static BikeModelDto ToDto(this BikeModel entity) + { + return new BikeModelDto + { + Id = entity.Id, + Type = entity.Type, + WheelSize = entity.WheelSize, + MaxСyclistWeight = entity.MaxCyclistWeight, + Weight = entity.Weight, + BrakeType = entity.BrakeType, + YearOfManufacture = entity.YearOfManufacture, + RentPrice = entity.RentPrice + }; + } + + public static BikeModel ToEntity(this BikeModelCreateUpdateDto dto) + { + return new BikeModel + { + Type = dto.Type, + WheelSize = dto.WheelSize, + MaxCyclistWeight = dto.MaxCyclistWeight, + Weight = dto.Weight, + BrakeType = dto.BrakeType, + YearOfManufacture = dto.YearOfManufacture, + RentPrice = dto.RentPrice + }; + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs new file mode 100644 index 000000000..fb415541b --- /dev/null +++ b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs @@ -0,0 +1,30 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Domain.Models; + +namespace BikeRental.Application.Mappings; + +internal static class LeaseMappings +{ + public static LeaseDto ToDto(this Lease entity) + { + return new LeaseDto + { + Id = entity.Id, + BikeId = entity.Bike.Id, + RenterId = entity.Renter.Id, + RentalStartTime = entity.RentalStartTime, + RentalDuration = entity.RentalDuration + }; + } + + public static Lease ToEntity(this LeaseCreateUpdateDto dto, Bike bike, Renter renter) + { + return new Lease + { + Bike = bike, + Renter = renter, + RentalStartTime = dto.RentalStartTime, + RentalDuration = dto.RentalDuration + }; + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs b/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs new file mode 100644 index 000000000..310577440 --- /dev/null +++ b/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs @@ -0,0 +1,26 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Domain.Models; + +namespace BikeRental.Application.Mappings; + +internal static class RenterMappings +{ + public static RenterDto ToDto(this Renter entity) + { + return new RenterDto + { + Id = entity.Id, + FullName = entity.FullName, + PhoneNumber = entity.PhoneNumber + }; + } + + public static Renter ToEntity(this RenterCreateUpdateDto dto) + { + return new Renter + { + FullName = dto.FullName, + PhoneNumber = dto.PhoneNumber + }; + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Services/BikeModelService.cs b/BikeRental/BikeRental.Application/Services/BikeModelService.cs new file mode 100644 index 000000000..8e241aa5b --- /dev/null +++ b/BikeRental/BikeRental.Application/Services/BikeModelService.cs @@ -0,0 +1,64 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using BikeRental.Application.Mappings; +using BikeRental.Domain.Interfaces; + +namespace BikeRental.Application.Services; + +/// +/// Application-сервис для работы с моделями велосипедов. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над IBikeModelRepository. +/// +public sealed class BikeModelService(IBikeModelRepository bikeModelRepository) : IBikeModelService +{ + public async Task> GetAll() + { + return (await bikeModelRepository.GetAll()). + Select(bm => bm.ToDto()); + } + + public async Task GetById(int id) + { + return (await bikeModelRepository.GetById(id))?.ToDto(); + } + + public async Task Create(BikeModelCreateUpdateDto dto) + { + var id = await bikeModelRepository.Add(dto.ToEntity()); + if (id > 0) + { + var createdEntity = await bikeModelRepository.GetById(id); + if (createdEntity != null) + { + return createdEntity.ToDto(); + } + } + throw new InvalidOperationException("Failed to create entity."); + } + + public async Task Update(int id, BikeModelCreateUpdateDto dto) + { + var createdEntity = await bikeModelRepository.GetById(id); + if (createdEntity == null) + { + throw new KeyNotFoundException($"Entity with id {id} not found."); + } + var entityToUpdate = dto.ToEntity(); + entityToUpdate.Id = id; + await bikeModelRepository.Update(entityToUpdate); + var updatedEntity = await bikeModelRepository.GetById(id); + return updatedEntity!.ToDto(); + } + + public async Task Delete(int id) + { + var entity = await bikeModelRepository.GetById(id); + if (entity == null) + { + return false; + } + await bikeModelRepository.Delete(entity); + return true; + } +} + diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs new file mode 100644 index 000000000..d898f3b23 --- /dev/null +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -0,0 +1,63 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using BikeRental.Application.Mappings; +using BikeRental.Domain.Interfaces; + +namespace BikeRental.Application.Services; + +/// +/// Application-сервис для работы с велосипедами. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над IBikeRepository. +/// +public sealed class BikeService(IBikeRepository bikeRepository) : IBikeService +{ + public async Task> GetAll() + { + return (await bikeRepository.GetAll()). + Select(b => b.ToDto()); + } + + public async Task GetById(int id) + { + return (await bikeRepository.GetById(id))?.ToDto(); + } + + public async Task Create(BikeCreateUpdateDto dto) + { + var id = await bikeRepository.Add(dto.ToEntity()); + if (id > 0) + { + var createdEntity = await bikeRepository.GetById(id); + if (createdEntity != null) + { + return createdEntity.ToDto(); + } + } + throw new InvalidOperationException("Failed to create entity."); + } + + public async Task Update(int id, BikeCreateUpdateDto dto) + { + var createdEntity = await bikeRepository.GetById(id); + if (createdEntity == null) + { + throw new KeyNotFoundException($"Entity with id {id} not found."); + } + var entityToUpdate = dto.ToEntity(); + entityToUpdate.Id = id; + await bikeRepository.Update(entityToUpdate); + var updatedEntity = await bikeRepository.GetById(id); + return updatedEntity!.ToDto(); + } + + public async Task Delete(int id) + { + var entity = await bikeRepository.GetById(id); + if (entity == null) + { + return false; + } + await bikeRepository.Delete(entity); + return true; + } +} diff --git a/BikeRental/BikeRental.Application/Services/LeaseService.cs b/BikeRental/BikeRental.Application/Services/LeaseService.cs new file mode 100644 index 000000000..25b864ee5 --- /dev/null +++ b/BikeRental/BikeRental.Application/Services/LeaseService.cs @@ -0,0 +1,88 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using BikeRental.Application.Mappings; +using BikeRental.Domain.Interfaces; + +namespace BikeRental.Application.Services; + +/// +/// Application-сервис для работы с договорами аренды велосипедов. +/// Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над ILeaseRepository. +/// +public sealed class LeaseService( + ILeaseRepository leaseRepository, + IBikeRepository bikeRepository, + IRenterRepository renterRepository) : ILeaseService +{ + public async Task> GetAll() + { + return (await leaseRepository.GetAll()). + Select(l => l.ToDto()); + } + + public async Task GetById(int id) + { + return (await leaseRepository.GetById(id))?.ToDto(); + } + + public async Task Create(LeaseCreateUpdateDto dto) + { + var bike = await bikeRepository.GetById(dto.BikeId); + if (bike == null) + { + throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + } + var renter = await renterRepository.GetById(dto.RenterId); + if (renter == null) + { + throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + } + var id = await leaseRepository.Add(dto.ToEntity(bike, renter)); + if (id > 0) + { + var createdEntity = await leaseRepository.GetById(id); + if (createdEntity != null) + { + return createdEntity.ToDto(); + } + } + throw new InvalidOperationException("Failed to create entity."); + } + + public async Task Update(int id, LeaseCreateUpdateDto dto) + { + var createdEntity = await leaseRepository.GetById(id); + if (createdEntity == null) + { + throw new KeyNotFoundException($"Entity with id {id} not found."); + } + var bike = await bikeRepository.GetById(dto.BikeId); + if (bike == null) + { + throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + } + var renter = await renterRepository.GetById(dto.RenterId); + if (renter == null) + { + throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + } + var entityToUpdate = dto.ToEntity(bike, renter); + entityToUpdate.Id = id; + await leaseRepository.Update(entityToUpdate); + var updatedEntity = await leaseRepository.GetById(id); + return updatedEntity!.ToDto(); + } + + public async Task Delete(int id) + { + var entity = await leaseRepository.GetById(id); + if (entity == null) + { + return false; + } + await leaseRepository.Delete(entity); + return true; + } +} + diff --git a/BikeRental/BikeRental.Application/Services/RenterService.cs b/BikeRental/BikeRental.Application/Services/RenterService.cs new file mode 100644 index 000000000..88bfd3cc2 --- /dev/null +++ b/BikeRental/BikeRental.Application/Services/RenterService.cs @@ -0,0 +1,64 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using BikeRental.Application.Mappings; +using BikeRental.Domain.Interfaces; + +namespace BikeRental.Application.Services; + +/// +/// Application-сервис для работы с арендаторами. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над IRenterRepository. +/// +public sealed class RenterService(IRenterRepository renterRepository) : IRenterService +{ + public async Task> GetAll() + { + return (await renterRepository.GetAll()). + Select(r => r.ToDto()); + } + + public async Task GetById(int id) + { + return (await renterRepository.GetById(id))?.ToDto(); + } + + public async Task Create(RenterCreateUpdateDto dto) + { + var id = await renterRepository.Add(dto.ToEntity()); + if (id > 0) + { + var createdEntity = await renterRepository.GetById(id); + if (createdEntity != null) + { + return createdEntity.ToDto(); + } + } + throw new InvalidOperationException("Failed to create entity."); + } + + public async Task Update(int id, RenterCreateUpdateDto dto) + { + var createdEntity = await renterRepository.GetById(id); + if (createdEntity == null) + { + throw new KeyNotFoundException($"Entity with id {id} not found."); + } + var entityToUpdate = dto.ToEntity(); + entityToUpdate.Id = id; + await renterRepository.Update(entityToUpdate); + var updatedEntity = await renterRepository.GetById(id); + return updatedEntity!.ToDto(); + } + + public async Task Delete(int id) + { + var entity = await renterRepository.GetById(id); + if (entity == null) + { + return false; + } + await renterRepository.Delete(entity); + return true; + } +} + diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs new file mode 100644 index 000000000..8865ea5de --- /dev/null +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs @@ -0,0 +1,10 @@ +using BikeRental.Domain.Models; + +namespace BikeRental.Domain.Interfaces; + +/// +/// Интерфейс репозитория описывает контракт для работы с моделями велосипедов +/// +public interface IBikeModelRepository : IRepository +{ +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs new file mode 100644 index 000000000..fc696a261 --- /dev/null +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs @@ -0,0 +1,10 @@ +using BikeRental.Domain.Models; + +namespace BikeRental.Domain.Interfaces; + +/// +/// Интерфейс репозитория описывает контракт для работы с велосипедами +/// +public interface IBikeRepository : IRepository +{ +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs new file mode 100644 index 000000000..843224e1a --- /dev/null +++ b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs @@ -0,0 +1,10 @@ +using BikeRental.Domain.Models; + +namespace BikeRental.Domain.Interfaces; + +/// +/// Интерфейс репозитория описывает контракт для работы с договорами на аренду велосипедов +/// +public interface ILeaseRepository : IRepository +{ +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs new file mode 100644 index 000000000..9a8627374 --- /dev/null +++ b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs @@ -0,0 +1,10 @@ +using BikeRental.Domain.Models; + +namespace BikeRental.Domain.Interfaces; + +/// +/// Интерфейс репозитория описывает контракт для работы с арендаторами +/// +public interface IRenterRepository : IRepository +{ +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs index b3452202c..27eb8fd11 100644 --- a/BikeRental/BikeRental.Domain/Models/Bike.cs +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace BikeRental.Domain.Models; /// @@ -8,7 +10,10 @@ public class Bike /// /// Bike's unique id /// - public required int Id { get; set; } + public int Id { get; set; } + + [ForeignKey(nameof(Model))] + public required int ModelId { get; set; } /// /// Bike's serial number @@ -23,5 +28,5 @@ public class Bike /// /// Bike's model /// - public required BikeModel Model { get; set; } -} + public virtual BikeModel Model { get; init; } = null!; +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs index 44712e7c7..8f17a6edb 100644 --- a/BikeRental/BikeRental.Domain/Models/BikeModel.cs +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -9,7 +9,7 @@ public class BikeModel /// /// The unique id for bike model /// - public required int Id { get; set; } + public int Id { get; set; } /// /// The type of bicycle: road, sport, mountain, hybrid @@ -24,7 +24,7 @@ public class BikeModel /// /// Maximum permissible cyclist weight /// - public required int MaxСyclistWeight { get; set; } + public required int MaxCyclistWeight { get; set; } /// /// Weight of the bike model diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index 099340117..81fabb03f 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -8,7 +8,7 @@ public class Lease /// /// Lease ID /// - public required int Id { get; set; } + public int Id { get; set; } /// /// Person who rents a bike diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs index 53f5ca0b5..cb8d9612f 100644 --- a/BikeRental/BikeRental.Domain/Models/Renter.cs +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -8,7 +8,7 @@ public class Renter /// /// Renter's id /// - public required int Id { get; set; } + public int Id { get; set; } /// /// Renter's full name diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj index 407a208ad..041ca759f 100644 --- a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -7,8 +7,15 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs b/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs new file mode 100644 index 000000000..0e3237aa1 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs @@ -0,0 +1,41 @@ +using BikeRental.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Database; + +/// +/// Контекст базы данных приложения +/// +/// Для создания миграции из командной строки: +/// Initial -Context ApplicationDbContext -OutputDir Database/Migrations +/// +/// +/// +public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + /// + /// Набор сущностей "BikeModel" (Модель велосипеда) + /// + public DbSet BikeModels { get; set; } + + /// + /// Набор сущностей "Bike" (Велосипед) + /// + public DbSet Bikes { get; set; } + + /// + /// Набор сущностей "Renter" (Арендатор) + /// + public DbSet Renters { get; set; } + + /// + /// Набор сущностей "Lease" (Договор аренды) + /// + public DbSet Leases { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Применить конфигурации из текущей сборки + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs new file mode 100644 index 000000000..051b87c88 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs @@ -0,0 +1,50 @@ +using BikeRental.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BikeRental.Infrastructure.Database.Configurations; + +/// +/// Конфигурация сущности "Bike" +/// +public class BikeConfiguration : IEntityTypeConfiguration +{ + /// + /// Настройка сущности "Bike" + /// + /// + public void Configure(EntityTypeBuilder builder) + { + // Установить наименование таблицы + builder.ToTable("Bikes"); + + // Первичный ключ + builder.HasKey(b => b.Id); + builder.Property(b => b.Id) + .ValueGeneratedOnAdd(); + + // Серийный номер велосипеда + builder.Property(b => b.SerialNumber) + .IsRequired() + .HasMaxLength(64); + + // Цвет + builder.Property(b => b.Color) + .IsRequired() + .HasMaxLength(32); + + // Внешний ключ на модель велосипеда + builder.Property(b => b.ModelId) + .IsRequired(); + + // Навигация на модель велосипеда + builder.HasOne(b => b.Model) + .WithMany() + .HasForeignKey(b => b.ModelId) + .OnDelete(DeleteBehavior.Restrict); + + // Уникальный индекс по серийному номеру + builder.HasIndex(b => b.SerialNumber) + .IsUnique(); + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs new file mode 100644 index 000000000..77e226c72 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs @@ -0,0 +1,64 @@ +using BikeRental.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BikeRental.Infrastructure.Database.Configurations; + +/// +/// Конфигурация сущности "BikeModel" +/// +public class BikeModelConfiguration : IEntityTypeConfiguration +{ + /// + /// Настройка сущности "BikeModel" + /// + /// + public void Configure(EntityTypeBuilder builder) + { + // Установить наименование таблицы + builder.ToTable("BikeModels"); + + // Первичный ключ + builder.HasKey(b => b.Id); + builder.Property(b => b.Id) + .ValueGeneratedOnAdd(); + + // Тип велосипеда (enum BikeType) — храним как int по умолчанию + builder.Property(b => b.Type) + .IsRequired(); + + // Размер колеса + builder.Property(b => b.WheelSize) + .IsRequired(); + + // Максимальный вес велосипедиста + builder.Property(b => b.MaxCyclistWeight) + .IsRequired(); + + // Вес велосипеда + builder.Property(b => b.Weight) + .IsRequired(); + + // Тип тормозной системы + builder.Property(b => b.BrakeType) + .IsRequired() + .HasMaxLength(50); + + // Год выпуска модели (строка из 4 символов) + builder.Property(b => b.YearOfManufacture) + .IsRequired() + .HasMaxLength(4); + + // Стоимость аренды в час + builder.Property(b => b.RentPrice) + .IsRequired() + .HasColumnType("decimal(10,2)"); + + // Индексы для типичных сценариев выборки + + // Индекс по типу велосипеда + builder.HasIndex(b => b.Type); + // Индекс по комбинации типа велосипеда и размера колеса + builder.HasIndex(b => new { b.Type, b.WheelSize }); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs new file mode 100644 index 000000000..14e65a07e --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs @@ -0,0 +1,45 @@ +using BikeRental.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BikeRental.Infrastructure.Database.Configurations; + +/// +/// Конфигурация сущности "Lease" +/// +public class LeaseConfiguration : IEntityTypeConfiguration +{ + /// + /// Настройка сущности "Lease" + /// + /// + public void Configure(EntityTypeBuilder builder) + { + // Установить наименование таблицы + builder.ToTable("Leases"); + + // Первичный ключ + builder.HasKey(l => l.Id); + builder.Property(l => l.Id) + .ValueGeneratedOnAdd(); + + // Связь с арендатором + builder.HasOne(l => l.Renter) + .WithMany() // коллекция договоров у Renter пока не определена + .IsRequired(); + + // Связь с велосипедом + builder.HasOne(l => l.Bike) + .WithMany() // коллекция договоров у Bike пока не определена + .IsRequired(); + + // Дата и время начала аренды + builder.Property(l => l.RentalStartTime) + .IsRequired(); + + // Продолжительность аренды + builder.Property(l => l.RentalDuration) + .IsRequired(); + } +} + diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs new file mode 100644 index 000000000..0024ee151 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs @@ -0,0 +1,41 @@ +using BikeRental.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BikeRental.Infrastructure.Database.Configurations; + +/// +/// Конфигурация сущности "Renter" +/// +public class RenterConfiguration : IEntityTypeConfiguration +{ + /// + /// Настройка сущности "Renter" + /// + /// + public void Configure(EntityTypeBuilder builder) + { + // Установить наименование таблицы + builder.ToTable("Renters"); + + // Первичный ключ + builder.HasKey(r => r.Id); + builder.Property(r => r.Id) + .ValueGeneratedOnAdd(); + + // Полное имя арендатора + builder.Property(r => r.FullName) + .IsRequired() + .HasMaxLength(200); + + // Номер телефона арендатора + builder.Property(r => r.PhoneNumber) + .IsRequired() + .HasMaxLength(32); + + // Уникальный индекс по номеру телефона + builder.HasIndex(r => r.PhoneNumber) + .IsUnique(); + } +} + diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs new file mode 100644 index 000000000..f75c52594 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs @@ -0,0 +1,46 @@ +using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Repositories; + +/// +/// Репозиторий для работы с моделями велосипедов. +/// +public sealed class BikeModelRepository(ApplicationDbContext dbContext) : IBikeModelRepository +{ + public async Task> GetAll() + { + return await dbContext.BikeModels + .ToListAsync(); + } + + public async Task GetById(int id) + { + return await dbContext.BikeModels + .FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task Add(BikeModel entity) + { + dbContext.BikeModels.Add(entity); + await dbContext.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(BikeModel entity) + { + if (dbContext.BikeModels.Local.All(e => e.Id != entity.Id)) + { + dbContext.BikeModels.Attach(entity); + } + await dbContext.SaveChangesAsync(); + } + + public async Task Delete(BikeModel entity) + { + dbContext.BikeModels.Remove(entity); + await dbContext.SaveChangesAsync(); + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs new file mode 100644 index 000000000..9888cd39f --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs @@ -0,0 +1,44 @@ +using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Repositories; + +public sealed class BikeRepository(ApplicationDbContext dbContext) : IBikeRepository +{ + public async Task> GetAll() + { + return await dbContext.Bikes + .ToListAsync(); + } + + public async Task GetById(int id) + { + return await dbContext.Bikes + .FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task Add(Bike entity) + { + dbContext.Bikes.Add(entity); + await dbContext.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Bike entity) + { + if (dbContext.Bikes.Local.All(e => e.Id != entity.Id)) + { + dbContext.Bikes.Attach(entity); + } + await dbContext.SaveChangesAsync(); + } + + public async Task Delete(Bike entity) + { + dbContext.Bikes.Remove(entity); + await dbContext.SaveChangesAsync(); + } +} + diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs new file mode 100644 index 000000000..467338a2d --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs @@ -0,0 +1,43 @@ +using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Repositories; + +public sealed class LeaseRepository(ApplicationDbContext dbContext) : ILeaseRepository +{ + public async Task> GetAll() + { + return await dbContext.Leases + .ToListAsync(); + } + + public async Task GetById(int id) + { + return await dbContext.Leases + .FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task Add(Lease entity) + { + dbContext.Leases.Add(entity); + await dbContext.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Lease entity) + { + if (dbContext.Leases.Local.All(e => e.Id != entity.Id)) + { + dbContext.Leases.Attach(entity); + } + await dbContext.SaveChangesAsync(); + } + + public async Task Delete(Lease entity) + { + dbContext.Leases.Remove(entity); + await dbContext.SaveChangesAsync(); + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs new file mode 100644 index 000000000..131c08714 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs @@ -0,0 +1,44 @@ +using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Repositories; + +public sealed class RenterRepository(ApplicationDbContext dbContext) : IRenterRepository +{ + public async Task> GetAll() + { + return await dbContext.Renters + .ToListAsync(); + } + + public async Task GetById(int id) + { + return await dbContext.Renters + .FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task Add(Renter entity) + { + dbContext.Renters.Add(entity); + await dbContext.SaveChangesAsync(); + return entity.Id; + } + + public async Task Update(Renter entity) + { + if (dbContext.Renters.Local.All(e => e.Id != entity.Id)) + { + dbContext.Renters.Attach(entity); + } + await dbContext.SaveChangesAsync(); + } + + public async Task Delete(Renter entity) + { + dbContext.Renters.Remove(entity); + await dbContext.SaveChangesAsync(); + } +} + diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index 629e44aac..c5d8d233b 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -38,16 +38,16 @@ public RentalFixture() private static List GetBikeModels() => [ - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxСyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, - new() { Id = 2, Type = BikeType.Road, WheelSize = 27, MaxСyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, - new() { Id = 3, Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, - new() { Id = 4, Type = BikeType.Road, WheelSize = 29, MaxСyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, - new() { Id = 5, Type = BikeType.Hybrid, WheelSize = 26, MaxСyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, - new() { Id = 6, Type = BikeType.Sport, WheelSize = 28, MaxСyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, - new() { Id = 7, Type = BikeType.Mountain, WheelSize = 27, MaxСyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, - new() { Id = 8, Type = BikeType.Hybrid, WheelSize = 29, MaxСyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, - new() { Id = 9, Type = BikeType.Sport, WheelSize = 26, MaxСyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, - new() { Id = 10, Type = BikeType.Road, WheelSize = 28, MaxСyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxCyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, + new() { Id = 2, Type = BikeType.Road, WheelSize = 27, MaxCyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, + new() { Id = 3, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, + new() { Id = 4, Type = BikeType.Road, WheelSize = 29, MaxCyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, + new() { Id = 5, Type = BikeType.Hybrid, WheelSize = 26, MaxCyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, + new() { Id = 6, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, + new() { Id = 7, Type = BikeType.Mountain, WheelSize = 27, MaxCyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, + new() { Id = 8, Type = BikeType.Hybrid, WheelSize = 29, MaxCyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, + new() { Id = 9, Type = BikeType.Sport, WheelSize = 26, MaxCyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, + new() { Id = 10, Type = BikeType.Road, WheelSize = 28, MaxCyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, ]; private static List GetRenters() => @@ -66,16 +66,86 @@ private static List GetRenters() => private static List GetBikes(List models) => [ - new() { Id = 1, SerialNumber = "R001", Color = "Silver", Model = models[0] }, - new() { Id = 2, SerialNumber = "R002", Color = "Navy", Model = models[1] }, - new() { Id = 3, SerialNumber = "R003", Color = "Charcoal", Model = models[2] }, - new() { Id = 4, SerialNumber = "R004", Color = "Beige", Model = models[3] }, - new() { Id = 5, SerialNumber = "R005", Color = "Burgundy", Model = models[4] }, - new() { Id = 6, SerialNumber = "R006", Color = "Teal", Model = models[5] }, - new() { Id = 7, SerialNumber = "R007", Color = "Coral", Model = models[6] }, - new() { Id = 8, SerialNumber = "R008", Color = "Indigo", Model = models[7] }, - new() { Id = 9, SerialNumber = "R009", Color = "Bronze", Model = models[8] }, - new() { Id = 10, SerialNumber = "R010", Color = "Lavender", Model = models[9] }, + new() + { + Id = 1, + SerialNumber = "R001", + Color = "Silver", + Model = models[0], + ModelId = 0 + }, + new() + { + Id = 2, + SerialNumber = "R002", + Color = "Navy", + Model = models[1], + ModelId = 0 + }, + new() + { + Id = 3, + SerialNumber = "R003", + Color = "Charcoal", + Model = models[2], + ModelId = 0 + }, + new() + { + Id = 4, + SerialNumber = "R004", + Color = "Beige", + Model = models[3], + ModelId = 0 + }, + new() + { + Id = 5, + SerialNumber = "R005", + Color = "Burgundy", + Model = models[4], + ModelId = 0 + }, + new() + { + Id = 6, + SerialNumber = "R006", + Color = "Teal", + Model = models[5], + ModelId = 0 + }, + new() + { + Id = 7, + SerialNumber = "R007", + Color = "Coral", + Model = models[6], + ModelId = 0 + }, + new() + { + Id = 8, + SerialNumber = "R008", + Color = "Indigo", + Model = models[7], + ModelId = 0 + }, + new() + { + Id = 9, + SerialNumber = "R009", + Color = "Bronze", + Model = models[8], + ModelId = 0 + }, + new() + { + Id = 10, + SerialNumber = "R010", + Color = "Lavender", + Model = models[9], + ModelId = 0 + }, ]; private static List GetLeases(List bikes, List renters) => From 083bef5f877fd3bdc314289dac0865d6ff8da1e6 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 17:56:22 +0400 Subject: [PATCH 25/48] add API controllers with CRUD endpoints --- .../Controllers/BikeModelsController.cs | 94 +++++++++++++++++++ .../Controllers/BikesController.cs | 76 +++++++++++++++ .../Controllers/LeasesController.cs | 76 +++++++++++++++ .../Controllers/RentersController.cs | 81 ++++++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs create mode 100644 BikeRental/BikeRental.Api/Controllers/BikesController.cs create mode 100644 BikeRental/BikeRental.Api/Controllers/LeasesController.cs create mode 100644 BikeRental/BikeRental.Api/Controllers/RentersController.cs diff --git a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs new file mode 100644 index 000000000..7ef11f2ce --- /dev/null +++ b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs @@ -0,0 +1,94 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace BikeRental.Api.Controllers; + +/// +/// Контроллер описывает конечные точки для работы с +/// ресурсом "BikeModel" (модель велосипеда) +/// "bikeModelService" - сервис для работы с ресурсом BikeModel +/// Зависимость от интерфейса, а не конкретной реализации в сервисе (SOLID - DIP) +/// +[ApiController] +[Route("bike-models")] +public sealed class BikeModelsController(IBikeModelService bikeModelService) : ControllerBase +{ + /// + /// Получить все модели велосипедов + /// + [HttpGet] + public async Task>> GetAll() + { + // Обратиться к репозиторию для получения всех моделей велосипедов + var models = await bikeModelService.GetAll(); + return Ok(models); + } + + /// + /// Получить модель велосипеда по идентификатору + /// + [HttpGet("{id:int}")] // ограничение - ID должен быть числом + public async Task> GetById(int id) + { + // Обратиться к репозиторию для получения модели велосипеда по идентификатору + var model = await bikeModelService.GetById(id); + if (model is null) + { + // вернуть код ответа 404 Not Found (не найдено) + return NotFound(); + } + + return Ok(model); + } + + /// + /// Создать новую модель велосипеда + /// "dto" - модель для создания модели велосипеда + /// + [HttpPost] + public async Task> Create([FromBody] BikeModelCreateUpdateDto dto) + { + // Обратиться к репозиторию для создания новой модели велосипеда + // с использованием данных из dto + var created = await bikeModelService.Create(dto); + + // Вернуть успешный результат обработки операции + // с кодом ответа 201 Created (создано) и созданной моделью велосипеда + + // Дополнительно вернуть в заголовке Location ссылку на созданный ресурс + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + /// + /// Обновить существующую модель велосипеда + /// + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] BikeModelCreateUpdateDto dto) + { + // Обратиться к репозиторию для обновления модели велосипеда по идентификатору + // с использованием данных из dto + var updated = await bikeModelService.Update(id, dto); + if (updated is null) + { + // Если не найдена + return NotFound(); + } + return Ok(updated); + } + + /// + /// Удалить модель велосипеда по идентификатору + /// + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + // Обратиться к репозиторию для удаления модели велосипеда по идентификатору + var deleted = await bikeModelService.Delete(id); + if (!deleted) + { + return NotFound(); + } + return NoContent(); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/BikesController.cs b/BikeRental/BikeRental.Api/Controllers/BikesController.cs new file mode 100644 index 000000000..95c716422 --- /dev/null +++ b/BikeRental/BikeRental.Api/Controllers/BikesController.cs @@ -0,0 +1,76 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace BikeRental.Api.Controllers; + +/// +/// Контроллер описывает конечные точки для работы +/// с ресурсом "Bike" (велосипед) +/// +[ApiController] +[Route("bikes")] +public sealed class BikesController(IBikeService bikeService) : ControllerBase +{ + /// + /// Получить все ресурсы Bike + /// + [HttpGet] + public async Task>> GetAll() + { + var bikes = await bikeService.GetAll(); + return Ok(bikes); + } + + /// + /// Получить ресурс по идентификатору Bike + /// + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var bike = await bikeService.GetById(id); + if (bike is null) + { + return NotFound(); + } + return Ok(bike); + } + + /// + /// Создать новый ресурс Bike + /// + [HttpPost] + public async Task> Create([FromBody] BikeCreateUpdateDto dto) + { + var created = await bikeService.Create(dto); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + /// + /// Обновить существующий ресурс Bike + /// + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] BikeCreateUpdateDto dto) + { + var updated = await bikeService.Update(id, dto); + if (updated is null) + { + return NotFound(); + } + return Ok(updated); + } + + /// + /// Удалить ресурс Bike + /// + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var deleted = await bikeService.Delete(id); + if (!deleted) + { + return NotFound(); + } + return NoContent(); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/LeasesController.cs b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs new file mode 100644 index 000000000..3c6c5eeb8 --- /dev/null +++ b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs @@ -0,0 +1,76 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace BikeRental.Api.Controllers; + +/// +/// Контроллер описывает конечные точки для работы с ресурсом +/// "Lease" (договор на аренду велосипеда) +/// +[ApiController] +[Route("leases")] +public sealed class LeasesController(ILeaseService leaseService) : ControllerBase +{ + /// + /// Получить все договора на аренду велосипедов + /// + [HttpGet] + public async Task>> GetAll() + { + var leases = await leaseService.GetAll(); + return Ok(leases); + } + + /// + /// Получить договор на аренду велосипеда по идентификатору + /// + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var lease = await leaseService.GetById(id); + if (lease is null) + { + return NotFound(); + } + return Ok(lease); + } + + /// + /// Создать договор на аренду велосипеда + /// + [HttpPost] + public async Task> Create([FromBody] LeaseCreateUpdateDto dto) + { + var created = await leaseService.Create(dto); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + /// + /// Обновить состояние текущего договора на аренду велосипеда + /// + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] LeaseCreateUpdateDto dto) + { + var updated = await leaseService.Update(id, dto); + if (updated is null) + { + return NotFound(); + } + return Ok(updated); + } + + /// + /// Удалить договор на аренду велосипеда по идентификатору + /// + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var deleted = await leaseService.Delete(id); + if (!deleted) + { + return NotFound(); + } + return NoContent(); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/RentersController.cs b/BikeRental/BikeRental.Api/Controllers/RentersController.cs new file mode 100644 index 000000000..d535d8c83 --- /dev/null +++ b/BikeRental/BikeRental.Api/Controllers/RentersController.cs @@ -0,0 +1,81 @@ +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace BikeRental.Api.Controllers; + +/// +/// Контроллер описывает конечные точки для работы с ресурсом +/// "Renter" (арендатор) +/// +[ApiController] +[Route("renters")] +public sealed class RentersController(IRenterService renterService) : ControllerBase +{ + /// + /// Получить всех арендаторов + /// + [HttpGet] + public async Task>> GetAll() + { + var renters = await renterService.GetAll(); + return Ok(renters); + } + + /// + /// Получить арендатора по идентификатору + /// + /// + [HttpGet("{id:int}")] + public async Task> GetById(int id) + { + var renter = await renterService.GetById(id); + if (renter is null) + { + return NotFound(); + } + return Ok(renter); + } + + /// + /// Создать нового арендатора + /// + /// + [HttpPost] + public async Task> Create([FromBody] RenterCreateUpdateDto dto) + { + var created = await renterService.Create(dto); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + /// + /// Обновить существующего арендатора + /// + /// + /// + [HttpPut("{id:int}")] + public async Task> Update(int id, [FromBody] RenterCreateUpdateDto dto) + { + var updated = await renterService.Update(id, dto); + if (updated is null) + { + return NotFound(); + } + return Ok(updated); + } + + /// + /// Удалить арендатора по идентификатору + /// + /// + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var deleted = await renterService.Delete(id); + if (!deleted) + { + return NotFound(); + } + return NoContent(); + } +} \ No newline at end of file From 4c24cb830d12812915071df2614800a08fa19149 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 17:57:13 +0400 Subject: [PATCH 26/48] add error handling and dependency injection: GlobalExceptionHandler middleware, DependencyInjection configuration, DatabaseExtensions for migrations --- .../BikeRental.Api/DependencyInjection.cs | 119 ++++++++++++++++++ .../Extensions/DatabaseExtensions.cs | 42 +++++++ .../Middleware/GlobalExceptionHandler.cs | 38 ++++++ 3 files changed, 199 insertions(+) create mode 100644 BikeRental/BikeRental.Api/DependencyInjection.cs create mode 100644 BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs create mode 100644 BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs diff --git a/BikeRental/BikeRental.Api/DependencyInjection.cs b/BikeRental/BikeRental.Api/DependencyInjection.cs new file mode 100644 index 000000000..a275f21e0 --- /dev/null +++ b/BikeRental/BikeRental.Api/DependencyInjection.cs @@ -0,0 +1,119 @@ +using BikeRental.Api.Middleware; +using BikeRental.Application.Interfaces; +using BikeRental.Application.Services; +using BikeRental.Domain.Interfaces; +using BikeRental.Infrastructure.Database; +using BikeRental.Infrastructure.Repositories; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace BikeRental.Api; + +/// +/// Настройка зависимостей приложения +/// +public static class DependencyInjection +{ + /// + /// Зарегистрировать и настроить сервисы контроллеров + /// + public static void AddControllers(this WebApplicationBuilder builder) + { + builder.Services.AddControllers(options => + { + options.ReturnHttpNotAcceptable = false; // 406 + }) + .AddNewtonsoftJson() // заменить стандартный JSON на Newtonsoft.json + .AddXmlSerializerFormatters(); // отвечать в XML формате + } + + /// + /// Зарегистрировать и настроить сервисы обработки ошибок + /// + public static void AddErrorHandling(this WebApplicationBuilder builder) + { + builder.Services.AddExceptionHandler(); + + builder.Services.AddProblemDetails(); + } + + /// + /// Зарегистрировать и настроить сервисы OpenTelemetry + /// + public static void AddObservability(this WebApplicationBuilder builder) + { + // Зарегистрировать сервис OpenTelemetry + builder.Services.AddOpenTelemetry() + // Добавить ресурс по наименованию приложения + .ConfigureResource(resource => resource.AddService(builder.Environment.ApplicationName)) + // Настроить распределенную трассировку + .WithTracing(tracing => tracing + // Добавить инструментарии для HttpClient и ASP.NET Core + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() + .AddEntityFrameworkCoreInstrumentation()) + // Добавить метрики + .WithMetrics(metrics => metrics + // Добавить инструментарии для .NET Runtime, HttpClient и ASP.NET Core + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() + .AddRuntimeInstrumentation()) + // Настроить глобальный экспортер метрик + .UseOtlpExporter(); + + // Настроить ведение журнала OpenTelemetry + builder.Logging.AddOpenTelemetry(options => + { + options.IncludeScopes = true; // Включить области + options.IncludeFormattedMessage = true; // Включить форматированные сообщения + }); + + } + + /// + /// Зарегистрировать и настроить сервисы взаимодействия с базой данных + /// + public static void AddDatabase(this WebApplicationBuilder builder) + { + builder.Services.AddDbContext(options => + options + .UseMySQL( + builder.Configuration.GetConnectionString("bike-rental") ?? throw new InvalidOperationException(), + npgsqlOptions => npgsqlOptions + // Настроить таблицу истории миграций + .MigrationsHistoryTable(HistoryRepository.DefaultTableName)) + // Использовать соглашение именования snake_case + .UseSnakeCaseNamingConvention()); + } + + /// + /// Зарегистрировать репозитории + /// + public static void AddRepositories(this WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + } + + /// + /// Регистрация сервисов общего назначения + /// + public static void AddServices(this WebApplicationBuilder builder) + { + // Зарегистрировать сервисы прикладного уровня + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + } +} diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs new file mode 100644 index 000000000..f9297015b --- /dev/null +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -0,0 +1,42 @@ +using BikeRental.Infrastructure.Database; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace BikeRental.Api.Extensions; + +/// +/// Предоставляет методы расширения для работы с базой данных +/// +public static class DatabaseExtensions +{ + /// + /// Применить все миграции к базе данных + /// + /// + public static async Task ApplyMigrationsAsync(this WebApplication app) // this делает метод расширением для типа WebApplication + { + // мы не в HTTP запросе тк это запуск приложения + // поэтому создаем Scope(один из уровней DI контейнера) вручную, как бы новую область видимости для DI + // Scope гарантирует, что все зависимости будут правильно созданы и уничтожены + using var scope = app.Services.CreateScope(); + + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + // scope.ServiceProvider - DI контейнер в рамках созданного Scope + // GetRequiredService() - получить сервис типа T + // Требует, чтобы сервис был зарегистрирован, иначе исключение + // DbContext реализует IAsyncDisposable (асинхронное освобождение ресурсов) + + try + { + await dbContext.Database.MigrateAsync(); // Применить все миграции к базе данных + app.Logger.LogInformation("Database migrations applied successfully."); + } + catch (Exception e) + { + app.Logger.LogError(e, "An error occurred while applying database migrations."); + throw; + } + } +} diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs new file mode 100644 index 000000000..3eb182d47 --- /dev/null +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace BikeRental.Api.Middleware; + +/// +/// Глобальный обработчик исключений +/// +/// +public sealed class GlobalExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler // - интерфейс в .NET 8 для обработки исключений +{ + /// + /// Попытаться обработать исключение + /// + public ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + return problemDetailsService.TryWriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, // получение информации о запросе + Exception = exception, // для логирования и диагностики + ProblemDetails = new ProblemDetails // базовая информация об ошибке + { + Title = "Internal Server Error", + Detail = "An error occurred while processing your request. Please try again" + } + // TryWriteAsync() пишет ответ в поток HTTP + // 1. Пользователь кинул запрос например GET .../999 + // 2. Контроллер отсылает в NullReferenceException - тк bikeModelService.GetById(id) вернет null. + // 3. ASP.NET Core ловит исключение + // 4. Вызывает GlobalExceptionHandler.TryHandleAsync() + // 5. ProblemDetailsService генерирует JSON ответ + }); + } +} From f98896cc98bb1ccb0bf69e28def0c1ecdb7b3125 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 18:15:28 +0400 Subject: [PATCH 27/48] Update project structure --- .../BikeRental.Api/BikeRental.Api.csproj | 42 ++++++++++- BikeRental/BikeRental.Api/Program.cs | 69 ++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index dad2ab4e6..dd70f6a88 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -8,7 +8,47 @@ - + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + appsettings.json + diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index e5dff12bc..546bc342d 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -1,3 +1,68 @@ -// See https://aka.ms/new-console-template for more information +using BikeRental.Api; +using BikeRental.Api.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; -Console.WriteLine("Hello, World!"); \ No newline at end of file +// Создать объект WebApplicationBuilder (построитель веб-приложения) +// с использованием переданных аргументов командной строки +var builder = WebApplication.CreateBuilder(args); +// Зарегистрировать и настроить сервисы контроллеров +builder.AddControllers(); +// Зарегистрировать и настроить сервисы обработки ошибок +builder.AddErrorHandling(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "BikeRental API", + Version = "v1", + Description = "API для управления сервисом проката велосипедов" + }); + + // описание XML документации + var basePath = AppContext.BaseDirectory; + var xmlPathApi = Path.Combine(basePath, $"BikeRental.Api.xml"); + options.IncludeXmlComments(xmlPathApi); +}); + + +// Зарегистрировать и настроить сервисы OpenTelemetry +builder.AddObservability(); +// Зарегистрировать и настроить сервисы взаимодействия с базой данных +builder.AddDatabase(); +// Зарегистрировать и настроить сервисы репозиториев +builder.AddRepositories(); +// Зарегистрировать и настроить сервисы общего назначения +builder.AddServices(); + +// Создать конвейер обработки запросов +var app = builder.Build(); + +// Если приложение работает в режиме разработки, то +if (app.Environment.IsDevelopment()) +{ + // https://localhost:/swagger + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "BikeRental API v1"); + c.RoutePrefix = "swagger"; + c.ShowCommonExtensions(); + }); + + + // Применить миграции базы данных (из DatabaceExtensions) + await app.ApplyMigrationsAsync(); +} + +// Использовать обработчики исключений (GlobalExceptionHandler, ValidationExceptionHandler) +app.UseExceptionHandler(); + +// Зарегистрировать конечные точки контроллеров +app.MapControllers(); +// Запустить приложение +await app.RunAsync().ConfigureAwait(false); \ No newline at end of file From ff7ec1b53b49deae0a4429a9214dba8a1a626df0 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 19:43:29 +0400 Subject: [PATCH 28/48] add initial EF Core migration --- .../20251215170139_Initial.Designer.cs | 211 ++++++++++++++++++ .../Migrations/20251215170139_Initial.cs | 158 +++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 208 +++++++++++++++++ 3 files changed, 577 insertions(+) create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.Designer.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs diff --git a/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.Designer.cs b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.Designer.cs new file mode 100644 index 000000000..7f851f994 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.Designer.cs @@ -0,0 +1,211 @@ +// +using System; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BikeRental.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251215170139_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("BikeRental.Domain.Models.Bike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("color"); + + b.Property("ModelId") + .HasColumnType("int") + .HasColumnName("model_id"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("serial_number"); + + b.HasKey("Id") + .HasName("pk_bikes"); + + b.HasIndex("ModelId") + .HasDatabaseName("ix_bikes_model_id"); + + b.HasIndex("SerialNumber") + .IsUnique() + .HasDatabaseName("ix_bikes_serial_number"); + + b.ToTable("Bikes", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.BikeModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("BrakeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("brake_type"); + + b.Property("MaxCyclistWeight") + .HasColumnType("int") + .HasColumnName("max_cyclist_weight"); + + b.Property("RentPrice") + .HasColumnType("decimal(10,2)") + .HasColumnName("rent_price"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.Property("Weight") + .HasColumnType("double") + .HasColumnName("weight"); + + b.Property("WheelSize") + .HasColumnType("int") + .HasColumnName("wheel_size"); + + b.Property("YearOfManufacture") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("varchar(4)") + .HasColumnName("year_of_manufacture"); + + b.HasKey("Id") + .HasName("pk_bike_models"); + + b.HasIndex("Type") + .HasDatabaseName("ix_bike_models_type"); + + b.HasIndex("Type", "WheelSize") + .HasDatabaseName("ix_bike_models_type_wheel_size"); + + b.ToTable("BikeModels", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Lease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("BikeId") + .HasColumnType("int") + .HasColumnName("bike_id"); + + b.Property("RentalDuration") + .HasColumnType("int") + .HasColumnName("rental_duration"); + + b.Property("RentalStartTime") + .HasColumnType("datetime(6)") + .HasColumnName("rental_start_time"); + + b.Property("RenterId") + .HasColumnType("int") + .HasColumnName("renter_id"); + + b.HasKey("Id") + .HasName("pk_leases"); + + b.HasIndex("BikeId") + .HasDatabaseName("ix_leases_bike_id"); + + b.HasIndex("RenterId") + .HasDatabaseName("ix_leases_renter_id"); + + b.ToTable("Leases", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Renter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("full_name"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("phone_number"); + + b.HasKey("Id") + .HasName("pk_renters"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasDatabaseName("ix_renters_phone_number"); + + b.ToTable("Renters", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Bike", b => + { + b.HasOne("BikeRental.Domain.Models.BikeModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bikes_bike_models_model_id"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Lease", b => + { + b.HasOne("BikeRental.Domain.Models.Bike", "Bike") + .WithMany() + .HasForeignKey("BikeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_leases_bikes_bike_id"); + + b.HasOne("BikeRental.Domain.Models.Renter", "Renter") + .WithMany() + .HasForeignKey("RenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_leases_renters_renter_id"); + + b.Navigation("Bike"); + + b.Navigation("Renter"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs new file mode 100644 index 000000000..7fb1ca6fc --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using MySql.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace BikeRental.Infrastructure.Database.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "BikeModels", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + type = table.Column(type: "int", nullable: false), + wheel_size = table.Column(type: "int", nullable: false), + max_cyclist_weight = table.Column(type: "int", nullable: false), + weight = table.Column(type: "double", nullable: false), + brake_type = table.Column(type: "varchar(50)", maxLength: 50, nullable: false), + year_of_manufacture = table.Column(type: "varchar(4)", maxLength: 4, nullable: false), + rent_price = table.Column(type: "decimal(10,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_bike_models", x => x.id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Renters", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + full_name = table.Column(type: "varchar(200)", maxLength: 200, nullable: false), + phone_number = table.Column(type: "varchar(32)", maxLength: 32, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_renters", x => x.id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Bikes", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + model_id = table.Column(type: "int", nullable: false), + serial_number = table.Column(type: "varchar(64)", maxLength: 64, nullable: false), + color = table.Column(type: "varchar(32)", maxLength: 32, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_bikes", x => x.id); + table.ForeignKey( + name: "fk_bikes_bike_models_model_id", + column: x => x.model_id, + principalTable: "BikeModels", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Leases", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + renter_id = table.Column(type: "int", nullable: false), + bike_id = table.Column(type: "int", nullable: false), + rental_start_time = table.Column(type: "datetime(6)", nullable: false), + rental_duration = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_leases", x => x.id); + table.ForeignKey( + name: "fk_leases_bikes_bike_id", + column: x => x.bike_id, + principalTable: "Bikes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_leases_renters_renter_id", + column: x => x.renter_id, + principalTable: "Renters", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "ix_bike_models_type", + table: "BikeModels", + column: "type"); + + migrationBuilder.CreateIndex( + name: "ix_bike_models_type_wheel_size", + table: "BikeModels", + columns: new[] { "type", "wheel_size" }); + + migrationBuilder.CreateIndex( + name: "ix_bikes_model_id", + table: "Bikes", + column: "model_id"); + + migrationBuilder.CreateIndex( + name: "ix_bikes_serial_number", + table: "Bikes", + column: "serial_number", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_leases_bike_id", + table: "Leases", + column: "bike_id"); + + migrationBuilder.CreateIndex( + name: "ix_leases_renter_id", + table: "Leases", + column: "renter_id"); + + migrationBuilder.CreateIndex( + name: "ix_renters_phone_number", + table: "Renters", + column: "phone_number", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Leases"); + + migrationBuilder.DropTable( + name: "Bikes"); + + migrationBuilder.DropTable( + name: "Renters"); + + migrationBuilder.DropTable( + name: "BikeModels"); + } + } +} diff --git a/BikeRental/BikeRental.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/BikeRental/BikeRental.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 000000000..c1905d249 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,208 @@ +// +using System; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BikeRental.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.22") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("BikeRental.Domain.Models.Bike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("color"); + + b.Property("ModelId") + .HasColumnType("int") + .HasColumnName("model_id"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("serial_number"); + + b.HasKey("Id") + .HasName("pk_bikes"); + + b.HasIndex("ModelId") + .HasDatabaseName("ix_bikes_model_id"); + + b.HasIndex("SerialNumber") + .IsUnique() + .HasDatabaseName("ix_bikes_serial_number"); + + b.ToTable("Bikes", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.BikeModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("BrakeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("brake_type"); + + b.Property("MaxCyclistWeight") + .HasColumnType("int") + .HasColumnName("max_cyclist_weight"); + + b.Property("RentPrice") + .HasColumnType("decimal(10,2)") + .HasColumnName("rent_price"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.Property("Weight") + .HasColumnType("double") + .HasColumnName("weight"); + + b.Property("WheelSize") + .HasColumnType("int") + .HasColumnName("wheel_size"); + + b.Property("YearOfManufacture") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("varchar(4)") + .HasColumnName("year_of_manufacture"); + + b.HasKey("Id") + .HasName("pk_bike_models"); + + b.HasIndex("Type") + .HasDatabaseName("ix_bike_models_type"); + + b.HasIndex("Type", "WheelSize") + .HasDatabaseName("ix_bike_models_type_wheel_size"); + + b.ToTable("BikeModels", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Lease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("BikeId") + .HasColumnType("int") + .HasColumnName("bike_id"); + + b.Property("RentalDuration") + .HasColumnType("int") + .HasColumnName("rental_duration"); + + b.Property("RentalStartTime") + .HasColumnType("datetime(6)") + .HasColumnName("rental_start_time"); + + b.Property("RenterId") + .HasColumnType("int") + .HasColumnName("renter_id"); + + b.HasKey("Id") + .HasName("pk_leases"); + + b.HasIndex("BikeId") + .HasDatabaseName("ix_leases_bike_id"); + + b.HasIndex("RenterId") + .HasDatabaseName("ix_leases_renter_id"); + + b.ToTable("Leases", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Renter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("full_name"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("phone_number"); + + b.HasKey("Id") + .HasName("pk_renters"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasDatabaseName("ix_renters_phone_number"); + + b.ToTable("Renters", (string)null); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Bike", b => + { + b.HasOne("BikeRental.Domain.Models.BikeModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bikes_bike_models_model_id"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("BikeRental.Domain.Models.Lease", b => + { + b.HasOne("BikeRental.Domain.Models.Bike", "Bike") + .WithMany() + .HasForeignKey("BikeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_leases_bikes_bike_id"); + + b.HasOne("BikeRental.Domain.Models.Renter", "Renter") + .WithMany() + .HasForeignKey("RenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_leases_renters_renter_id"); + + b.Navigation("Bike"); + + b.Navigation("Renter"); + }); +#pragma warning restore 612, 618 + } + } +} From 5aa70a5909b5af0156e4e5d0f57e18ceb6452b36 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 19:44:51 +0400 Subject: [PATCH 29/48] implemented data seeding service and extensions, added appsettings and project configurations --- .../BikeRental.Api/BikeRental.Api.csproj | 4 +- .../Extensions/SeedDataExtensions.cs | 21 ++++ .../Properties/launchSettings.json | 22 ++++ .../appsettings.Development.json | 12 ++ BikeRental/BikeRental.Api/appsettings.json | 11 ++ .../BikeRental.Application.Contracts.csproj | 4 + .../BikeRental.Infrastructure.csproj | 1 + .../Services/ISeedDataService.cs | 13 +++ .../Services/Impl/SeedDataService.cs | 108 ++++++++++++++++++ 9 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs create mode 100644 BikeRental/BikeRental.Api/Properties/launchSettings.json create mode 100644 BikeRental/BikeRental.Api/appsettings.Development.json create mode 100644 BikeRental/BikeRental.Api/appsettings.json create mode 100644 BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs create mode 100644 BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index dd70f6a88..41c4d6495 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -1,7 +1,6 @@ - + - Exe net8.0 enable enable @@ -50,5 +49,6 @@ appsettings.json + diff --git a/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs b/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs new file mode 100644 index 000000000..a8ed9b333 --- /dev/null +++ b/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs @@ -0,0 +1,21 @@ +using BikeRental.Infrastructure.Services; + +namespace BikeRental.Api.Extensions; + +/// +/// Предоставляет методы расширения для выполнения +/// первичной инициализации данных в бд +/// +public static class SeedDataExtensions +{ + /// + /// Проинициализировать данные в бд + /// + /// + public static async Task SeedData(this IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + var seedDataService = scope.ServiceProvider.GetRequiredService(); + await seedDataService.SeedDataAsync(); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Properties/launchSettings.json b/BikeRental/BikeRental.Api/Properties/launchSettings.json new file mode 100644 index 000000000..52cfc9a3d --- /dev/null +++ b/BikeRental/BikeRental.Api/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5043" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/appsettings.Development.json b/BikeRental/BikeRental.Api/appsettings.Development.json new file mode 100644 index 000000000..a66c3ef14 --- /dev/null +++ b/BikeRental/BikeRental.Api/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "bike-rental": "server=bike-rental-db;Database=bike-rental;User Id=root;Password=1234512345Aa$;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} + diff --git a/BikeRental/BikeRental.Api/appsettings.json b/BikeRental/BikeRental.Api/appsettings.json new file mode 100644 index 000000000..204e3d4c7 --- /dev/null +++ b/BikeRental/BikeRental.Api/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj index 44ea37af1..0a85e0c57 100644 --- a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -9,4 +9,8 @@ + + + + diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj index 041ca759f..c8140ce09 100644 --- a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -7,6 +7,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs new file mode 100644 index 000000000..312cb067d --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs @@ -0,0 +1,13 @@ +namespace BikeRental.Infrastructure.Services; + +/// +/// Интерфейс описывает сервис инициализации данных +/// +public interface ISeedDataService +{ + /// + /// Выполнить инициализацию данных + /// + /// + public Task SeedDataAsync(); +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs new file mode 100644 index 000000000..176374680 --- /dev/null +++ b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs @@ -0,0 +1,108 @@ +using BikeRental.Domain.Enum; +using BikeRental.Domain.Models; +using BikeRental.Infrastructure.Database; +using Bogus; +using Microsoft.EntityFrameworkCore; + +namespace BikeRental.Infrastructure.Services.Impl; + +/// +/// Сервис инициализации данных +/// +/// +public class SeedDataService(ApplicationDbContext dbContext) : ISeedDataService +{ + /// + /// Выполнить инициализацию данных + /// + public async Task SeedDataAsync() + { + // Подготовить генератор фейковых данных + var faker = new Faker("ru"); + + // Создать модели велосипедов, если они отсутствуют + if (!await dbContext.BikeModels.AnyAsync()) + { + var bikeModels = new List(); + for (var i = 0; i < 10; i++) + { + var bikeModel = new BikeModel + { + Type = faker.PickRandom(), + WheelSize = faker.Random.Int(20, 29), + MaxCyclistWeight = faker.Random.Int(60, 120), + Weight = Math.Round(faker.Random.Double(7.0, 15.0), 2), + BrakeType = faker.PickRandom("Disc", "Rim", "Drum"), + YearOfManufacture = faker.Date.Past(10).Year.ToString(), + RentPrice = Math.Round(faker.Random.Decimal(5.0m, 20.0m), 2) + }; + bikeModels.Add(bikeModel); + } + + dbContext.BikeModels.AddRange(bikeModels); + await dbContext.SaveChangesAsync(); + } + + // Создать арендаторов, если они отсутствуют + if (!await dbContext.Renters.AnyAsync()) + { + var renters = new List(); + for (var i = 0; i < 20; i++) + { + var renter = new Renter + { + FullName = faker.Name.FullName(), + PhoneNumber = faker.Phone.PhoneNumber() + }; + renters.Add(renter); + } + + dbContext.Renters.AddRange(renters); + await dbContext.SaveChangesAsync(); + } + + // Создать велосипеды, если они отсутствуют + if (!await dbContext.Bikes.AnyAsync()) + { + var bikeModels = await dbContext.BikeModels.ToListAsync(); + var bikes = new List(); + for (var i = 0; i < 30; i++) + { + var bike = new Bike + { + ModelId = faker.PickRandom(bikeModels).Id, + SerialNumber = faker.Random.AlphaNumeric(10).ToUpper(), + Color = faker.Commerce.Color(), + Model = faker.PickRandom(bikeModels) + }; + bikes.Add(bike); + } + + dbContext.Bikes.AddRange(bikes); + await dbContext.SaveChangesAsync(); + } + + // Создать договора аренды, если они отсутствуют для + // некоторых велосипедов + if (!await dbContext.Leases.AnyAsync()) + { + var renters = await dbContext.Renters.ToListAsync(); + var bikes = await dbContext.Bikes.ToListAsync(); + var leases = new List(); + for (var i = 0; i < 15; i++) + { + var lease = new Lease + { + Renter = faker.PickRandom(renters), + Bike = faker.PickRandom(bikes), + RentalStartTime = faker.Date.Recent(10), + RentalDuration = faker.Random.Int(1, 72) + }; + leases.Add(lease); + } + + dbContext.Leases.AddRange(leases); + await dbContext.SaveChangesAsync(); + } + } +} \ No newline at end of file From b4cd94f6efb830cc37823f19ab0362c1cc32f7e3 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 18 Dec 2025 23:28:28 +0400 Subject: [PATCH 30/48] updated the Lease model, improved the infrastructure, and added source data generation. --- BikeRental/AppHost/AppHost.csproj | 29 +++++ BikeRental/AppHost/Program.cs | 19 +++ .../AppHost/Properties/launchSettings.json | 31 +++++ .../AppHost/appsettings.Development.json | 8 ++ BikeRental/AppHost/appsettings.json | 9 ++ .../BikeRental.Api/BikeRental.Api.csproj | 3 + .../BikeRental.Api/DependencyInjection.cs | 13 ++- .../Extensions/DatabaseExtensions.cs | 3 - BikeRental/BikeRental.Api/Program.cs | 6 +- .../BikeRental.Application.Contracts.csproj | 4 - .../Mappings/LeaseMappings.cs | 2 + BikeRental/BikeRental.Domain/Models/Lease.cs | 26 +++-- .../Services/Impl/SeedDataService.cs | 6 +- BikeRental/BikeRental.Tests/RentalFixture.cs | 110 ++++++++++++++++-- BikeRental/BikeRental.sln | 6 + 15 files changed, 237 insertions(+), 38 deletions(-) create mode 100644 BikeRental/AppHost/AppHost.csproj create mode 100644 BikeRental/AppHost/Program.cs create mode 100644 BikeRental/AppHost/Properties/launchSettings.json create mode 100644 BikeRental/AppHost/appsettings.Development.json create mode 100644 BikeRental/AppHost/appsettings.json diff --git a/BikeRental/AppHost/AppHost.csproj b/BikeRental/AppHost/AppHost.csproj new file mode 100644 index 000000000..20e1d8189 --- /dev/null +++ b/BikeRental/AppHost/AppHost.csproj @@ -0,0 +1,29 @@ + + + + + + Exe + net8.0 + enable + enable + fa0efc8b-9978-4cb9-83d3-bbcd25eab020 + + + + + + + + + + + appsettings.json + + + + + + + + diff --git a/BikeRental/AppHost/Program.cs b/BikeRental/AppHost/Program.cs new file mode 100644 index 000000000..b4b9e2597 --- /dev/null +++ b/BikeRental/AppHost/Program.cs @@ -0,0 +1,19 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var bikeRentalDbPassword = builder.AddParameter( + name: "bike-rental-db-password", + value: "1234512345Aa$", + secret: true); +var bikeRentalSql = builder.AddMySql("bike-rental-db", + password: bikeRentalDbPassword) + .WithAdminer() + .WithDataVolume("bike-rental-volume"); + +var bikeRentalDb = + bikeRentalSql.AddDatabase("bike-rental"); + +builder.AddProject("bike-rental-api") + .WaitFor(bikeRentalDb) + .WithReference(bikeRentalDb); + +builder.Build().Run(); \ No newline at end of file diff --git a/BikeRental/AppHost/Properties/launchSettings.json b/BikeRental/AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..9103756f1 --- /dev/null +++ b/BikeRental/AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17195;http://localhost:15246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21053", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22270", + "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19196", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20132", + "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true" + } + } + } +} diff --git a/BikeRental/AppHost/appsettings.Development.json b/BikeRental/AppHost/appsettings.Development.json new file mode 100644 index 000000000..1b2d3bafd --- /dev/null +++ b/BikeRental/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/BikeRental/AppHost/appsettings.json b/BikeRental/AppHost/appsettings.json new file mode 100644 index 000000000..888f884e2 --- /dev/null +++ b/BikeRental/AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index 41c4d6495..1fc9d06a8 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -4,6 +4,9 @@ net8.0 enable enable + Linux + bin\Debug\net8.0\BikeRental.Api.xml + ..\.. diff --git a/BikeRental/BikeRental.Api/DependencyInjection.cs b/BikeRental/BikeRental.Api/DependencyInjection.cs index a275f21e0..0e8a41799 100644 --- a/BikeRental/BikeRental.Api/DependencyInjection.cs +++ b/BikeRental/BikeRental.Api/DependencyInjection.cs @@ -4,12 +4,10 @@ using BikeRental.Domain.Interfaces; using BikeRental.Infrastructure.Database; using BikeRental.Infrastructure.Repositories; -using Microsoft.AspNetCore.Builder; +using BikeRental.Infrastructure.Services; +using BikeRental.Infrastructure.Services.Impl; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -31,8 +29,8 @@ public static void AddControllers(this WebApplicationBuilder builder) { options.ReturnHttpNotAcceptable = false; // 406 }) - .AddNewtonsoftJson() // заменить стандартный JSON на Newtonsoft.json - .AddXmlSerializerFormatters(); // отвечать в XML формате + .AddNewtonsoftJson(); // заменить стандартный JSON на Newtonsoft.json + //.AddXmlSerializerFormatters(); // отвечать в XML формате } /// @@ -110,6 +108,9 @@ public static void AddRepositories(this WebApplicationBuilder builder) /// public static void AddServices(this WebApplicationBuilder builder) { + // Зарегистрировать сервис инициализации данных + builder.Services.AddScoped(); + // Зарегистрировать сервисы прикладного уровня builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs index f9297015b..be7a71e99 100644 --- a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -1,8 +1,5 @@ using BikeRental.Infrastructure.Database; -using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace BikeRental.Api.Extensions; diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index 546bc342d..6077948ba 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -1,8 +1,5 @@ using BikeRental.Api; using BikeRental.Api.Extensions; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; // Создать объект WebApplicationBuilder (построитель веб-приложения) @@ -57,6 +54,9 @@ // Применить миграции базы данных (из DatabaceExtensions) await app.ApplyMigrationsAsync(); + + // Инициализировать данные в бд + await app.SeedData(); } // Использовать обработчики исключений (GlobalExceptionHandler, ValidationExceptionHandler) diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj index 0a85e0c57..44ea37af1 100644 --- a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -9,8 +9,4 @@ - - - - diff --git a/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs index fb415541b..d4632177d 100644 --- a/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs @@ -21,6 +21,8 @@ public static Lease ToEntity(this LeaseCreateUpdateDto dto, Bike bike, Renter re { return new Lease { + BikeId = bike.Id, + RenterId = renter.Id, Bike = bike, Renter = renter, RentalStartTime = dto.RentalStartTime, diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index 81fabb03f..67e9b6fbe 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace BikeRental.Domain.Models; /// @@ -10,15 +12,11 @@ public class Lease /// public int Id { get; set; } - /// - /// Person who rents a bike - /// - public required Renter Renter { get; set; } - - /// - /// Bike for rent - /// - public required Bike Bike { get; set; } + [ForeignKey(nameof(Bike))] + public required int BikeId { get; set; } + + [ForeignKey(nameof(Renter))] + public required int RenterId { get; set; } /// /// Rental start time @@ -29,4 +27,14 @@ public class Lease /// Rental duration in hours /// public required int RentalDuration { get; set; } + + /// + /// Person who rents a bike + /// + public virtual Renter Renter { get; set; } + + /// + /// Bike for rent + /// + public virtual Bike Bike { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs index 176374680..b1dad7c9b 100644 --- a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs +++ b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs @@ -32,7 +32,7 @@ public async Task SeedDataAsync() WheelSize = faker.Random.Int(20, 29), MaxCyclistWeight = faker.Random.Int(60, 120), Weight = Math.Round(faker.Random.Double(7.0, 15.0), 2), - BrakeType = faker.PickRandom("Disc", "Rim", "Drum"), + BrakeType = faker.PickRandom("Road", "Sport", "Mountain","Hybrid"), YearOfManufacture = faker.Date.Past(10).Year.ToString(), RentPrice = Math.Round(faker.Random.Decimal(5.0m, 20.0m), 2) }; @@ -93,8 +93,8 @@ public async Task SeedDataAsync() { var lease = new Lease { - Renter = faker.PickRandom(renters), - Bike = faker.PickRandom(bikes), + RenterId = faker.PickRandom(renters).Id, + BikeId = faker.PickRandom(bikes).Id, RentalStartTime = faker.Date.Recent(10), RentalDuration = faker.Random.Int(1, 72) }; diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index c5d8d233b..9b5aef2a0 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -150,15 +150,105 @@ private static List GetBikes(List models) => private static List GetLeases(List bikes, List renters) => [ - new() { Id = 1, Bike = bikes[0], Renter = renters[0], RentalStartTime = DateTime.Now.AddHours(-12), RentalDuration = 3 }, - new() { Id = 2, Bike = bikes[1], Renter = renters[1], RentalStartTime = DateTime.Now.AddHours(-8), RentalDuration = 6 }, - new() { Id = 3, Bike = bikes[2], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-15), RentalDuration = 4 }, - new() { Id = 4, Bike = bikes[3], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-5), RentalDuration = 2 }, - new() { Id = 5, Bike = bikes[4], Renter = renters[4], RentalStartTime = DateTime.Now.AddHours(-20), RentalDuration = 8 }, - new() { Id = 6, Bike = bikes[5], Renter = renters[5], RentalStartTime = DateTime.Now.AddHours(-3), RentalDuration = 1 }, - new() { Id = 7, Bike = bikes[6], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-18), RentalDuration = 5 }, - new() { Id = 8, Bike = bikes[7], Renter = renters[6], RentalStartTime = DateTime.Now.AddHours(-7), RentalDuration = 7 }, - new() { Id = 9, Bike = bikes[8], Renter = renters[8], RentalStartTime = DateTime.Now.AddHours(-10), RentalDuration = 4 }, - new() { Id = 10, Bike = bikes[9], Renter = renters[9], RentalStartTime = DateTime.Now.AddHours(-2), RentalDuration = 4 }, + new() + { + Id = 1, + Bike = bikes[0], + Renter = renters[0], + RentalStartTime = DateTime.Now.AddHours(-12), + RentalDuration = 3, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 2, + Bike = bikes[1], + Renter = renters[1], + RentalStartTime = DateTime.Now.AddHours(-8), + RentalDuration = 6, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 3, + Bike = bikes[2], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-15), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 4, + Bike = bikes[3], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-5), + RentalDuration = 2, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 5, + Bike = bikes[4], + Renter = renters[4], + RentalStartTime = DateTime.Now.AddHours(-20), + RentalDuration = 8, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 6, + Bike = bikes[5], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-3), + RentalDuration = 1, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 7, + Bike = bikes[6], + Renter = renters[6], + RentalStartTime = DateTime.Now.AddHours(-18), + RentalDuration = 5, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 8, + Bike = bikes[7], + Renter = renters[6], + RentalStartTime = DateTime.Now.AddHours(-7), + RentalDuration = 7, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 9, + Bike = bikes[8], + Renter = renters[8], + RentalStartTime = DateTime.Now.AddHours(-10), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 10, + Bike = bikes[9], + Renter = renters[9], + RentalStartTime = DateTime.Now.AddHours(-2), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + }, ]; } \ No newline at end of file diff --git a/BikeRental/BikeRental.sln b/BikeRental/BikeRental.sln index 61720c520..5d431361d 100644 --- a/BikeRental/BikeRental.sln +++ b/BikeRental/BikeRental.sln @@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Infrastructure", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Application.Contracts", "BikeRental.Application.Contracts\BikeRental.Application.Contracts.csproj", "{A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "AppHost\AppHost.csproj", "{FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Debug|Any CPU.Build.0 = Debug|Any CPU {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Release|Any CPU.ActiveCfg = Release|Any CPU {A637BFD7-2BE5-4B58-AB34-8330FE4D6B43}.Release|Any CPU.Build.0 = Release|Any CPU + {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 9c14f8bca2e1724c6bee4da157749cf23831d7be Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Fri, 19 Dec 2025 02:03:24 +0400 Subject: [PATCH 31/48] upd README.md for lab 2 and 3 --- README.md | 166 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 145 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a92ff9ee8..243dd7123 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,155 @@ # Разработка корпоративных приложений ## Задание + ### Цель Реализация проекта сервисно-ориентированного приложения. + ### Предметная область «Пункт велопроката» В базе пункта проката хранятся сведения о велосипедах, их арендаторах и выданных в аренду транспортных средствах. -Велосипед характеризуется серийным номером, моделью, цветом. -Модель велосипеда является справочником, содержащим сведения о типе велосипеда, размере колес, предельно допустимом весе пассажира, весе велосипеда, типе тормозов, модельном годе. Для каждой модели велосипеда указывается цена часа аренды. -Тип велосипеда является перечислением. -Арендатор характеризуется ФИО, телефоном. -При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах. -Используется в качестве контракта. -### Задание на лабораторную 1: - -В рамках первой лабораторной работы была подготовлена структура классов, описывающая предметную область: -* класс Bike. Велосипед характеризуется серийным номером, моделью, цветом - поля: SerialNumber, Model, Color соответственно, а также уникальный индефикатор велосипеда - поле Id. -* класс BikeModel. Модель велосипеда является справочником, содержащим сведения о типе велосипеда, размере колес, предельно допустимом весе пассажира, весе велосипеда, типе тормозов, модельном годе и цены часа аренды - поля: Type, WheelSize, MaxСyclistWeight, Weight, BrakeType, YearOfManufacture,RentPrice соответственно, а также уникальный индефикатор модели - поле Id. -* класс Renter. Арендатор характеризуется ФИО, телефоном - поля: FullName, PhoneNumber соответственно, а также уникальный индефикатор арендатора - поле Id. -* класс Lease используется в качестве контракта. -В нем указан как адендатор (поле Renter), так и арендованый велосипед (поле Bike), а также уникальный индефикатор контракта аренды - поле Id. При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах - за это отвечают поля RentalStartTime и RentalDuration соответственно. - -Было включено 10 экземпляров каждого класса в датасид(RentalFixture) и реализованы **юнит-тесты**: -* InfoAboutSportBikes - Выводит информацию обо всех спортивных велосипедах. -* TopFiveModelsIncome и TopFiveModelsDuration - Выводят топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). -* MinMaxAvgRental - Выводит информацию о минимальном, максимальном и среднем времени аренды велосипедов. -* TotalRentalTimeByType - Выводит суммарное время аренды велосипедов каждого типа. -* TopThreeRenters - Выводит информацию о клиентах, бравших велосипеды на прокат больше всего раз. +**Велосипед** характеризуется: +- Серийным номером +- Моделью +- Цветом + +**Модель велосипеда** является справочником, содержащим сведения о: +- Типе велосипеда +- Размере колес +- Предельно допустимом весе пассажира +- Весе велосипеда +- Типе тормозов +- Модельном годе +- Цене часа аренды + +**Тип велосипеда** является перечислением. + +**Арендатор** характеризуется: +- ФИО +- Телефоном + +**Аренда (контракт):** +- При выдаче велосипеда фиксируется время начала аренды +- Отмечается продолжительность аренды в часах + +--- + +## Задание на лабораторную работу №1 + +### Подготовленная структура классов + +#### Класс `Bike` (Велосипед) +| Поле | Описание | +|------|----------| +| `Id` | Уникальный идентификатор | +| `SerialNumber` | Серийный номер | +| `Model` | Модель | +| `Color` | Цвет | + +#### Класс `BikeModel` (Модель велосипеда) +| Поле | Описание | +|------|----------| +| `Id` | Уникальный идентификатор | +| `Type` | Тип велосипеда | +| `WheelSize` | Размер колес | +| `MaxCyclistWeight` | Максимальный вес велосипедиста | +| `Weight` | Вес велосипеда | +| `BrakeType` | Тип тормозов | +| `YearOfManufacture` | Год выпуска модели | +| `RentPrice` | Цена часа аренды | + +#### Класс `Renter` (Арендатор) +| Поле | Описание | +|------|----------| +| `Id` | Уникальный идентификатор | +| `FullName` | ФИО | +| `PhoneNumber` | Номер телефона | + +#### Класс `Lease` (Контракт аренды) +| Поле | Описание | +|------|----------| +| `Id` | Уникальный идентификатор | +| `Renter` | Арендатор | +| `Bike` | Арендованный велосипед | +| `RentalStartTime` | Время начала аренды | +| `RentalDuration` | Продолжительность аренды (в часах) | + +--- + +### Реализованные компоненты + +#### 1. **Датасет** (`RentalFixture`) +- Включено по 10 экземпляров каждого класса + +#### 2. **Юнит-тесты** + +| Название теста | Описание | +|----------------|----------| +| `InfoAboutSportBikes` | Выводит информацию обо всех спортивных велосипедах | +| `TopFiveModelsIncome` | Выводит топ-5 моделей велосипедов по прибыли от аренды | +| `TopFiveModelsDuration` | Выводит топ-5 моделей велосипедов по длительности аренды | +| `MinMaxAvgRental` | Выводит минимальное, максимальное и среднее время аренды | +| `TotalRentalTimeByType` | Выводит суммарное время аренды велосипедов каждого типа | +| `TopThreeRenters` | Выводит информацию о клиентах, бравших велосипеды на прокат больше всего раз | + +--- +- .NET (C#) +- xUnit (для юнит-тестов) + +# Лабораторные работы №2-3: REST API + ORM + Aspire + +### **База данных MySQL + EF Core** +- Доменные модели с внешними ключами: + ```csharp + public class Bike { + public int ModelId { get; set; } // FK to BikeModel + public virtual BikeModel Model { get; init; } + } + +* Репозитории с асинхронными методами +* Миграции EF Core для создания схемы БД +* Контекст ApplicationDbContext с конфигурациями +#### **Генерация данных:** +```csharp + new BikeModel + { + Type = faker.PickRandom(), // Случайный тип из enum + WheelSize = faker.Random.Int(20, 29), // Размер колёс 20-29" + MaxCyclistWeight = faker.Random.Int(60, 120), // Вес 60-120 кг + Weight = Math.Round(faker.Random.Double(7.0, 15.0), 2), // Вес велосипеда + BrakeType = faker.PickRandom("Road", "Sport", "Mountain", "Hybrid"), + YearOfManufacture = faker.Date.Past(10).Year.ToString(), // Год выпуска + RentPrice = Math.Round(faker.Random.Decimal(5.0m, 20.0m), 2) // Цена аренды + } +``` +Bogus генерировал реалистичные тестовые данные: +* Русские ФИО, телефоны +* Параметры велосипедов (размеры, вес, цены) +* Даты аренд +* Связи между таблицами +* `var faker = new Faker("ru");` +### **REST API на ASP.NET Core** +- CRUD-операции для всех сущностей (Bikes, BikeModels, Renters, Leases) +- Тесты xUnit адаптированы + +#### Запуск и порты: +- Запустить проект через AppHost +- **Aspire Dashboard:** автоматически открывается при запуске +- **Adminer для БД:** доступен через Dashboard +- `https://localhost:21053` - для метрик +- `https://localhost:17195` +- `http://localhost:15246` - порты API +- Используется `launchSettings.json` с http и https +#### Администрирование БД через Adminer +1. в Aspire Dashboard +2. Найти контейнер `bike_rental_db_adminer` +3. Войти с данными: +* Система: MySQL +* Сервер: `bike-rental-db` +* Пользователь: `root` +* Пароль: `1234512345Aa$` +* База данных: bike-rental - не обязательно +### Swagger +- Для получения доступа к backend: `http://localhost:5043/swagger/index.html` - клиент для тестирования \ No newline at end of file From feb10bba3d456784c81af1e35087a69cccdc059d Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Fri, 19 Dec 2025 19:22:39 +0400 Subject: [PATCH 32/48] small fixes, cleaned up code --- .../Controllers/BikeModelsController.cs | 17 +------- .../BikeRental.Api/DependencyInjection.cs | 9 ++--- .../Extensions/DatabaseExtensions.cs | 11 +---- .../Middleware/GlobalExceptionHandler.cs | 17 +++----- BikeRental/BikeRental.Api/Program.cs | 20 +--------- .../Dtos/BikeDto.cs | 3 +- .../Dtos/LeaseDto.cs | 3 -- .../BikeRental.Application.csproj | 2 - .../Services/BikeModelService.cs | 9 ++--- .../Services/BikeService.cs | 8 ++-- .../Services/LeaseService.cs | 40 +++++++------------ .../Services/RenterService.cs | 8 ++-- .../Interfaces/IBikeModelRepository.cs | 4 +- .../Interfaces/IBikeRepository.cs | 4 +- .../Interfaces/ILeaseRepository.cs | 4 +- .../Interfaces/IRenterRepository.cs | 4 +- .../Migrations/20251215170139_Initial.cs | 3 +- 17 files changed, 46 insertions(+), 120 deletions(-) diff --git a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs index 7ef11f2ce..37a7f8139 100644 --- a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs +++ b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs @@ -20,7 +20,6 @@ public sealed class BikeModelsController(IBikeModelService bikeModelService) : C [HttpGet] public async Task>> GetAll() { - // Обратиться к репозиторию для получения всех моделей велосипедов var models = await bikeModelService.GetAll(); return Ok(models); } @@ -28,14 +27,12 @@ public async Task>> GetAll() /// /// Получить модель велосипеда по идентификатору /// - [HttpGet("{id:int}")] // ограничение - ID должен быть числом + [HttpGet("{id:int}")] public async Task> GetById(int id) { - // Обратиться к репозиторию для получения модели велосипеда по идентификатору var model = await bikeModelService.GetById(id); if (model is null) { - // вернуть код ответа 404 Not Found (не найдено) return NotFound(); } @@ -49,15 +46,9 @@ public async Task> GetById(int id) [HttpPost] public async Task> Create([FromBody] BikeModelCreateUpdateDto dto) { - // Обратиться к репозиторию для создания новой модели велосипеда - // с использованием данных из dto var created = await bikeModelService.Create(dto); - // Вернуть успешный результат обработки операции - // с кодом ответа 201 Created (создано) и созданной моделью велосипеда - - // Дополнительно вернуть в заголовке Location ссылку на созданный ресурс - return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } /// @@ -66,12 +57,9 @@ public async Task> Create([FromBody] BikeModelCreateU [HttpPut("{id:int}")] public async Task> Update(int id, [FromBody] BikeModelCreateUpdateDto dto) { - // Обратиться к репозиторию для обновления модели велосипеда по идентификатору - // с использованием данных из dto var updated = await bikeModelService.Update(id, dto); if (updated is null) { - // Если не найдена return NotFound(); } return Ok(updated); @@ -83,7 +71,6 @@ public async Task> Update(int id, [FromBody] BikeMode [HttpDelete("{id:int}")] public async Task Delete(int id) { - // Обратиться к репозиторию для удаления модели велосипеда по идентификатору var deleted = await bikeModelService.Delete(id); if (!deleted) { diff --git a/BikeRental/BikeRental.Api/DependencyInjection.cs b/BikeRental/BikeRental.Api/DependencyInjection.cs index 0e8a41799..95eb67067 100644 --- a/BikeRental/BikeRental.Api/DependencyInjection.cs +++ b/BikeRental/BikeRental.Api/DependencyInjection.cs @@ -26,11 +26,10 @@ public static class DependencyInjection public static void AddControllers(this WebApplicationBuilder builder) { builder.Services.AddControllers(options => - { - options.ReturnHttpNotAcceptable = false; // 406 - }) - .AddNewtonsoftJson(); // заменить стандартный JSON на Newtonsoft.json - //.AddXmlSerializerFormatters(); // отвечать в XML формате + { + options.ReturnHttpNotAcceptable = false; // 406 + }) + .AddNewtonsoftJson(); // заменить стандартный JSON на Newtonsoft.json } /// diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs index be7a71e99..9db81241b 100644 --- a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -12,22 +12,15 @@ public static class DatabaseExtensions /// Применить все миграции к базе данных /// /// - public static async Task ApplyMigrationsAsync(this WebApplication app) // this делает метод расширением для типа WebApplication + public static async Task ApplyMigrationsAsync(this WebApplication app) { - // мы не в HTTP запросе тк это запуск приложения - // поэтому создаем Scope(один из уровней DI контейнера) вручную, как бы новую область видимости для DI - // Scope гарантирует, что все зависимости будут правильно созданы и уничтожены using var scope = app.Services.CreateScope(); await using var dbContext = scope.ServiceProvider.GetRequiredService(); - // scope.ServiceProvider - DI контейнер в рамках созданного Scope - // GetRequiredService() - получить сервис типа T - // Требует, чтобы сервис был зарегистрирован, иначе исключение - // DbContext реализует IAsyncDisposable (асинхронное освобождение ресурсов) try { - await dbContext.Database.MigrateAsync(); // Применить все миграции к базе данных + await dbContext.Database.MigrateAsync(); app.Logger.LogInformation("Database migrations applied successfully."); } catch (Exception e) diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs index 3eb182d47..2c77faeff 100644 --- a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace BikeRental.Api.Middleware; @@ -7,8 +6,7 @@ namespace BikeRental.Api.Middleware; /// /// Глобальный обработчик исключений /// -/// -public sealed class GlobalExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler // - интерфейс в .NET 8 для обработки исключений +public sealed class GlobalExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler { /// /// Попытаться обработать исключение @@ -20,19 +18,14 @@ public ValueTask TryHandleAsync( { return problemDetailsService.TryWriteAsync(new ProblemDetailsContext { - HttpContext = httpContext, // получение информации о запросе - Exception = exception, // для логирования и диагностики - ProblemDetails = new ProblemDetails // базовая информация об ошибке + HttpContext = httpContext, + Exception = exception, + ProblemDetails = new ProblemDetails { Title = "Internal Server Error", Detail = "An error occurred while processing your request. Please try again" } - // TryWriteAsync() пишет ответ в поток HTTP - // 1. Пользователь кинул запрос например GET .../999 - // 2. Контроллер отсылает в NullReferenceException - тк bikeModelService.GetById(id) вернет null. - // 3. ASP.NET Core ловит исключение - // 4. Вызывает GlobalExceptionHandler.TryHandleAsync() - // 5. ProblemDetailsService генерирует JSON ответ + }); } } diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index 6077948ba..e7dcea8c4 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -2,12 +2,8 @@ using BikeRental.Api.Extensions; using Microsoft.OpenApi.Models; -// Создать объект WebApplicationBuilder (построитель веб-приложения) -// с использованием переданных аргументов командной строки var builder = WebApplication.CreateBuilder(args); -// Зарегистрировать и настроить сервисы контроллеров builder.AddControllers(); -// Зарегистрировать и настроить сервисы обработки ошибок builder.AddErrorHandling(); builder.Services.AddEndpointsApiExplorer(); @@ -20,26 +16,19 @@ Description = "API для управления сервисом проката велосипедов" }); - // описание XML документации var basePath = AppContext.BaseDirectory; var xmlPathApi = Path.Combine(basePath, $"BikeRental.Api.xml"); options.IncludeXmlComments(xmlPathApi); }); -// Зарегистрировать и настроить сервисы OpenTelemetry builder.AddObservability(); -// Зарегистрировать и настроить сервисы взаимодействия с базой данных builder.AddDatabase(); -// Зарегистрировать и настроить сервисы репозиториев builder.AddRepositories(); -// Зарегистрировать и настроить сервисы общего назначения builder.AddServices(); -// Создать конвейер обработки запросов var app = builder.Build(); -// Если приложение работает в режиме разработки, то if (app.Environment.IsDevelopment()) { // https://localhost:/swagger @@ -50,19 +39,14 @@ c.RoutePrefix = "swagger"; c.ShowCommonExtensions(); }); - - // Применить миграции базы данных (из DatabaceExtensions) await app.ApplyMigrationsAsync(); - // Инициализировать данные в бд await app.SeedData(); } -// Использовать обработчики исключений (GlobalExceptionHandler, ValidationExceptionHandler) app.UseExceptionHandler(); -// Зарегистрировать конечные точки контроллеров app.MapControllers(); -// Запустить приложение -await app.RunAsync().ConfigureAwait(false); \ No newline at end of file + +await app.RunAsync().ConfigureAwait(false); \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs index 18e752396..28e26c227 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs @@ -25,6 +25,5 @@ public class BikeDto /// public required int ModelId { get; set; } - // model information - //public BikeModelDto? Model { get; set; } + } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs index b8d7ace58..a049174d0 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs @@ -30,7 +30,4 @@ public class LeaseDto /// public required int RentalDuration { get; set; } - //public RenterDto? Renter { get; set; } - //public BikeDto? Bike { get; set; } - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj index f639569fa..7809b43c6 100644 --- a/BikeRental/BikeRental.Application/BikeRental.Application.csproj +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -7,9 +7,7 @@ - - diff --git a/BikeRental/BikeRental.Application/Services/BikeModelService.cs b/BikeRental/BikeRental.Application/Services/BikeModelService.cs index 8e241aa5b..f23a6d0d5 100644 --- a/BikeRental/BikeRental.Application/Services/BikeModelService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeModelService.cs @@ -7,7 +7,6 @@ namespace BikeRental.Application.Services; /// /// Application-сервис для работы с моделями велосипедов. Инкапсулирует бизнес-логику и доступ к репозиторию. -/// На текущем этапе является тонкой обёрткой над IBikeModelRepository. /// public sealed class BikeModelService(IBikeModelRepository bikeModelRepository) : IBikeModelService { @@ -38,11 +37,9 @@ public async Task Create(BikeModelCreateUpdateDto dto) public async Task Update(int id, BikeModelCreateUpdateDto dto) { - var createdEntity = await bikeModelRepository.GetById(id); - if (createdEntity == null) - { - throw new KeyNotFoundException($"Entity with id {id} not found."); - } + var createdEntity = await bikeModelRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + var entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; await bikeModelRepository.Update(entityToUpdate); diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs index d898f3b23..1249dd8bb 100644 --- a/BikeRental/BikeRental.Application/Services/BikeService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -38,11 +38,9 @@ public async Task Create(BikeCreateUpdateDto dto) public async Task Update(int id, BikeCreateUpdateDto dto) { - var createdEntity = await bikeRepository.GetById(id); - if (createdEntity == null) - { - throw new KeyNotFoundException($"Entity with id {id} not found."); - } + var createdEntity = await bikeRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + var entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; await bikeRepository.Update(entityToUpdate); diff --git a/BikeRental/BikeRental.Application/Services/LeaseService.cs b/BikeRental/BikeRental.Application/Services/LeaseService.cs index 25b864ee5..2dced7b27 100644 --- a/BikeRental/BikeRental.Application/Services/LeaseService.cs +++ b/BikeRental/BikeRental.Application/Services/LeaseService.cs @@ -28,16 +28,12 @@ public async Task> GetAll() public async Task Create(LeaseCreateUpdateDto dto) { - var bike = await bikeRepository.GetById(dto.BikeId); - if (bike == null) - { - throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); - } - var renter = await renterRepository.GetById(dto.RenterId); - if (renter == null) - { - throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); - } + var bike = await bikeRepository.GetById(dto.BikeId) + ?? throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + + var renter = await renterRepository.GetById(dto.RenterId) + ?? throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + var id = await leaseRepository.Add(dto.ToEntity(bike, renter)); if (id > 0) { @@ -52,21 +48,15 @@ public async Task Create(LeaseCreateUpdateDto dto) public async Task Update(int id, LeaseCreateUpdateDto dto) { - var createdEntity = await leaseRepository.GetById(id); - if (createdEntity == null) - { - throw new KeyNotFoundException($"Entity with id {id} not found."); - } - var bike = await bikeRepository.GetById(dto.BikeId); - if (bike == null) - { - throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); - } - var renter = await renterRepository.GetById(dto.RenterId); - if (renter == null) - { - throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); - } + var createdEntity = await leaseRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + + var bike = await bikeRepository.GetById(dto.BikeId) + ?? throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + + var renter = await renterRepository.GetById(dto.RenterId) + ?? throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + var entityToUpdate = dto.ToEntity(bike, renter); entityToUpdate.Id = id; await leaseRepository.Update(entityToUpdate); diff --git a/BikeRental/BikeRental.Application/Services/RenterService.cs b/BikeRental/BikeRental.Application/Services/RenterService.cs index 88bfd3cc2..5763dc39a 100644 --- a/BikeRental/BikeRental.Application/Services/RenterService.cs +++ b/BikeRental/BikeRental.Application/Services/RenterService.cs @@ -38,11 +38,9 @@ public async Task Create(RenterCreateUpdateDto dto) public async Task Update(int id, RenterCreateUpdateDto dto) { - var createdEntity = await renterRepository.GetById(id); - if (createdEntity == null) - { - throw new KeyNotFoundException($"Entity with id {id} not found."); - } + var createdEntity = await renterRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + var entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; await renterRepository.Update(entityToUpdate); diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs index 8865ea5de..67e5b7d23 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs @@ -5,6 +5,4 @@ namespace BikeRental.Domain.Interfaces; /// /// Интерфейс репозитория описывает контракт для работы с моделями велосипедов /// -public interface IBikeModelRepository : IRepository -{ -} \ No newline at end of file +public interface IBikeModelRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs index fc696a261..bf74d8bf9 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs @@ -5,6 +5,4 @@ namespace BikeRental.Domain.Interfaces; /// /// Интерфейс репозитория описывает контракт для работы с велосипедами /// -public interface IBikeRepository : IRepository -{ -} \ No newline at end of file +public interface IBikeRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs index 843224e1a..6eabbff71 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs @@ -5,6 +5,4 @@ namespace BikeRental.Domain.Interfaces; /// /// Интерфейс репозитория описывает контракт для работы с договорами на аренду велосипедов /// -public interface ILeaseRepository : IRepository -{ -} \ No newline at end of file +public interface ILeaseRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs index 9a8627374..828acb64c 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs @@ -5,6 +5,4 @@ namespace BikeRental.Domain.Interfaces; /// /// Интерфейс репозитория описывает контракт для работы с арендаторами /// -public interface IRenterRepository : IRepository -{ -} \ No newline at end of file +public interface IRenterRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs index 7fb1ca6fc..ed4379f90 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Migrations/20251215170139_Initial.cs @@ -1,4 +1,5 @@ -using System; +// +using System; using Microsoft.EntityFrameworkCore.Migrations; using MySql.EntityFrameworkCore.Metadata; From 990ee4463ffac48fcea562780f79f636ee9737f1 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Sun, 21 Dec 2025 18:49:50 +0400 Subject: [PATCH 33/48] cleanded code --- .../Services/BikeService.cs | 13 ++++++++++--- .../Configurations/BikeConfiguration.cs | 19 ++++++------------- .../Configurations/BikeModelConfiguration.cs | 9 --------- .../Configurations/RenterConfiguration.cs | 4 ---- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs index 1249dd8bb..aad7fbdb5 100644 --- a/BikeRental/BikeRental.Application/Services/BikeService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -9,7 +9,7 @@ namespace BikeRental.Application.Services; /// Application-сервис для работы с велосипедами. Инкапсулирует бизнес-логику и доступ к репозиторию. /// На текущем этапе является тонкой обёрткой над IBikeRepository. /// -public sealed class BikeService(IBikeRepository bikeRepository) : IBikeService +public sealed class BikeService(IBikeRepository bikeRepository, IBikeModelRepository modelRepository) : IBikeService { public async Task> GetAll() { @@ -24,7 +24,11 @@ public async Task> GetAll() public async Task Create(BikeCreateUpdateDto dto) { - var id = await bikeRepository.Add(dto.ToEntity()); + var model = await modelRepository.GetById(dto.ModelId) + ?? throw new KeyNotFoundException($"Bike with id {dto.ModelId} not found."); + + var id = await bikeRepository.Add(dto.ToEntity(model)); + if (id > 0) { var createdEntity = await bikeRepository.GetById(id); @@ -41,7 +45,10 @@ public async Task Update(int id, BikeCreateUpdateDto dto) var createdEntity = await bikeRepository.GetById(id) ?? throw new KeyNotFoundException($"Entity with id {id} not found."); - var entityToUpdate = dto.ToEntity(); + var model = await modelRepository.GetById(dto.ModelId) + ?? throw new KeyNotFoundException($"Bike with id {dto.ModelId} not found."); + + var entityToUpdate = dto.ToEntity(model); entityToUpdate.Id = id; await bikeRepository.Update(entityToUpdate); var updatedEntity = await bikeRepository.GetById(id); diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs index 051b87c88..f04c6baf0 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs @@ -15,35 +15,28 @@ public class BikeConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - // Установить наименование таблицы builder.ToTable("Bikes"); - - // Первичный ключ + builder.HasKey(b => b.Id); builder.Property(b => b.Id) .ValueGeneratedOnAdd(); - - // Серийный номер велосипеда + builder.Property(b => b.SerialNumber) .IsRequired() .HasMaxLength(64); - - // Цвет + builder.Property(b => b.Color) .IsRequired() .HasMaxLength(32); - - // Внешний ключ на модель велосипеда + builder.Property(b => b.ModelId) .IsRequired(); - - // Навигация на модель велосипеда + builder.HasOne(b => b.Model) .WithMany() .HasForeignKey(b => b.ModelId) .OnDelete(DeleteBehavior.Restrict); - - // Уникальный индекс по серийному номеру + builder.HasIndex(b => b.SerialNumber) .IsUnique(); } diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs index 77e226c72..0af5cc4e3 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs @@ -15,41 +15,32 @@ public class BikeModelConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - // Установить наименование таблицы builder.ToTable("BikeModels"); - // Первичный ключ builder.HasKey(b => b.Id); builder.Property(b => b.Id) .ValueGeneratedOnAdd(); - // Тип велосипеда (enum BikeType) — храним как int по умолчанию builder.Property(b => b.Type) .IsRequired(); - // Размер колеса builder.Property(b => b.WheelSize) .IsRequired(); - // Максимальный вес велосипедиста builder.Property(b => b.MaxCyclistWeight) .IsRequired(); - // Вес велосипеда builder.Property(b => b.Weight) .IsRequired(); - // Тип тормозной системы builder.Property(b => b.BrakeType) .IsRequired() .HasMaxLength(50); - // Год выпуска модели (строка из 4 символов) builder.Property(b => b.YearOfManufacture) .IsRequired() .HasMaxLength(4); - // Стоимость аренды в час builder.Property(b => b.RentPrice) .IsRequired() .HasColumnType("decimal(10,2)"); diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs index 0024ee151..a7fa1cbf5 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs @@ -15,20 +15,16 @@ public class RenterConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - // Установить наименование таблицы builder.ToTable("Renters"); - // Первичный ключ builder.HasKey(r => r.Id); builder.Property(r => r.Id) .ValueGeneratedOnAdd(); - // Полное имя арендатора builder.Property(r => r.FullName) .IsRequired() .HasMaxLength(200); - // Номер телефона арендатора builder.Property(r => r.PhoneNumber) .IsRequired() .HasMaxLength(32); From d3cc69aa9319db97e098add545b0e43b3ee0bb60 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Mon, 22 Dec 2025 00:10:54 +0400 Subject: [PATCH 34/48] cleanup in Infrastructure only, minor changes in .csproj, Repositories --- .editorconfig | 23 ++++++++++--------- .../BikeRental.Api/BikeRental.Api.csproj | 2 -- .../Extensions/DatabaseExtensions.cs | 5 ++-- .../Dtos/BikeDto.cs | 5 ++-- .../Mappings/BikeMappings.cs | 7 +++--- BikeRental/BikeRental.Domain/Models/Bike.cs | 2 +- BikeRental/BikeRental.Domain/Models/Lease.cs | 9 ++++---- .../BikeRental.Infrastructure.csproj | 6 ++--- .../Database/ApplicationDbContext.cs | 16 +++++-------- .../Configurations/BikeConfiguration.cs | 14 +++++------ .../Configurations/BikeModelConfiguration.cs | 2 +- .../Configurations/LeaseConfiguration.cs | 23 +++++++++++-------- .../Configurations/RenterConfiguration.cs | 5 ++-- .../Repositories/BikeModelRepository.cs | 11 +++++---- .../Repositories/BikeRepository.cs | 14 ++++++----- .../Repositories/LeaseRepository.cs | 13 +++++++---- .../Repositories/RenterRepository.cs | 12 +++++----- .../Services/Impl/SeedDataService.cs | 7 +++--- 18 files changed, 91 insertions(+), 85 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0f3bba5ca..a8a58a0e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,6 +43,7 @@ csharp_style_var_for_built_in_types = true:error csharp_style_var_when_type_is_apparent = true:error csharp_style_var_elsewhere = false:silent csharp_space_around_binary_operators = before_and_after + [*.{cs,vb}] #### Naming styles #### @@ -64,31 +65,31 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index 1fc9d06a8..fb4a2addc 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -14,9 +14,7 @@ - - diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs index 9db81241b..33173f342 100644 --- a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -12,12 +12,11 @@ public static class DatabaseExtensions /// Применить все миграции к базе данных /// /// - public static async Task ApplyMigrationsAsync(this WebApplication app) + public static async Task ApplyMigrationsAsync(this WebApplication app) { using var scope = app.Services.CreateScope(); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); - + try { await dbContext.Database.MigrateAsync(); diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs index 28e26c227..da8a3f6fc 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs @@ -21,9 +21,8 @@ public class BikeDto public required string Color { get; set; } /// - /// Bike's model + /// Bike's model type /// - public required int ModelId { get; set; } - + public required string ModelType { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs index 4236bfe72..db162c0ad 100644 --- a/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs @@ -12,17 +12,18 @@ public static BikeDto ToDto(this Bike entity) Id = entity.Id, SerialNumber = entity.SerialNumber, Color = entity.Color, - ModelId = entity.ModelId + ModelType = entity.Model.BrakeType }; } - public static Bike ToEntity(this BikeCreateUpdateDto dto) + public static Bike ToEntity(this BikeCreateUpdateDto dto, BikeModel model) { return new Bike { SerialNumber = dto.SerialNumber, Color = dto.Color, - ModelId = dto.ModelId + ModelId = dto.ModelId, + Model = model, }; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs index 27eb8fd11..5c719b5f2 100644 --- a/BikeRental/BikeRental.Domain/Models/Bike.cs +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -28,5 +28,5 @@ public class Bike /// /// Bike's model /// - public virtual BikeModel Model { get; init; } = null!; + public required BikeModel Model { get; init; } = null!; } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index 67e9b6fbe..c35f6155d 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -13,10 +13,10 @@ public class Lease public int Id { get; set; } [ForeignKey(nameof(Bike))] - public required int BikeId { get; set; } + public int BikeId { get; set; } [ForeignKey(nameof(Renter))] - public required int RenterId { get; set; } + public int RenterId { get; set; } /// /// Rental start time @@ -31,10 +31,11 @@ public class Lease /// /// Person who rents a bike /// - public virtual Renter Renter { get; set; } + public required Renter Renter { get; set; } /// /// Bike for rent /// - public virtual Bike Bike { get; set; } + public required Bike Bike { get; set; } + // сделала required тогда их айди автоматически должны установиться EF core } \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj index c8140ce09..cb259a19e 100644 --- a/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj +++ b/BikeRental/BikeRental.Infrastructure/BikeRental.Infrastructure.csproj @@ -7,16 +7,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs b/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs index 0e3237aa1..811cb6e05 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/ApplicationDbContext.cs @@ -4,32 +4,28 @@ namespace BikeRental.Infrastructure.Database; /// -/// Контекст базы данных приложения -/// -/// Для создания миграции из командной строки: -/// Initial -Context ApplicationDbContext -OutputDir Database/Migrations -/// +/// Контекст базы данных приложения /// /// public sealed class ApplicationDbContext(DbContextOptions options) : DbContext(options) { /// - /// Набор сущностей "BikeModel" (Модель велосипеда) + /// Набор сущностей "BikeModel" (Модель велосипеда) /// public DbSet BikeModels { get; set; } /// - /// Набор сущностей "Bike" (Велосипед) + /// Набор сущностей "Bike" (Велосипед) /// public DbSet Bikes { get; set; } /// - /// Набор сущностей "Renter" (Арендатор) + /// Набор сущностей "Renter" (Арендатор) /// public DbSet Renters { get; set; } /// - /// Набор сущностей "Lease" (Договор аренды) + /// Набор сущностей "Lease" (Договор аренды) /// public DbSet Leases { get; set; } @@ -38,4 +34,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // Применить конфигурации из текущей сборки modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs index f04c6baf0..ef6c74326 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs @@ -16,28 +16,28 @@ public class BikeConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.ToTable("Bikes"); - + builder.HasKey(b => b.Id); builder.Property(b => b.Id) .ValueGeneratedOnAdd(); - + builder.Property(b => b.SerialNumber) .IsRequired() .HasMaxLength(64); - + builder.Property(b => b.Color) .IsRequired() .HasMaxLength(32); - + builder.Property(b => b.ModelId) .IsRequired(); - + builder.HasOne(b => b.Model) .WithMany() .HasForeignKey(b => b.ModelId) .OnDelete(DeleteBehavior.Restrict); - + builder.HasIndex(b => b.SerialNumber) .IsUnique(); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs index 0af5cc4e3..62c2bedb6 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs @@ -46,7 +46,7 @@ public void Configure(EntityTypeBuilder builder) .HasColumnType("decimal(10,2)"); // Индексы для типичных сценариев выборки - + // Индекс по типу велосипеда builder.HasIndex(b => b.Type); // Индекс по комбинации типа велосипеда и размера колеса diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs index 14e65a07e..2fdb8e89b 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/LeaseConfiguration.cs @@ -5,17 +5,16 @@ namespace BikeRental.Infrastructure.Database.Configurations; /// -/// Конфигурация сущности "Lease" +/// Конфигурация сущности "Lease" /// public class LeaseConfiguration : IEntityTypeConfiguration { /// - /// Настройка сущности "Lease" + /// Настройка сущности "Lease" /// /// public void Configure(EntityTypeBuilder builder) { - // Установить наименование таблицы builder.ToTable("Leases"); // Первичный ключ @@ -23,23 +22,29 @@ public void Configure(EntityTypeBuilder builder) builder.Property(l => l.Id) .ValueGeneratedOnAdd(); + builder.Property(l => l.RenterId) + .IsRequired(); + // Связь с арендатором builder.HasOne(l => l.Renter) - .WithMany() // коллекция договоров у Renter пока не определена + .WithMany() + .HasForeignKey(l => l.RenterId) + .IsRequired(); + + + builder.Property(l => l.BikeId) .IsRequired(); // Связь с велосипедом builder.HasOne(l => l.Bike) - .WithMany() // коллекция договоров у Bike пока не определена + .WithMany() + .HasForeignKey(l => l.BikeId) .IsRequired(); - // Дата и время начала аренды builder.Property(l => l.RentalStartTime) .IsRequired(); - // Продолжительность аренды builder.Property(l => l.RentalDuration) .IsRequired(); } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs index a7fa1cbf5..67089664d 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs @@ -28,10 +28,9 @@ public void Configure(EntityTypeBuilder builder) builder.Property(r => r.PhoneNumber) .IsRequired() .HasMaxLength(32); - + // Уникальный индекс по номеру телефона builder.HasIndex(r => r.PhoneNumber) .IsUnique(); } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs index f75c52594..77e75b8fa 100644 --- a/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs +++ b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs @@ -31,10 +31,11 @@ public async Task Add(BikeModel entity) public async Task Update(BikeModel entity) { - if (dbContext.BikeModels.Local.All(e => e.Id != entity.Id)) - { - dbContext.BikeModels.Attach(entity); - } + BikeModel existing = await dbContext.BikeModels.FindAsync(entity.Id) + ?? throw new KeyNotFoundException($"Model with id {entity.Id} not found."); + + dbContext.Entry(existing).CurrentValues.SetValues(entity); + await dbContext.SaveChangesAsync(); } @@ -43,4 +44,4 @@ public async Task Delete(BikeModel entity) dbContext.BikeModels.Remove(entity); await dbContext.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs index 9888cd39f..8d6c7322b 100644 --- a/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs +++ b/BikeRental/BikeRental.Infrastructure/Repositories/BikeRepository.cs @@ -10,12 +10,14 @@ public sealed class BikeRepository(ApplicationDbContext dbContext) : IBikeReposi public async Task> GetAll() { return await dbContext.Bikes + .Include(l => l.Model) .ToListAsync(); } public async Task GetById(int id) { return await dbContext.Bikes + .Include(l => l.Model) .FirstOrDefaultAsync(x => x.Id == id); } @@ -28,10 +30,11 @@ public async Task Add(Bike entity) public async Task Update(Bike entity) { - if (dbContext.Bikes.Local.All(e => e.Id != entity.Id)) - { - dbContext.Bikes.Attach(entity); - } + Bike existing = await dbContext.Bikes.FindAsync(entity.Id) + ?? throw new KeyNotFoundException($"Bike with id {entity.Id} not found."); + + dbContext.Entry(existing).CurrentValues.SetValues(entity); + await dbContext.SaveChangesAsync(); } @@ -40,5 +43,4 @@ public async Task Delete(Bike entity) dbContext.Bikes.Remove(entity); await dbContext.SaveChangesAsync(); } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs index 467338a2d..6b31f93b4 100644 --- a/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs +++ b/BikeRental/BikeRental.Infrastructure/Repositories/LeaseRepository.cs @@ -10,12 +10,16 @@ public sealed class LeaseRepository(ApplicationDbContext dbContext) : ILeaseRepo public async Task> GetAll() { return await dbContext.Leases + .Include(l => l.Bike) + .Include(l => l.Renter) .ToListAsync(); } public async Task GetById(int id) { return await dbContext.Leases + .Include(l => l.Bike) + .Include(l => l.Renter) .FirstOrDefaultAsync(x => x.Id == id); } @@ -28,10 +32,9 @@ public async Task Add(Lease entity) public async Task Update(Lease entity) { - if (dbContext.Leases.Local.All(e => e.Id != entity.Id)) - { - dbContext.Leases.Attach(entity); - } + Lease existing = await dbContext.Leases.FindAsync(entity.Id) + ?? throw new KeyNotFoundException($"Lease with id {entity.Id} not found."); + dbContext.Entry(existing).CurrentValues.SetValues(entity); await dbContext.SaveChangesAsync(); } @@ -40,4 +43,4 @@ public async Task Delete(Lease entity) dbContext.Leases.Remove(entity); await dbContext.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs index 131c08714..4f4740508 100644 --- a/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs +++ b/BikeRental/BikeRental.Infrastructure/Repositories/RenterRepository.cs @@ -28,10 +28,11 @@ public async Task Add(Renter entity) public async Task Update(Renter entity) { - if (dbContext.Renters.Local.All(e => e.Id != entity.Id)) - { - dbContext.Renters.Attach(entity); - } + Renter existing = await dbContext.Renters.FindAsync(entity.Id) + ?? throw new KeyNotFoundException($"Renter with id {entity.Id} not found."); + + dbContext.Entry(existing).CurrentValues.SetValues(entity); + await dbContext.SaveChangesAsync(); } @@ -40,5 +41,4 @@ public async Task Delete(Renter entity) dbContext.Renters.Remove(entity); await dbContext.SaveChangesAsync(); } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs index b1dad7c9b..8bc228f41 100644 --- a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs +++ b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs @@ -32,7 +32,7 @@ public async Task SeedDataAsync() WheelSize = faker.Random.Int(20, 29), MaxCyclistWeight = faker.Random.Int(60, 120), Weight = Math.Round(faker.Random.Double(7.0, 15.0), 2), - BrakeType = faker.PickRandom("Road", "Sport", "Mountain","Hybrid"), + BrakeType = faker.PickRandom("Road", "Sport", "Mountain", "Hybrid"), YearOfManufacture = faker.Date.Past(10).Year.ToString(), RentPrice = Math.Round(faker.Random.Decimal(5.0m, 20.0m), 2) }; @@ -89,12 +89,13 @@ public async Task SeedDataAsync() var renters = await dbContext.Renters.ToListAsync(); var bikes = await dbContext.Bikes.ToListAsync(); var leases = new List(); + for (var i = 0; i < 15; i++) { var lease = new Lease { - RenterId = faker.PickRandom(renters).Id, - BikeId = faker.PickRandom(bikes).Id, + Renter = faker.PickRandom(renters), + Bike = faker.PickRandom(bikes), RentalStartTime = faker.Date.Recent(10), RentalDuration = faker.Random.Int(1, 72) }; From 1f52fcb5b8deebefe63a1cebcde5e4f30818148c Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Mon, 22 Dec 2025 01:00:26 +0400 Subject: [PATCH 35/48] returned useful comments --- .../BikeRental.Api/Extensions/DatabaseExtensions.cs | 7 +++++++ .../Middleware/GlobalExceptionHandler.cs | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs index 33173f342..f53924464 100644 --- a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -14,8 +14,15 @@ public static class DatabaseExtensions /// public static async Task ApplyMigrationsAsync(this WebApplication app) { + // мы не в HTTP запросе тк это запуск приложения + // поэтому создаем Scope(один из уровней DI контейнера) вручную, как бы новую область видимости для DI + // Scope гарантирует, что все зависимости будут правильно созданы и уничтожены using var scope = app.Services.CreateScope(); await using var dbContext = scope.ServiceProvider.GetRequiredService(); + // scope.ServiceProvider - DI контейнер в рамках созданного Scope + // GetRequiredService() - получить сервис типа T + // Требует, чтобы сервис был зарегистрирован, иначе исключение + // DbContext реализует IAsyncDisposable (асинхронное освобождение ресурсов) try { diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs index 2c77faeff..51e78b23c 100644 --- a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace BikeRental.Api.Middleware; @@ -25,7 +26,12 @@ public ValueTask TryHandleAsync( Title = "Internal Server Error", Detail = "An error occurred while processing your request. Please try again" } - + // TryWriteAsync() пишет ответ в поток HTTP + // 1. Пользователь кинул запрос например GET .../999 + // 2. Контроллер отсылает в NullReferenceException - тк bikeModelService.GetById(id) вернет null. + // 3. ASP.NET Core ловит исключение + // 4. Вызывает GlobalExceptionHandler.TryHandleAsync() + // 5. ProblemDetailsService генерирует JSON ответ }); } -} +} \ No newline at end of file From bac5a49a63df309ecdb3cff6f94bf2e01603b8de Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Mon, 22 Dec 2025 18:22:33 +0400 Subject: [PATCH 36/48] added logging to the global exception handler and fixed exception output in servers, disabled default ASP.NET logging for clean console output --- .../BikeRental.Api/DependencyInjection.cs | 3 +- .../Middleware/GlobalExceptionHandler.cs | 149 ++++++++++++++++-- BikeRental/BikeRental.Api/Program.cs | 5 +- .../appsettings.Development.json | 10 +- BikeRental/BikeRental.Api/appsettings.json | 21 +-- .../Services/BikeService.cs | 6 +- .../Services/LeaseService.cs | 10 +- 7 files changed, 166 insertions(+), 38 deletions(-) diff --git a/BikeRental/BikeRental.Api/DependencyInjection.cs b/BikeRental/BikeRental.Api/DependencyInjection.cs index 95eb67067..ed7877d62 100644 --- a/BikeRental/BikeRental.Api/DependencyInjection.cs +++ b/BikeRental/BikeRental.Api/DependencyInjection.cs @@ -28,8 +28,7 @@ public static void AddControllers(this WebApplicationBuilder builder) builder.Services.AddControllers(options => { options.ReturnHttpNotAcceptable = false; // 406 - }) - .AddNewtonsoftJson(); // заменить стандартный JSON на Newtonsoft.json + }); } /// diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs index 51e78b23c..a83ddda44 100644 --- a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -1,37 +1,158 @@ using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace BikeRental.Api.Middleware; /// -/// Глобальный обработчик исключений +/// Глобальный обработчик исключений с логированием /// -public sealed class GlobalExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler -{ +public sealed class GlobalExceptionHandler : IExceptionHandler +{ + private readonly IProblemDetailsService _problemDetailsService; // сервис для создания ответов об ошибках + private readonly ILogger _logger; + + /// + /// Инициализирует новый экземпляр глобального обработчика исключений с зависимостями для логирования и генерации ProblemDetails. + /// + public GlobalExceptionHandler( + IProblemDetailsService problemDetailsService, + ILogger logger) + { + _problemDetailsService = problemDetailsService; + _logger = logger; + } + /// /// Попытаться обработать исключение /// - public ValueTask TryHandleAsync( + public async ValueTask TryHandleAsync( HttpContext httpContext, Exception exception, - CancellationToken cancellationToken) + CancellationToken cancellationToken) { - return problemDetailsService.TryWriteAsync(new ProblemDetailsContext + // понятным сообщением сделать логи + LogExceptionWithSimpleMessage(httpContext, exception); + + var problemDetails = CreateProblemDetails(exception); + + return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext { HttpContext = httpContext, Exception = exception, - ProblemDetails = new ProblemDetails - { - Title = "Internal Server Error", - Detail = "An error occurred while processing your request. Please try again" - } - // TryWriteAsync() пишет ответ в поток HTTP + ProblemDetails = problemDetails // 1. Пользователь кинул запрос например GET .../999 // 2. Контроллер отсылает в NullReferenceException - тк bikeModelService.GetById(id) вернет null. // 3. ASP.NET Core ловит исключение // 4. Вызывает GlobalExceptionHandler.TryHandleAsync() - // 5. ProblemDetailsService генерирует JSON ответ + // 5. логируем ошибку и создаем ProblemDetails, ProblemDetailsService генерирует JSON ответ + + // Возвращает true, если исключение было успешно обработано, false - если нужно пробросить дальше + // TryWriteAsync возвращает false - клиент получает дефолтный ответ (500) + // клиент не узнаёт что именно не так + // но в консольке все выводится }); } + + /// + /// Логирование с короткими понятными сообщениями + /// + private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception exception) + { + var requestPath = httpContext.Request.Path; + var method = httpContext.Request.Method; + var exceptionType = exception.GetType().Name; + + // Основное понятное сообщение + var message = exception switch + { + KeyNotFoundException keyEx => $"Resource not found: {keyEx.Message.Replace("not found", "")}", + ArgumentException argEx => $"Invalid input: {argEx.Message}", + InvalidOperationException opEx => $"Invalid operation: {opEx.Message}", + UnauthorizedAccessException => "Access denied", + _ => "Internal server error" + }; + + // Для 404 и 400 - Warning с кратким сообщением + if (exception is KeyNotFoundException or ArgumentException or InvalidOperationException) + { + _logger.LogWarning( + "[{StatusCode}] {Method} {Path} - {Message}", + GetStatusCode(exception), + method, + requestPath, + message); + } + // Для остальных - Error с полным stack trace + else + { + _logger.LogError( + exception, + "[{StatusCode}] {Method} {Path} - {ExceptionType}: {Message}", + GetStatusCode(exception), + method, + requestPath, + exceptionType, + message); + } + } + + /// + /// Создание ProblemDetails + /// + private ProblemDetails CreateProblemDetails(Exception exception) + { + var statusCode = GetStatusCode(exception); + + return new ProblemDetails + { + Title = GetTitle(exception), + Detail = GetDetail(exception), + Status = statusCode + }; + } + + /// + /// Получение статус кода + /// + private int GetStatusCode(Exception exception) + { + return exception switch + { + KeyNotFoundException => StatusCodes.Status404NotFound, + ArgumentException => StatusCodes.Status400BadRequest, + InvalidOperationException => StatusCodes.Status400BadRequest, + UnauthorizedAccessException => StatusCodes.Status401Unauthorized, + _ => StatusCodes.Status500InternalServerError + }; + } + + /// + /// Получение заголовка + /// + private string GetTitle(Exception exception) + { + return exception switch + { + KeyNotFoundException => "Resource not found", + ArgumentException => "Bad request", + InvalidOperationException => "Invalid operation", + UnauthorizedAccessException => "Unauthorized", + _ => "Internal server error" + }; + } + + /// + /// Получение деталей + /// + private string GetDetail(Exception exception) + { + // Для клиентских ошибок показываем сообщение исключения + if (exception is KeyNotFoundException or ArgumentException or InvalidOperationException) + { + return exception.Message; + } + + // Для серверных ошибок - общее сообщение + return "An error occurred while processing your request. Please try again later."; + } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index e7dcea8c4..2c4827f79 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -3,6 +3,7 @@ using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); + builder.AddControllers(); builder.AddErrorHandling(); @@ -29,6 +30,8 @@ var app = builder.Build(); +app.UseExceptionHandler(); + if (app.Environment.IsDevelopment()) { // https://localhost:/swagger @@ -45,8 +48,6 @@ await app.SeedData(); } -app.UseExceptionHandler(); - app.MapControllers(); await app.RunAsync().ConfigureAwait(false); \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/appsettings.Development.json b/BikeRental/BikeRental.Api/appsettings.Development.json index a66c3ef14..031328052 100644 --- a/BikeRental/BikeRental.Api/appsettings.Development.json +++ b/BikeRental/BikeRental.Api/appsettings.Development.json @@ -4,9 +4,13 @@ }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Warning", + + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", + "Microsoft": "Error", + "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None", + + "BikeRental": "Information" } } } - diff --git a/BikeRental/BikeRental.Api/appsettings.json b/BikeRental/BikeRental.Api/appsettings.json index 204e3d4c7..a4ae2f053 100644 --- a/BikeRental/BikeRental.Api/appsettings.json +++ b/BikeRental/BikeRental.Api/appsettings.json @@ -1,11 +1,14 @@ { - "ConnectionStrings": { - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "ConnectionStrings": { + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Error", + "Microsoft.AspNetCore": "Error", + "Microsoft.EntityFrameworkCore": "Error", + "BikeRental": "Warning" + } + }, + "AllowedHosts": "*" } diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs index aad7fbdb5..a210effa3 100644 --- a/BikeRental/BikeRental.Application/Services/BikeService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -25,7 +25,7 @@ public async Task> GetAll() public async Task Create(BikeCreateUpdateDto dto) { var model = await modelRepository.GetById(dto.ModelId) - ?? throw new KeyNotFoundException($"Bike with id {dto.ModelId} not found."); + ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); var id = await bikeRepository.Add(dto.ToEntity(model)); @@ -43,10 +43,10 @@ public async Task Create(BikeCreateUpdateDto dto) public async Task Update(int id, BikeCreateUpdateDto dto) { var createdEntity = await bikeRepository.GetById(id) - ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + ?? throw new KeyNotFoundException($"Bike with id {id} not found."); var model = await modelRepository.GetById(dto.ModelId) - ?? throw new KeyNotFoundException($"Bike with id {dto.ModelId} not found."); + ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); var entityToUpdate = dto.ToEntity(model); entityToUpdate.Id = id; diff --git a/BikeRental/BikeRental.Application/Services/LeaseService.cs b/BikeRental/BikeRental.Application/Services/LeaseService.cs index 2dced7b27..4921c08f9 100644 --- a/BikeRental/BikeRental.Application/Services/LeaseService.cs +++ b/BikeRental/BikeRental.Application/Services/LeaseService.cs @@ -29,10 +29,10 @@ public async Task> GetAll() public async Task Create(LeaseCreateUpdateDto dto) { var bike = await bikeRepository.GetById(dto.BikeId) - ?? throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); var renter = await renterRepository.GetById(dto.RenterId) - ?? throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); var id = await leaseRepository.Add(dto.ToEntity(bike, renter)); if (id > 0) @@ -49,13 +49,13 @@ public async Task Create(LeaseCreateUpdateDto dto) public async Task Update(int id, LeaseCreateUpdateDto dto) { var createdEntity = await leaseRepository.GetById(id) - ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + ?? throw new KeyNotFoundException($"Lease with id {id} not found."); var bike = await bikeRepository.GetById(dto.BikeId) - ?? throw new KeyNotFoundException($"Bike with id {dto.BikeId} not found."); + ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); var renter = await renterRepository.GetById(dto.RenterId) - ?? throw new KeyNotFoundException($"Renter with id {dto.RenterId} not found."); + ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); var entityToUpdate = dto.ToEntity(bike, renter); entityToUpdate.Id = id; From 17cbdb3a416255f1c1ba7843d950da015684b93b Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Mon, 22 Dec 2025 18:43:31 +0400 Subject: [PATCH 37/48] removed useless ConfigureAwait(false) --- BikeRental/BikeRental.Api/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index 2c4827f79..38cd6cd89 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -50,4 +50,4 @@ app.MapControllers(); -await app.RunAsync().ConfigureAwait(false); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file From edbecf824616bf10ab2e5c3edd66ca73c606ca06 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Mon, 22 Dec 2025 19:13:51 +0400 Subject: [PATCH 38/48] added validation attributes to all DTOs for creation/deletion --- .../Dtos/BikeCreateUpdateDto.cs | 9 ++++++++- .../Dtos/BikeModelCreateUpdateDto.cs | 14 ++++++++++++++ .../Dtos/LeaseCreateUpdateDto.cs | 9 +++++++++ .../Dtos/RenterCreateUpdateDto.cs | 6 ++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs index 2da728fdf..06b0a8722 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace BikeRental.Application.Contracts.Dtos; public class BikeCreateUpdateDto @@ -5,16 +7,21 @@ public class BikeCreateUpdateDto /// /// Bike's serial number /// + [Required] + [StringLength(50, MinimumLength = 3, ErrorMessage = "Длина SerialNumber должна быть 3-50 символов.")] public required string SerialNumber { get; set; } /// /// Bike's color /// + [Required] + [StringLength(20, ErrorMessage = "Макс. длина Color 20 символов.")] public required string Color { get; set; } /// /// Bike's model /// + [Required] + [Range(1, int.MaxValue, ErrorMessage = "ModelId должно быть положительное число.")] public required int ModelId { get; set; } - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs index 22b9f4732..ccc3eec1d 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using BikeRental.Domain.Enum; namespace BikeRental.Application.Contracts.Dtos; @@ -7,35 +8,48 @@ public class BikeModelCreateUpdateDto /// /// The type of bicycle: road, sport, mountain, hybrid /// + [Required] public required BikeType Type { get; set; } /// /// The size of the bicycle's wheels /// + [Required] + [Range(12, 36, ErrorMessage = "Размер колес должен быть 12-36.")] public required int WheelSize { get; set; } /// /// Maximum permissible cyclist weight /// + [Required] + [Range(30, 200, ErrorMessage = "Вес человека должен быть 30-200 кг.")] public required int MaxCyclistWeight { get; set; } /// /// Weight of the bike model /// + [Required] + [Range(3.0, 50.0, ErrorMessage = "Вес байка 3-50 кг.")] public required double Weight { get; set; } /// /// The type of braking system used in this model of bike /// + [Required] + [StringLength(30, ErrorMessage = "Макс. длина 30 символов.")] public required string BrakeType { get; set; } /// /// Year of manufacture of the bicycle model /// + [Required] + [RegularExpression(@"^\d{4}$", ErrorMessage = "Год должен быть 4 цифры.")] public required string YearOfManufacture { get; set; } /// /// Cost per hour rental /// + [Required] + [Range(0.01, 1000, ErrorMessage = "Цена должна быть > 0.")] public required decimal RentPrice { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs index 5dea42c9b..75d1bfba3 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace BikeRental.Application.Contracts.Dtos; public class LeaseCreateUpdateDto @@ -5,20 +7,27 @@ public class LeaseCreateUpdateDto /// /// Person who rents a bike /// + [Required] + [Range(1, int.MaxValue, ErrorMessage = "ID человека должно быть положительное число.")] public required int RenterId { get; set; } /// /// Bike for rent /// + [Required] + [Range(1, int.MaxValue, ErrorMessage = "ID велика должно быть положительное число.")] public required int BikeId { get; set; } /// /// Rental start time /// + [Required] public required DateTime RentalStartTime { get; set; } /// /// Rental duration in hours /// + [Required] + [Range(1, int.MaxValue, ErrorMessage = "Время должно быть от часа.")] public required int RentalDuration { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs index 13683a2b4..cfcab4901 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace BikeRental.Application.Contracts.Dtos; public class RenterCreateUpdateDto @@ -5,10 +7,14 @@ public class RenterCreateUpdateDto /// /// Renter's full name /// + [Required] + [StringLength(100, MinimumLength = 3, ErrorMessage = "Длина 3-100 символов.")] public required string FullName { get; set; } /// /// Renter's phone number /// + [Required] + [Phone(ErrorMessage = "Неверный формат телефона.")] public required string PhoneNumber { get; set; } } \ No newline at end of file From 35f41e9588d49334b8d856c72c3c28d031966506 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Tue, 23 Dec 2025 12:40:16 +0400 Subject: [PATCH 39/48] replaced unused variables, marked static, used primary constructor, private fields removed --- .../Middleware/GlobalExceptionHandler.cs | 63 +++++++------------ .../Services/BikeModelService.cs | 4 +- .../Services/BikeService.cs | 4 +- .../Services/LeaseService.cs | 12 ++-- .../Services/RenterService.cs | 4 +- BikeRental/BikeRental.Domain/Models/Bike.cs | 2 +- 6 files changed, 36 insertions(+), 53 deletions(-) diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs index a83ddda44..a4f405c7d 100644 --- a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -6,22 +6,11 @@ namespace BikeRental.Api.Middleware; /// /// Глобальный обработчик исключений с логированием /// -public sealed class GlobalExceptionHandler : IExceptionHandler +public sealed class GlobalExceptionHandler( + IProblemDetailsService problemDetailsService, + ILogger logger) + : IExceptionHandler { - private readonly IProblemDetailsService _problemDetailsService; // сервис для создания ответов об ошибках - private readonly ILogger _logger; - - /// - /// Инициализирует новый экземпляр глобального обработчика исключений с зависимостями для логирования и генерации ProblemDetails. - /// - public GlobalExceptionHandler( - IProblemDetailsService problemDetailsService, - ILogger logger) - { - _problemDetailsService = problemDetailsService; - _logger = logger; - } - /// /// Попытаться обработать исключение /// @@ -35,7 +24,7 @@ public async ValueTask TryHandleAsync( var problemDetails = CreateProblemDetails(exception); - return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext + return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext { HttpContext = httpContext, Exception = exception, @@ -75,7 +64,7 @@ private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception ex // Для 404 и 400 - Warning с кратким сообщением if (exception is KeyNotFoundException or ArgumentException or InvalidOperationException) { - _logger.LogWarning( + logger.LogWarning( "[{StatusCode}] {Method} {Path} - {Message}", GetStatusCode(exception), method, @@ -85,7 +74,7 @@ private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception ex // Для остальных - Error с полным stack trace else { - _logger.LogError( + logger.LogError( exception, "[{StatusCode}] {Method} {Path} - {ExceptionType}: {Message}", GetStatusCode(exception), @@ -99,7 +88,7 @@ private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception ex /// /// Создание ProblemDetails /// - private ProblemDetails CreateProblemDetails(Exception exception) + private static ProblemDetails CreateProblemDetails(Exception exception) { var statusCode = GetStatusCode(exception); @@ -114,37 +103,31 @@ private ProblemDetails CreateProblemDetails(Exception exception) /// /// Получение статус кода /// - private int GetStatusCode(Exception exception) + private static int GetStatusCode(Exception exception) => exception switch { - return exception switch - { - KeyNotFoundException => StatusCodes.Status404NotFound, - ArgumentException => StatusCodes.Status400BadRequest, - InvalidOperationException => StatusCodes.Status400BadRequest, - UnauthorizedAccessException => StatusCodes.Status401Unauthorized, - _ => StatusCodes.Status500InternalServerError - }; - } + KeyNotFoundException => StatusCodes.Status404NotFound, + ArgumentException => StatusCodes.Status400BadRequest, + InvalidOperationException => StatusCodes.Status400BadRequest, + UnauthorizedAccessException => StatusCodes.Status401Unauthorized, + _ => StatusCodes.Status500InternalServerError + }; /// /// Получение заголовка /// - private string GetTitle(Exception exception) + private static string GetTitle(Exception exception) => exception switch { - return exception switch - { - KeyNotFoundException => "Resource not found", - ArgumentException => "Bad request", - InvalidOperationException => "Invalid operation", - UnauthorizedAccessException => "Unauthorized", - _ => "Internal server error" - }; - } + KeyNotFoundException => "Resource not found", + ArgumentException => "Bad request", + InvalidOperationException => "Invalid operation", + UnauthorizedAccessException => "Unauthorized", + _ => "Internal server error" + }; /// /// Получение деталей /// - private string GetDetail(Exception exception) + private static string GetDetail(Exception exception) { // Для клиентских ошибок показываем сообщение исключения if (exception is KeyNotFoundException or ArgumentException or InvalidOperationException) diff --git a/BikeRental/BikeRental.Application/Services/BikeModelService.cs b/BikeRental/BikeRental.Application/Services/BikeModelService.cs index f23a6d0d5..b740af054 100644 --- a/BikeRental/BikeRental.Application/Services/BikeModelService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeModelService.cs @@ -37,8 +37,8 @@ public async Task Create(BikeModelCreateUpdateDto dto) public async Task Update(int id, BikeModelCreateUpdateDto dto) { - var createdEntity = await bikeModelRepository.GetById(id) - ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + _ = await bikeModelRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); var entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs index a210effa3..e517a7c8c 100644 --- a/BikeRental/BikeRental.Application/Services/BikeService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -42,8 +42,8 @@ public async Task Create(BikeCreateUpdateDto dto) public async Task Update(int id, BikeCreateUpdateDto dto) { - var createdEntity = await bikeRepository.GetById(id) - ?? throw new KeyNotFoundException($"Bike with id {id} not found."); + _ = await bikeRepository.GetById(id) + ?? throw new KeyNotFoundException($"Bike with id {id} not found."); var model = await modelRepository.GetById(dto.ModelId) ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); diff --git a/BikeRental/BikeRental.Application/Services/LeaseService.cs b/BikeRental/BikeRental.Application/Services/LeaseService.cs index 4921c08f9..e95b9d371 100644 --- a/BikeRental/BikeRental.Application/Services/LeaseService.cs +++ b/BikeRental/BikeRental.Application/Services/LeaseService.cs @@ -48,14 +48,14 @@ public async Task Create(LeaseCreateUpdateDto dto) public async Task Update(int id, LeaseCreateUpdateDto dto) { - var createdEntity = await leaseRepository.GetById(id) - ?? throw new KeyNotFoundException($"Lease with id {id} not found."); + _ = await leaseRepository.GetById(id) + ?? throw new KeyNotFoundException($"Lease with id {id} not found."); - var bike = await bikeRepository.GetById(dto.BikeId) - ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); + var bike = await bikeRepository.GetById(dto.BikeId) + ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); - var renter = await renterRepository.GetById(dto.RenterId) - ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); + var renter = await renterRepository.GetById(dto.RenterId) + ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); var entityToUpdate = dto.ToEntity(bike, renter); entityToUpdate.Id = id; diff --git a/BikeRental/BikeRental.Application/Services/RenterService.cs b/BikeRental/BikeRental.Application/Services/RenterService.cs index 5763dc39a..29f0386f4 100644 --- a/BikeRental/BikeRental.Application/Services/RenterService.cs +++ b/BikeRental/BikeRental.Application/Services/RenterService.cs @@ -38,8 +38,8 @@ public async Task Create(RenterCreateUpdateDto dto) public async Task Update(int id, RenterCreateUpdateDto dto) { - var createdEntity = await renterRepository.GetById(id) - ?? throw new KeyNotFoundException($"Entity with id {id} not found."); + _ = await renterRepository.GetById(id) + ?? throw new KeyNotFoundException($"Entity with id {id} not found."); var entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs index 5c719b5f2..77b9edc66 100644 --- a/BikeRental/BikeRental.Domain/Models/Bike.cs +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -28,5 +28,5 @@ public class Bike /// /// Bike's model /// - public required BikeModel Model { get; init; } = null!; + public required BikeModel Model { get; init; } } \ No newline at end of file From e184a9e655b6c6931136513427d01a951fbd7ac3 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Wed, 24 Dec 2025 14:25:11 +0400 Subject: [PATCH 40/48] started 4 labs: structure, possible elements --- .../BikeRental.Generator.Nats.Host.csproj | 13 +++++ .../BikeRentalNatsProducer.cs | 49 +++++++++++++++++++ .../LeaseBatchGenerator.cs | 1 + .../BikeRental.Generator.Nats.Host/Program.cs | 9 ++++ .../Properties/launchSettings.json | 12 +++++ .../BikeRental.Generator.Nats.Host/Worker.cs | 25 ++++++++++ .../appsettings.Development.json | 8 +++ .../appsettings.json | 8 +++ BikeRental/BikeRental.sln | 6 +++ 9 files changed, 131 insertions(+) create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/Program.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/Properties/launchSettings.json create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/Worker.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/appsettings.Development.json create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/appsettings.json diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj new file mode 100644 index 000000000..6c7c1c0f9 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + dotnet-BikeRental.Generator.Nats.Host-bb308bf8-660f-4f89-810e-494ba2f57c24 + + + + + + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs new file mode 100644 index 000000000..2e5391852 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs @@ -0,0 +1,49 @@ +namespace BikeRental.Generator.Nats.Host; + +/// +/// Имплементация для отправки контрактов через стрим Nats +/// +/// Конфигурация +/// Подключение к Nats +/// Логгер +public class BikeRentalNatsProducer +{ + // TODO по примеру из лекций создать/обновить JetStream stream +} + +/* +namespace BookStore.Generator.Nats.Host + { + /// + /// Имплементация для отправки контрактов через стрим Nats + /// + /// Конфигурация + /// Подключение к Nats + /// Логгер + public class BookStoreNatsProducer(IConfiguration configuration, INatsConnection connection, ILogger logger) + { + private readonly string _streamName = configuration.GetSection("Nats")["StreamName"] ?? throw new KeyNotFoundException("StreamName is not configured in Nats section."); + private readonly string _subjectName = configuration.GetSection("Nats")["SubjectName"] ?? throw new KeyNotFoundException("SubjectName is not configured in Nats section."); + + /// + public async Task SendAsync(IList batch) + { + try + { + await connection.ConnectAsync(); + var context = connection.CreateJetStreamContext(); + var stream = await context.CreateOrUpdateStreamAsync(new NATS.Client.JetStream.Models.StreamConfig(_streamName, [_subjectName])); + + logger.LogInformation("Establishing a stream {stream} with subject {subject}", _streamName, _subjectName); + + await context.PublishAsync(_subjectName, JsonSerializer.SerializeToUtf8Bytes(batch)); + logger.LogInformation("Sent a batch of {count} contracts to {subject} of {stream}", batch.Count, _subjectName, _streamName); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occured during sending a batch of {count} contracts to {stream}/{subject}", batch.Count, _streamName, _subjectName); + } + } + } + } +*/ \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs new file mode 100644 index 000000000..0ff2c7c04 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs @@ -0,0 +1 @@ +// генерация списка из дтошек LeaseCreateUpdateDto пусть \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs new file mode 100644 index 000000000..f61fc7d49 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs @@ -0,0 +1,9 @@ +using BikeRental.Generator.Nats.Host; + +// TODO тут подключение к NATS + генератор и воркер + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Properties/launchSettings.json b/BikeRental/BikeRental.Generator.Nats.Host/Properties/launchSettings.json new file mode 100644 index 000000000..f7da70958 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "BikeRental.Generator.Nats.Host": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs b/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs new file mode 100644 index 000000000..367daaa15 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs @@ -0,0 +1,25 @@ +namespace BikeRental.Generator.Nats.Host; +// TODO периодически запускать генерацию и отправку, переименовать + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + + await Task.Delay(1000, stoppingToken); + } + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.Development.json b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/BikeRental/BikeRental.sln b/BikeRental/BikeRental.sln index 5d431361d..33f144c78 100644 --- a/BikeRental/BikeRental.sln +++ b/BikeRental/BikeRental.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Application.Cont EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "AppHost\AppHost.csproj", "{FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BikeRental.Generator.Nats.Host", "BikeRental.Generator.Nats.Host\BikeRental.Generator.Nats.Host.csproj", "{3292CC61-FAAA-47C4-87BE-76E6CA5BB1C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFC8D593-B70B-430D-8FC5-A9F3EE1DDE17}.Release|Any CPU.Build.0 = Release|Any CPU + {3292CC61-FAAA-47C4-87BE-76E6CA5BB1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3292CC61-FAAA-47C4-87BE-76E6CA5BB1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3292CC61-FAAA-47C4-87BE-76E6CA5BB1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3292CC61-FAAA-47C4-87BE-76E6CA5BB1C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From a2b8e6c0b1b7bf1ab8d72230a5172e404fee5c0c Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Wed, 24 Dec 2025 15:09:19 +0400 Subject: [PATCH 41/48] added simple generator --- .../BikeRental.Generator.Nats.Host.csproj | 4 ++ .../LeaseBatchGenerator.cs | 69 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj index 6c7c1c0f9..15d36c2fc 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs index 0ff2c7c04..a393d095e 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs @@ -1 +1,68 @@ -// генерация списка из дтошек LeaseCreateUpdateDto пусть \ No newline at end of file +using BikeRental.Application.Contracts.Dtos; + +namespace BikeRental.Generator.Nats.Host; + +public sealed class LeaseGenerationOptions + //todo потом убрать в другой класс и может как-то связать с дто, чтобы самим не прописывать + // нет проверки на существование велика и арендатора с таким айди +{ + public int BatchSize { get; init; } = 10; + public int BikeIdMin { get; init; } = 1; + public int BikeIdMax { get; init; } = 30; + public int RenterIdMin { get; init; } = 1; + public int RenterIdMax { get; init; } = 20; + public int RentalDurationMinHours { get; init; } = 1; + public int RentalDurationMaxHours { get; init; } = 72; + public int RentalStartDaysBackMax { get; init; } = 10; +} + +public sealed class LeaseBatchGenerator +{ + public IList GenerateBatch(LeaseGenerationOptions settings) + { + Validate(settings); + + var batch = new List(settings.BatchSize); + for (var i = 0; i < settings.BatchSize; i++) + { + var renterId = Random.Shared.Next(settings.RenterIdMin, settings.RenterIdMax + 1); + var bikeId = Random.Shared.Next(settings.BikeIdMin, settings.BikeIdMax + 1); + var duration = Random.Shared.Next(settings.RentalDurationMinHours, settings.RentalDurationMaxHours + 1); + var daysBack = Random.Shared.Next(0, settings.RentalStartDaysBackMax + 1); + var hoursBack = Random.Shared.Next(0, 24); + + batch.Add(new LeaseCreateUpdateDto + { + RenterId = renterId, + BikeId = bikeId, + RentalDuration = duration, + RentalStartTime = DateTime.UtcNow.AddDays(-daysBack).AddHours(-hoursBack) + }); + } + + return batch; + } + + private static void Validate(LeaseGenerationOptions settings) + { + if (settings.BikeIdMin > settings.BikeIdMax) + { + throw new InvalidOperationException("LeaseGeneration.BikeIdMin must be <= LeaseGeneration.BikeIdMax."); + } + + if (settings.RenterIdMin > settings.RenterIdMax) + { + throw new InvalidOperationException("LeaseGeneration.RenterIdMin must be <= LeaseGeneration.RenterIdMax."); + } + + if (settings.RentalDurationMinHours > settings.RentalDurationMaxHours) + { + throw new InvalidOperationException("LeaseGeneration.RentalDurationMinHours must be <= LeaseGeneration.RentalDurationMaxHours."); + } + + if (settings.RentalStartDaysBackMax < 0) + { + throw new InvalidOperationException("LeaseGeneration.RentalStartDaysBackMax must be >= 0."); + } + } +} From 9c91fd3713fa9b9c80e4ce2be062fd0169cb463a Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Wed, 24 Dec 2025 19:05:35 +0400 Subject: [PATCH 42/48] added connection to NATS, generator, and worker --- BikeRental/AppHost/AppHost.csproj | 1 + BikeRental/AppHost/Program.cs | 11 +++ .../BikeRental.Generator.Nats.Host.csproj | 2 + .../BikeRentalNatsProducer.cs | 98 ++++++++++--------- .../LeaseBatchGenerator.cs | 14 --- .../LeaseBatchWorker.cs | 38 +++++++ .../LeaseGenerationOptions.cs | 15 +++ .../NatsSettings.cs | 8 ++ .../BikeRental.Generator.Nats.Host/Program.cs | 31 +++++- .../BikeRental.Generator.Nats.Host/Worker.cs | 25 ----- .../appsettings.json | 16 +++ 11 files changed, 171 insertions(+), 88 deletions(-) create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs create mode 100644 BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs delete mode 100644 BikeRental/BikeRental.Generator.Nats.Host/Worker.cs diff --git a/BikeRental/AppHost/AppHost.csproj b/BikeRental/AppHost/AppHost.csproj index 20e1d8189..bcf809e7f 100644 --- a/BikeRental/AppHost/AppHost.csproj +++ b/BikeRental/AppHost/AppHost.csproj @@ -24,6 +24,7 @@ + diff --git a/BikeRental/AppHost/Program.cs b/BikeRental/AppHost/Program.cs index b4b9e2597..857fa7b91 100644 --- a/BikeRental/AppHost/Program.cs +++ b/BikeRental/AppHost/Program.cs @@ -1,5 +1,9 @@ var builder = DistributedApplication.CreateBuilder(args); +var nats = builder.AddContainer("nats", "nats:2.10") + .WithArgs("-js") + .WithEndpoint(4222, 4222); + var bikeRentalDbPassword = builder.AddParameter( name: "bike-rental-db-password", value: "1234512345Aa$", @@ -12,8 +16,15 @@ var bikeRentalDb = bikeRentalSql.AddDatabase("bike-rental"); +builder.AddProject("bike-rental-nats-generator") + .WaitFor(nats) + .WithEnvironment("Nats__Url", "nats://nats:4222") + .WithEnvironment("Nats__StreamName", "bike-rental-stream") + .WithEnvironment("Nats__SubjectName", "bike-rental.leases"); + builder.AddProject("bike-rental-api") .WaitFor(bikeRentalDb) .WithReference(bikeRentalDb); + builder.Build().Run(); \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj index 15d36c2fc..9ab7fb93d 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj @@ -9,6 +9,8 @@ + + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs index 2e5391852..ce9e9f600 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs @@ -1,49 +1,57 @@ +using System.Text.Json; +using BikeRental.Application.Contracts.Dtos; +using Microsoft.Extensions.Options; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; +using NATS.Net; + namespace BikeRental.Generator.Nats.Host; -/// -/// Имплементация для отправки контрактов через стрим Nats -/// -/// Конфигурация -/// Подключение к Nats -/// Логгер -public class BikeRentalNatsProducer +public sealed class BikeRentalNatsProducer( + IOptions settings, + INatsConnection connection, + ILogger logger) { - // TODO по примеру из лекций создать/обновить JetStream stream -} + private readonly string _streamName = GetRequired(settings.Value.StreamName, "StreamName"); + private readonly string _subjectName = GetRequired(settings.Value.SubjectName, "SubjectName"); + + public async Task SendAsync(IList batch, CancellationToken cancellationToken) + { + if (batch.Count == 0) + { + logger.LogInformation("Skipping empty lease batch."); + return; + } + + try + { + await connection.ConnectAsync(); + var context = connection.CreateJetStreamContext(); + + var streamConfig = new StreamConfig(_streamName, new List { _subjectName }); + await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken); + + var payload = JsonSerializer.SerializeToUtf8Bytes(batch); + await context.PublishAsync( + subject: _subjectName, + data: payload, + cancellationToken: cancellationToken); + + logger.LogInformation("Sent a batch of {count} leases to {subject} of {stream}", batch.Count, _subjectName, _streamName); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occurred during sending a batch of {count} leases to {stream}/{subject}", batch.Count, _streamName, _subjectName); + } + } -/* -namespace BookStore.Generator.Nats.Host - { - /// - /// Имплементация для отправки контрактов через стрим Nats - /// - /// Конфигурация - /// Подключение к Nats - /// Логгер - public class BookStoreNatsProducer(IConfiguration configuration, INatsConnection connection, ILogger logger) - { - private readonly string _streamName = configuration.GetSection("Nats")["StreamName"] ?? throw new KeyNotFoundException("StreamName is not configured in Nats section."); - private readonly string _subjectName = configuration.GetSection("Nats")["SubjectName"] ?? throw new KeyNotFoundException("SubjectName is not configured in Nats section."); - - /// - public async Task SendAsync(IList batch) - { - try - { - await connection.ConnectAsync(); - var context = connection.CreateJetStreamContext(); - var stream = await context.CreateOrUpdateStreamAsync(new NATS.Client.JetStream.Models.StreamConfig(_streamName, [_subjectName])); - - logger.LogInformation("Establishing a stream {stream} with subject {subject}", _streamName, _subjectName); - - await context.PublishAsync(_subjectName, JsonSerializer.SerializeToUtf8Bytes(batch)); - logger.LogInformation("Sent a batch of {count} contracts to {subject} of {stream}", batch.Count, _subjectName, _streamName); - } - catch (Exception ex) - { - logger.LogError(ex, "Exception occured during sending a batch of {count} contracts to {stream}/{subject}", batch.Count, _streamName, _subjectName); - } - } - } - } -*/ \ No newline at end of file + private static string GetRequired(string? value, string key) // мини проверка + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new KeyNotFoundException($"{key} is not configured in Nats section."); + } + return value; + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs index a393d095e..450d8b0b1 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs @@ -2,20 +2,6 @@ namespace BikeRental.Generator.Nats.Host; -public sealed class LeaseGenerationOptions - //todo потом убрать в другой класс и может как-то связать с дто, чтобы самим не прописывать - // нет проверки на существование велика и арендатора с таким айди -{ - public int BatchSize { get; init; } = 10; - public int BikeIdMin { get; init; } = 1; - public int BikeIdMax { get; init; } = 30; - public int RenterIdMin { get; init; } = 1; - public int RenterIdMax { get; init; } = 20; - public int RentalDurationMinHours { get; init; } = 1; - public int RentalDurationMaxHours { get; init; } = 72; - public int RentalStartDaysBackMax { get; init; } = 10; -} - public sealed class LeaseBatchGenerator { public IList GenerateBatch(LeaseGenerationOptions settings) diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs new file mode 100644 index 000000000..4fb7af2ed --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Options; + +namespace BikeRental.Generator.Nats.Host; + +public sealed class LeaseBatchWorker( + LeaseBatchGenerator generator, + BikeRentalNatsProducer producer, + IOptions options, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var settings = options.Value; + if (settings.BatchSize <= 0) + { + logger.LogError("LeaseGeneration.BatchSize must be greater than 0."); + return; + } + + if (settings.IntervalSeconds <= 0) + { + await SendBatchAsync(settings, stoppingToken); + return; + } + + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(settings.IntervalSeconds)); + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await SendBatchAsync(settings, stoppingToken); + } + } + + private async Task SendBatchAsync(LeaseGenerationOptions settings, CancellationToken stoppingToken) + { + var batch = generator.GenerateBatch(settings); + await producer.SendAsync(batch, stoppingToken); + } +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs new file mode 100644 index 000000000..e51e1ba76 --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs @@ -0,0 +1,15 @@ +namespace BikeRental.Generator.Nats.Host; + +// нет проверки на существование велика и арендатора с таким айди +public sealed class LeaseGenerationOptions +{ + public int BatchSize { get; init; } + public int IntervalSeconds { get; init; } + public int BikeIdMin { get; init; } + public int BikeIdMax { get; init; } + public int RenterIdMin { get; init; } + public int RenterIdMax { get; init; } + public int RentalDurationMinHours { get; init; } + public int RentalDurationMaxHours { get; init; } + public int RentalStartDaysBackMax { get; init; } +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs new file mode 100644 index 000000000..12d445a4f --- /dev/null +++ b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs @@ -0,0 +1,8 @@ +namespace BikeRental.Generator.Nats.Host; +// для типизированной конфигурации +public sealed class NatsSettings +{ + public string Url { get; init; } = "nats://localhost:4222"; + public string StreamName { get; init; } = string.Empty; + public string SubjectName { get; init; } = string.Empty; +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs index f61fc7d49..7a36e6c04 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs @@ -1,9 +1,32 @@ using BikeRental.Generator.Nats.Host; - -// TODO тут подключение к NATS + генератор и воркер +using Microsoft.Extensions.Options; +using NATS.Client.Core; var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddHostedService(); + + +var natsSettingsSection = builder.Configuration.GetSection("NatsSettings"); + +if (natsSettingsSection.Exists()) +{ + Console.WriteLine($"Nats.Url: {natsSettingsSection["Url"]}"); + Console.WriteLine($"Nats.StreamName: {natsSettingsSection["StreamName"]}"); +} + +builder.Services.Configure(builder.Configuration.GetSection("NatsSettings")); +builder.Services.Configure(builder.Configuration.GetSection("LeaseGeneration")); + +builder.Services.AddSingleton(sp => +{ + var settings = sp.GetRequiredService>().Value; + var options = new NatsOpts { Url = settings.Url }; + return new NatsConnection(options); +}); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); var host = builder.Build(); -host.Run(); +await host.RunAsync(); +//host.Run(); \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs b/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs deleted file mode 100644 index 367daaa15..000000000 --- a/BikeRental/BikeRental.Generator.Nats.Host/Worker.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace BikeRental.Generator.Nats.Host; -// TODO периодически запускать генерацию и отправку, переименовать - -public class Worker : BackgroundService -{ - private readonly ILogger _logger; - - public Worker(ILogger logger) - { - _logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - } - - await Task.Delay(1000, stoppingToken); - } - } -} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json index b2dcdb674..d450cb11e 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json +++ b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json @@ -4,5 +4,21 @@ "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } + }, + "NatsSettings": { + "Url": "nats://localhost:4222", + "StreamName": "bike-rental-stream", + "SubjectName": "bike-rental.leases" + }, + "LeaseGeneration": { + "BatchSize": 10, + "IntervalSeconds": 10, + "BikeIdMin": 1, + "BikeIdMax": 30, + "RenterIdMin": 1, + "RenterIdMax": 20, + "RentalDurationMinHours": 1, + "RentalDurationMaxHours": 72, + "RentalStartDaysBackMax": 10 } } From 03c9cecadd2e090f54ca9802126d58b7524ac418 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Wed, 24 Dec 2025 22:23:18 +0400 Subject: [PATCH 43/48] added consumer - first worked try --- BikeRental/AppHost/Program.cs | 11 +- .../BikeRental.Api/BikeRental.Api.csproj | 2 + .../Messaging/NatsConsumerSettings.cs | 17 ++ .../Messaging/NatsLeaseConsumer.cs | 227 ++++++++++++++++++ BikeRental/BikeRental.Api/Program.cs | 35 ++- BikeRental/BikeRental.Api/appsettings.json | 16 +- .../BikeRentalNatsProducer.cs | 92 ++++++- .../LeaseBatchGenerator.cs | 28 ++- .../LeaseBatchWorker.cs | 27 ++- .../LeaseGenerationOptions.cs | 21 +- .../NatsSettings.cs | 5 + .../BikeRental.Generator.Nats.Host/Program.cs | 17 +- .../appsettings.json | 12 +- 13 files changed, 477 insertions(+), 33 deletions(-) create mode 100644 BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs create mode 100644 BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs diff --git a/BikeRental/AppHost/Program.cs b/BikeRental/AppHost/Program.cs index 857fa7b91..707cf4351 100644 --- a/BikeRental/AppHost/Program.cs +++ b/BikeRental/AppHost/Program.cs @@ -18,13 +18,16 @@ builder.AddProject("bike-rental-nats-generator") .WaitFor(nats) - .WithEnvironment("Nats__Url", "nats://nats:4222") + .WithEnvironment("Nats__Url", "nats://localhost:4222") .WithEnvironment("Nats__StreamName", "bike-rental-stream") .WithEnvironment("Nats__SubjectName", "bike-rental.leases"); builder.AddProject("bike-rental-api") .WaitFor(bikeRentalDb) - .WithReference(bikeRentalDb); - + .WaitFor(nats) + .WithReference(bikeRentalDb) + .WithEnvironment("Nats__Url", "nats://localhost:4222") + .WithEnvironment("Nats__StreamName", "bike-rental-stream") + .WithEnvironment("Nats__SubjectName", "bike-rental.leases"); -builder.Build().Run(); \ No newline at end of file +builder.Build().Run(); diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index fb4a2addc..af587b3e0 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -34,6 +34,8 @@ + + diff --git a/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs b/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs new file mode 100644 index 000000000..2cef2226e --- /dev/null +++ b/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs @@ -0,0 +1,17 @@ +namespace BikeRental.Api.Messaging; + +internal sealed class NatsConsumerSettings +{ + public string Url { get; init; } = "nats://localhost:4222"; + public string StreamName { get; init; } = string.Empty; + public string SubjectName { get; init; } = string.Empty; + public string DurableName { get; init; } = "bike-rental-lease-consumer"; + public int AckWaitSeconds { get; init; } = 30; + public long MaxDeliver { get; init; } = 5; + public int ConnectRetryAttempts { get; init; } = 5; + public int ConnectRetryDelayMs { get; init; } = 2000; + public double RetryBackoffFactor { get; init; } = 2; + public int ConsumeMaxMsgs { get; init; } = 100; + public int ConsumeExpiresSeconds { get; init; } = 30; + public int ConsumeRetryDelayMs { get; init; } = 2000; +} diff --git a/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs new file mode 100644 index 000000000..48fe5de1c --- /dev/null +++ b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs @@ -0,0 +1,227 @@ +using System.Text.Json; +using BikeRental.Application.Contracts.Dtos; +using BikeRental.Application.Interfaces; +using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; +using NATS.Net; + +namespace BikeRental.Api.Messaging; + +internal sealed class NatsLeaseConsumer( + INatsConnection connection, + IOptions settings, + IServiceScopeFactory scopeFactory, + ILogger logger) : BackgroundService +{ + private readonly NatsConsumerSettings _settings = settings.Value; + private readonly INatsDeserialize _deserializer = BuildDeserializer(connection); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + ValidateSettings(_settings); + + await ExecuteWithRetryAsync( + "connect to NATS", + _settings.ConnectRetryAttempts, + TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), + stoppingToken, + async () => await connection.ConnectAsync()); + + var context = connection.CreateJetStreamContext(); + var streamConfig = new StreamConfig(_settings.StreamName, new List { _settings.SubjectName }); + await ExecuteWithRetryAsync( + "create/update stream", + _settings.ConnectRetryAttempts, + TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), + stoppingToken, + async () => await context.CreateOrUpdateStreamAsync(streamConfig, stoppingToken)); + + var consumerConfig = new ConsumerConfig + { + Name = _settings.DurableName, + DurableName = _settings.DurableName, + AckPolicy = ConsumerConfigAckPolicy.Explicit, + DeliverPolicy = ConsumerConfigDeliverPolicy.All, + ReplayPolicy = ConsumerConfigReplayPolicy.Instant, + FilterSubject = _settings.SubjectName, + AckWait = TimeSpan.FromSeconds(Math.Max(1, _settings.AckWaitSeconds)), + MaxDeliver = Math.Max(1, _settings.MaxDeliver) + }; + + var consumer = await ExecuteWithRetryAsync( + "create/update consumer", + _settings.ConnectRetryAttempts, + TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), + stoppingToken, + async () => await context.CreateOrUpdateConsumerAsync(_settings.StreamName, consumerConfig, stoppingToken)); + + var consumeOptions = new NatsJSConsumeOpts + { + MaxMsgs = Math.Max(1, _settings.ConsumeMaxMsgs), + Expires = TimeSpan.FromSeconds(Math.Max(1, _settings.ConsumeExpiresSeconds)) + }; + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await foreach (var msg in consumer.ConsumeAsync(_deserializer, consumeOptions, stoppingToken)) + { + await HandleMessageAsync(msg, stoppingToken); + } + } + catch (Exception ex) when (!stoppingToken.IsCancellationRequested) + { + logger.LogError(ex, "Error while consuming leases from NATS. Retrying in {delay}ms.", _settings.ConsumeRetryDelayMs); + await Task.Delay(Math.Max(0, _settings.ConsumeRetryDelayMs), stoppingToken); + } + } + } + + private async Task HandleMessageAsync(INatsJSMsg msg, CancellationToken stoppingToken) + { + if (msg.Data is null || msg.Data.Length == 0) + { + logger.LogWarning("Received empty lease batch message."); + await msg.AckAsync(cancellationToken: stoppingToken); + return; + } + + List? leases; + try + { + leases = JsonSerializer.Deserialize>(msg.Data); + } + catch (JsonException ex) + { + logger.LogError(ex, "Failed to deserialize lease batch payload."); + await msg.AckTerminateAsync(cancellationToken: stoppingToken); + return; + } + + if (leases is null || leases.Count == 0) + { + logger.LogWarning("Received lease batch with no items."); + await msg.AckAsync(cancellationToken: stoppingToken); + return; + } + + try + { + await SaveBatchAsync(leases, stoppingToken); + await msg.AckAsync(cancellationToken: stoppingToken); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Lease batch contains invalid references. Message will be terminated."); + await msg.AckTerminateAsync(cancellationToken: stoppingToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to persist lease batch. Message will be retried."); + } + } + + private async Task SaveBatchAsync(IReadOnlyList leases, CancellationToken stoppingToken) + { + await using var scope = scopeFactory.CreateAsyncScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var leaseService = scope.ServiceProvider.GetRequiredService(); + + await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + foreach (var lease in leases) + { + await leaseService.Create(lease); + } + await transaction.CommitAsync(stoppingToken); + } + + private async Task ExecuteWithRetryAsync( + string operation, + int attempts, + TimeSpan baseDelay, + CancellationToken stoppingToken, + Func action) + { + _ = await ExecuteWithRetryAsync( + operation, + attempts, + baseDelay, + stoppingToken, + async () => + { + await action(); + return new object(); + }); + } + + private async Task ExecuteWithRetryAsync( + string operation, + int attempts, + TimeSpan baseDelay, + CancellationToken stoppingToken, + Func> action) + { + var retries = Math.Max(1, attempts); + var delay = baseDelay; + var backoff = _settings.RetryBackoffFactor <= 0 ? 2 : _settings.RetryBackoffFactor; + + for (var attempt = 1; attempt <= retries; attempt++) + { + try + { + return await action(); + } + catch (Exception ex) when (attempt < retries && !stoppingToken.IsCancellationRequested) + { + if (delay > TimeSpan.Zero) + { + logger.LogWarning( + ex, + "Failed to {operation} (attempt {attempt}/{retries}). Retrying in {delay}.", + operation, + attempt, + retries, + delay); + await Task.Delay(delay, stoppingToken); + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * backoff); + } + else + { + logger.LogWarning( + ex, + "Failed to {operation} (attempt {attempt}/{retries}). Retrying immediately.", + operation, + attempt, + retries); + } + } + } + + throw new InvalidOperationException($"Failed to {operation} after {retries} attempts."); + } + + private static INatsDeserialize BuildDeserializer(INatsConnection connection) + { + var registry = connection.Opts.SerializerRegistry ?? new NatsDefaultSerializerRegistry(); + return registry.GetDeserializer(); + } + + private static void ValidateSettings(NatsConsumerSettings settings) + { + if (string.IsNullOrWhiteSpace(settings.StreamName)) + { + throw new KeyNotFoundException("StreamName is not configured in Nats section."); + } + + if (string.IsNullOrWhiteSpace(settings.SubjectName)) + { + throw new KeyNotFoundException("SubjectName is not configured in Nats section."); + } + } +} diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index 38cd6cd89..d64fd9ee8 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -1,6 +1,9 @@ using BikeRental.Api; +using BikeRental.Api.Messaging; using BikeRental.Api.Extensions; using Microsoft.OpenApi.Models; +using Microsoft.Extensions.Options; +using NATS.Client.Core; var builder = WebApplication.CreateBuilder(args); @@ -28,6 +31,36 @@ builder.AddRepositories(); builder.AddServices(); +var natsSettingsSection = builder.Configuration.GetSection("NatsConsumerSettings"); + +if (natsSettingsSection.Exists()) +{ + Console.WriteLine($"Nats.Url: {natsSettingsSection["Url"]}"); + Console.WriteLine($"Nats.StreamName: {natsSettingsSection["StreamName"]}"); +} + +builder.Services.Configure(builder.Configuration.GetSection("NatsConsumerSettings")); +builder.Services.AddSingleton(sp => +{ + var settings = sp.GetRequiredService>().Value; + var connectRetryDelayMs = Math.Max(0, settings.ConnectRetryDelayMs); + var reconnectWaitMin = TimeSpan.FromMilliseconds(connectRetryDelayMs); + var reconnectWaitMax = TimeSpan.FromMilliseconds( + Math.Max(connectRetryDelayMs, connectRetryDelayMs * Math.Max(settings.RetryBackoffFactor, 1))); + + var options = new NatsOpts + { + Url = settings.Url, + RetryOnInitialConnect = settings.ConnectRetryAttempts > 1, + MaxReconnectRetry = Math.Max(0, settings.ConnectRetryAttempts), + ReconnectWaitMin = reconnectWaitMin, + ReconnectWaitMax = reconnectWaitMax, + ReconnectJitter = TimeSpan.FromMilliseconds(connectRetryDelayMs * 0.2) + }; + return new NatsConnection(options); +}); +builder.Services.AddHostedService(); + var app = builder.Build(); app.UseExceptionHandler(); @@ -50,4 +83,4 @@ app.MapControllers(); -await app.RunAsync(); \ No newline at end of file +await app.RunAsync(); diff --git a/BikeRental/BikeRental.Api/appsettings.json b/BikeRental/BikeRental.Api/appsettings.json index a4ae2f053..bb9d82ad1 100644 --- a/BikeRental/BikeRental.Api/appsettings.json +++ b/BikeRental/BikeRental.Api/appsettings.json @@ -10,5 +10,19 @@ "BikeRental": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "NatsConsumerSettings": { + "Url": "nats://localhost:4222", + "StreamName": "bike-rental-stream", + "SubjectName": "bike-rental.leases", + "DurableName": "bike-rental-lease-consumer", + "AckWaitSeconds": 30, + "MaxDeliver": 5, + "ConnectRetryAttempts": 5, + "ConnectRetryDelayMs": 2000, + "RetryBackoffFactor": 2, + "ConsumeMaxMsgs": 100, + "ConsumeExpiresSeconds": 30, + "ConsumeRetryDelayMs": 2000 + } } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs index ce9e9f600..ad3ea2a95 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs @@ -15,7 +15,14 @@ public sealed class BikeRentalNatsProducer( { private readonly string _streamName = GetRequired(settings.Value.StreamName, "StreamName"); private readonly string _subjectName = GetRequired(settings.Value.SubjectName, "SubjectName"); - + private readonly INatsSerialize _serializer = BuildSerializer(connection); + private readonly NatsJSPubOpts _publishOptions = new(); + private readonly int _connectRetryAttempts = Math.Max(1, settings.Value.ConnectRetryAttempts); + private readonly TimeSpan _connectRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.ConnectRetryDelayMs)); + private readonly int _publishRetryAttempts = Math.Max(1, settings.Value.PublishRetryAttempts); + private readonly TimeSpan _publishRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.PublishRetryDelayMs)); + private readonly double _retryBackoffFactor = settings.Value.RetryBackoffFactor <= 0 ? 2 : settings.Value.RetryBackoffFactor; + public async Task SendAsync(IList batch, CancellationToken cancellationToken) { if (batch.Count == 0) @@ -26,17 +33,36 @@ public async Task SendAsync(IList batch, CancellationToken try { - await connection.ConnectAsync(); + await ExecuteWithRetryAsync( + "connect to NATS", + _connectRetryAttempts, + _connectRetryDelay, + cancellationToken, + async () => await connection.ConnectAsync()); + var context = connection.CreateJetStreamContext(); var streamConfig = new StreamConfig(_streamName, new List { _subjectName }); - await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken); + await ExecuteWithRetryAsync( + "create/update stream", + _publishRetryAttempts, + _publishRetryDelay, + cancellationToken, + async () => await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken)); var payload = JsonSerializer.SerializeToUtf8Bytes(batch); - await context.PublishAsync( - subject: _subjectName, - data: payload, - cancellationToken: cancellationToken); + await ExecuteWithRetryAsync( + "publish batch", + _publishRetryAttempts, + _publishRetryDelay, + cancellationToken, + async () => await context.PublishAsync( + _subjectName, + payload, + _serializer, + _publishOptions, + new NatsHeaders(), + cancellationToken)); logger.LogInformation("Sent a batch of {count} leases to {subject} of {stream}", batch.Count, _subjectName, _streamName); } @@ -46,7 +72,49 @@ await context.PublishAsync( } } - private static string GetRequired(string? value, string key) // мини проверка + private async Task ExecuteWithRetryAsync( + string operation, + int attempts, + TimeSpan baseDelay, + CancellationToken cancellationToken, + Func action) + { + var delay = baseDelay; + for (var attempt = 1; attempt <= attempts; attempt++) + { + try + { + await action(); + return; + } + catch (Exception ex) when (attempt < attempts && !cancellationToken.IsCancellationRequested) + { + if (delay > TimeSpan.Zero) + { + logger.LogWarning( + ex, + "Failed to {operation} (attempt {attempt}/{attempts}). Retrying in {delay}.", + operation, + attempt, + attempts, + delay); + await Task.Delay(delay, cancellationToken); + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * _retryBackoffFactor); + } + else + { + logger.LogWarning( + ex, + "Failed to {operation} (attempt {attempt}/{attempts}). Retrying immediately.", + operation, + attempt, + attempts); + } + } + } + } + + private static string GetRequired(string? value, string key) { if (string.IsNullOrWhiteSpace(value)) { @@ -54,4 +122,10 @@ private static string GetRequired(string? value, string key) // мини про } return value; } -} \ No newline at end of file + + private static INatsSerialize BuildSerializer(INatsConnection connection) + { + var registry = connection.Opts.SerializerRegistry ?? new NatsDefaultSerializerRegistry(); + return registry.GetSerializer(); + } +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs index 450d8b0b1..881d401be 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs @@ -11,8 +11,8 @@ public IList GenerateBatch(LeaseGenerationOptions settings var batch = new List(settings.BatchSize); for (var i = 0; i < settings.BatchSize; i++) { - var renterId = Random.Shared.Next(settings.RenterIdMin, settings.RenterIdMax + 1); - var bikeId = Random.Shared.Next(settings.BikeIdMin, settings.BikeIdMax + 1); + var renterId = PickId(settings.RenterIds, settings.RenterIdMin, settings.RenterIdMax); + var bikeId = PickId(settings.BikeIds, settings.BikeIdMin, settings.BikeIdMax); var duration = Random.Shared.Next(settings.RentalDurationMinHours, settings.RentalDurationMaxHours + 1); var daysBack = Random.Shared.Next(0, settings.RentalStartDaysBackMax + 1); var hoursBack = Random.Shared.Next(0, 24); @@ -31,16 +31,26 @@ public IList GenerateBatch(LeaseGenerationOptions settings private static void Validate(LeaseGenerationOptions settings) { - if (settings.BikeIdMin > settings.BikeIdMax) + if (settings.BikeIds.Count == 0 && settings.BikeIdMin > settings.BikeIdMax) { throw new InvalidOperationException("LeaseGeneration.BikeIdMin must be <= LeaseGeneration.BikeIdMax."); } - if (settings.RenterIdMin > settings.RenterIdMax) + if (settings.RenterIds.Count == 0 && settings.RenterIdMin > settings.RenterIdMax) { throw new InvalidOperationException("LeaseGeneration.RenterIdMin must be <= LeaseGeneration.RenterIdMax."); } + if (settings.BikeIds.Any(id => id <= 0)) + { + throw new InvalidOperationException("LeaseGeneration.BikeIds must contain only positive IDs."); + } + + if (settings.RenterIds.Any(id => id <= 0)) + { + throw new InvalidOperationException("LeaseGeneration.RenterIds must contain only positive IDs."); + } + if (settings.RentalDurationMinHours > settings.RentalDurationMaxHours) { throw new InvalidOperationException("LeaseGeneration.RentalDurationMinHours must be <= LeaseGeneration.RentalDurationMaxHours."); @@ -51,4 +61,14 @@ private static void Validate(LeaseGenerationOptions settings) throw new InvalidOperationException("LeaseGeneration.RentalStartDaysBackMax must be >= 0."); } } + + private static int PickId(IReadOnlyList ids, int min, int max) + { + if (ids.Count > 0) + { + return ids[Random.Shared.Next(ids.Count)]; + } + + return Random.Shared.Next(min, max + 1); + } } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs index 4fb7af2ed..91980d014 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Options; +using BikeRental.Application.Contracts.Dtos; +using System.Text.Json; namespace BikeRental.Generator.Nats.Host; @@ -33,6 +35,29 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async Task SendBatchAsync(LeaseGenerationOptions settings, CancellationToken stoppingToken) { var batch = generator.GenerateBatch(settings); + LogBatchSample(batch, settings); await producer.SendAsync(batch, stoppingToken); } -} \ No newline at end of file + + private void LogBatchSample(IList batch, LeaseGenerationOptions settings) + { + if (settings.LogBatchSampleCount <= 0) + { + return; + } + + var sampleCount = Math.Min(settings.LogBatchSampleCount, batch.Count); + if (sampleCount == 0) + { + return; + } + + var sample = batch.Take(sampleCount).ToList(); + var payload = JsonSerializer.Serialize(sample); + logger.LogInformation( + "Generated lease batch sample ({sampleCount}/{total}): {payload}", + sampleCount, + batch.Count, + payload); + } +} diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs index e51e1ba76..5bfee7edb 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs @@ -3,13 +3,16 @@ namespace BikeRental.Generator.Nats.Host; // нет проверки на существование велика и арендатора с таким айди public sealed class LeaseGenerationOptions { - public int BatchSize { get; init; } - public int IntervalSeconds { get; init; } - public int BikeIdMin { get; init; } - public int BikeIdMax { get; init; } - public int RenterIdMin { get; init; } - public int RenterIdMax { get; init; } - public int RentalDurationMinHours { get; init; } - public int RentalDurationMaxHours { get; init; } - public int RentalStartDaysBackMax { get; init; } + public int BatchSize { get; init; } = 10; + public int IntervalSeconds { get; init; } = 30; + public int BikeIdMin { get; init; } = 1; + public int BikeIdMax { get; init; } = 30; + public int RenterIdMin { get; init; } = 1; + public int RenterIdMax { get; init; } = 20; + public int RentalDurationMinHours { get; init; } = 1; + public int RentalDurationMaxHours { get; init; } = 72; + public int RentalStartDaysBackMax { get; init; } = 10; + public IReadOnlyList BikeIds { get; init; } = Array.Empty(); + public IReadOnlyList RenterIds { get; init; } = Array.Empty(); + public int LogBatchSampleCount { get; init; } = 0; } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs index 12d445a4f..14c1ee201 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs @@ -5,4 +5,9 @@ public sealed class NatsSettings public string Url { get; init; } = "nats://localhost:4222"; public string StreamName { get; init; } = string.Empty; public string SubjectName { get; init; } = string.Empty; + public int ConnectRetryAttempts { get; init; } = 5; + public int ConnectRetryDelayMs { get; init; } = 2000; + public int PublishRetryAttempts { get; init; } = 3; + public int PublishRetryDelayMs { get; init; } = 1000; + public double RetryBackoffFactor { get; init; } = 2; } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs index 7a36e6c04..7a57c075b 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs @@ -19,7 +19,20 @@ builder.Services.AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; - var options = new NatsOpts { Url = settings.Url }; + var connectRetryDelayMs = Math.Max(0, settings.ConnectRetryDelayMs); + var reconnectWaitMin = TimeSpan.FromMilliseconds(connectRetryDelayMs); + var reconnectWaitMax = TimeSpan.FromMilliseconds( + Math.Max(connectRetryDelayMs, connectRetryDelayMs * Math.Max(settings.RetryBackoffFactor, 1))); + + var options = new NatsOpts + { + Url = settings.Url, + RetryOnInitialConnect = settings.ConnectRetryAttempts > 1, + MaxReconnectRetry = Math.Max(0, settings.ConnectRetryAttempts), + ReconnectWaitMin = reconnectWaitMin, + ReconnectWaitMax = reconnectWaitMax, + ReconnectJitter = TimeSpan.FromMilliseconds(connectRetryDelayMs * 0.2) + }; return new NatsConnection(options); }); @@ -29,4 +42,4 @@ var host = builder.Build(); await host.RunAsync(); -//host.Run(); \ No newline at end of file +//host.Run(); diff --git a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json index d450cb11e..b6757810d 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json +++ b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json @@ -8,7 +8,12 @@ "NatsSettings": { "Url": "nats://localhost:4222", "StreamName": "bike-rental-stream", - "SubjectName": "bike-rental.leases" + "SubjectName": "bike-rental.leases", + "ConnectRetryAttempts": 5, + "ConnectRetryDelayMs": 2000, + "PublishRetryAttempts": 3, + "PublishRetryDelayMs": 1000, + "RetryBackoffFactor": 2 }, "LeaseGeneration": { "BatchSize": 10, @@ -19,6 +24,9 @@ "RenterIdMax": 20, "RentalDurationMinHours": 1, "RentalDurationMaxHours": 72, - "RentalStartDaysBackMax": 10 + "RentalStartDaysBackMax": 10, + "BikeIds": [], + "RenterIds": [], + "LogBatchSampleCount": 0 } } From edbde525aa9c2a7f488e7fd82c85a95bf6b104a7 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 25 Dec 2025 00:37:43 +0400 Subject: [PATCH 44/48] made better structure --- .../BikeRentalNatsProducer.cs | 58 +++++++++++-------- .../{ => Generator}/LeaseBatchGenerator.cs | 2 +- .../{ => Generator}/LeaseGenerationOptions.cs | 4 +- .../LeaseBatchWorker.cs | 28 +-------- .../BikeRental.Generator.Nats.Host/Program.cs | 2 + 5 files changed, 39 insertions(+), 55 deletions(-) rename BikeRental/BikeRental.Generator.Nats.Host/{ => Generator}/LeaseBatchGenerator.cs (98%) rename BikeRental/BikeRental.Generator.Nats.Host/{ => Generator}/LeaseGenerationOptions.cs (75%) diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs index ad3ea2a95..9842f3032 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs @@ -15,13 +15,14 @@ public sealed class BikeRentalNatsProducer( { private readonly string _streamName = GetRequired(settings.Value.StreamName, "StreamName"); private readonly string _subjectName = GetRequired(settings.Value.SubjectName, "SubjectName"); - private readonly INatsSerialize _serializer = BuildSerializer(connection); - private readonly NatsJSPubOpts _publishOptions = new(); + + // для настройки повторных попыток - ретраи private readonly int _connectRetryAttempts = Math.Max(1, settings.Value.ConnectRetryAttempts); private readonly TimeSpan _connectRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.ConnectRetryDelayMs)); private readonly int _publishRetryAttempts = Math.Max(1, settings.Value.PublishRetryAttempts); private readonly TimeSpan _publishRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.PublishRetryDelayMs)); private readonly double _retryBackoffFactor = settings.Value.RetryBackoffFactor <= 0 ? 2 : settings.Value.RetryBackoffFactor; + public async Task SendAsync(IList batch, CancellationToken cancellationToken) { @@ -33,16 +34,21 @@ public async Task SendAsync(IList batch, CancellationToken try { + // await connection.ConnectAsync(); + // вызов с повторными попытками await ExecuteWithRetryAsync( "connect to NATS", - _connectRetryAttempts, - _connectRetryDelay, - cancellationToken, + _connectRetryAttempts,// сколько раз пытаться + _connectRetryDelay,// начальная задержка + cancellationToken,// токен отмены async () => await connection.ConnectAsync()); var context = connection.CreateJetStreamContext(); var streamConfig = new StreamConfig(_streamName, new List { _subjectName }); + + + // await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken); await ExecuteWithRetryAsync( "create/update stream", _publishRetryAttempts, @@ -51,33 +57,38 @@ await ExecuteWithRetryAsync( async () => await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken)); var payload = JsonSerializer.SerializeToUtf8Bytes(batch); + + // await context.PublishAsync(subject: _subjectName, data: payload, cancellationToken: cancellationToken); await ExecuteWithRetryAsync( "publish batch", _publishRetryAttempts, _publishRetryDelay, cancellationToken, async () => await context.PublishAsync( - _subjectName, - payload, - _serializer, - _publishOptions, - new NatsHeaders(), - cancellationToken)); + subject: _subjectName, + data: payload, + cancellationToken: cancellationToken)); - logger.LogInformation("Sent a batch of {count} leases to {subject} of {stream}", batch.Count, _subjectName, _streamName); + logger.LogInformation( + "Sent a batch of {count} leases to {subject} of {stream}", + batch.Count, _subjectName, _streamName); } catch (Exception ex) { - logger.LogError(ex, "Exception occurred during sending a batch of {count} leases to {stream}/{subject}", batch.Count, _streamName, _subjectName); + logger.LogError( + ex, + "Exception occurred during sending a batch of {count} leases to {stream}/{subject}", + batch.Count, _streamName, _subjectName); } } + // механизм повторных попыток private async Task ExecuteWithRetryAsync( string operation, - int attempts, - TimeSpan baseDelay, - CancellationToken cancellationToken, - Func action) + int attempts,// Максимальное количество попыток + TimeSpan baseDelay,// Начальная задержка + CancellationToken cancellationToken,// Токен отмены + Func action)// Операция для выполнения { var delay = baseDelay; for (var attempt = 1; attempt <= attempts; attempt++) @@ -89,6 +100,7 @@ private async Task ExecuteWithRetryAsync( } catch (Exception ex) when (attempt < attempts && !cancellationToken.IsCancellationRequested) { + // есть еще попытки, операцию не отменили if (delay > TimeSpan.Zero) { logger.LogWarning( @@ -99,6 +111,8 @@ private async Task ExecuteWithRetryAsync( attempts, delay); await Task.Delay(delay, cancellationToken); + + // увеличить задержку delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * _retryBackoffFactor); } else @@ -114,7 +128,7 @@ private async Task ExecuteWithRetryAsync( } } - private static string GetRequired(string? value, string key) + private static string GetRequired(string? value, string key) //мини проверка конфигов { if (string.IsNullOrWhiteSpace(value)) { @@ -122,10 +136,4 @@ private static string GetRequired(string? value, string key) } return value; } - - private static INatsSerialize BuildSerializer(INatsConnection connection) - { - var registry = connection.Opts.SerializerRegistry ?? new NatsDefaultSerializerRegistry(); - return registry.GetSerializer(); - } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs similarity index 98% rename from BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs rename to BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs index 881d401be..408ac8ceb 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs @@ -1,6 +1,6 @@ using BikeRental.Application.Contracts.Dtos; -namespace BikeRental.Generator.Nats.Host; +namespace BikeRental.Generator.Nats.Host.Generator; public sealed class LeaseBatchGenerator { diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs similarity index 75% rename from BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs rename to BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs index 5bfee7edb..32b329fcf 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs @@ -1,6 +1,5 @@ -namespace BikeRental.Generator.Nats.Host; +namespace BikeRental.Generator.Nats.Host.Generator; -// нет проверки на существование велика и арендатора с таким айди public sealed class LeaseGenerationOptions { public int BatchSize { get; init; } = 10; @@ -14,5 +13,4 @@ public sealed class LeaseGenerationOptions public int RentalStartDaysBackMax { get; init; } = 10; public IReadOnlyList BikeIds { get; init; } = Array.Empty(); public IReadOnlyList RenterIds { get; init; } = Array.Empty(); - public int LogBatchSampleCount { get; init; } = 0; } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs index 91980d014..3c66e5c6d 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs @@ -1,9 +1,7 @@ using Microsoft.Extensions.Options; -using BikeRental.Application.Contracts.Dtos; -using System.Text.Json; +using BikeRental.Generator.Nats.Host.Generator; namespace BikeRental.Generator.Nats.Host; - public sealed class LeaseBatchWorker( LeaseBatchGenerator generator, BikeRentalNatsProducer producer, @@ -35,29 +33,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async Task SendBatchAsync(LeaseGenerationOptions settings, CancellationToken stoppingToken) { var batch = generator.GenerateBatch(settings); - LogBatchSample(batch, settings); await producer.SendAsync(batch, stoppingToken); } - - private void LogBatchSample(IList batch, LeaseGenerationOptions settings) - { - if (settings.LogBatchSampleCount <= 0) - { - return; - } - - var sampleCount = Math.Min(settings.LogBatchSampleCount, batch.Count); - if (sampleCount == 0) - { - return; - } - - var sample = batch.Take(sampleCount).ToList(); - var payload = JsonSerializer.Serialize(sample); - logger.LogInformation( - "Generated lease batch sample ({sampleCount}/{total}): {payload}", - sampleCount, - batch.Count, - payload); - } } + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs index 7a57c075b..62a1050ad 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs @@ -1,6 +1,8 @@ using BikeRental.Generator.Nats.Host; using Microsoft.Extensions.Options; using NATS.Client.Core; +using BikeRental.Generator.Nats.Host.Generator; + var builder = Host.CreateApplicationBuilder(args); From 488ccbed1be1cda90f10abae4cc7d299f24b3d21 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 25 Dec 2025 17:13:30 +0400 Subject: [PATCH 45/48] added Bogus to the generator --- .../Messaging/NatsLeaseConsumer.cs | 6 +- .../BikeRental.Generator.Nats.Host.csproj | 1 + .../Generator/LeaseBatchGenerator.cs | 98 +++++++++---------- .../Generator/LeaseGenerationOptions.cs | 2 - .../appsettings.json | 6 +- 5 files changed, 52 insertions(+), 61 deletions(-) diff --git a/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs index 48fe5de1c..e87119f2f 100644 --- a/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs +++ b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs @@ -2,8 +2,6 @@ using BikeRental.Application.Contracts.Dtos; using BikeRental.Application.Interfaces; using BikeRental.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NATS.Client.Core; using NATS.Client.JetStream; @@ -148,7 +146,7 @@ private async Task ExecuteWithRetryAsync( CancellationToken stoppingToken, Func action) { - _ = await ExecuteWithRetryAsync( + _ = await ExecuteWithRetryAsync( operation, attempts, baseDelay, @@ -208,7 +206,7 @@ private async Task ExecuteWithRetryAsync( private static INatsDeserialize BuildDeserializer(INatsConnection connection) { - var registry = connection.Opts.SerializerRegistry ?? new NatsDefaultSerializerRegistry(); + var registry = connection.Opts.SerializerRegistry; return registry.GetDeserializer(); } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj index 9ab7fb93d..5b8795135 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj @@ -8,6 +8,7 @@ + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs index 408ac8ceb..ddd08b170 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs @@ -1,3 +1,4 @@ +using Bogus; using BikeRental.Application.Contracts.Dtos; namespace BikeRental.Generator.Nats.Host.Generator; @@ -8,67 +9,60 @@ public IList GenerateBatch(LeaseGenerationOptions settings { Validate(settings); - var batch = new List(settings.BatchSize); - for (var i = 0; i < settings.BatchSize; i++) - { - var renterId = PickId(settings.RenterIds, settings.RenterIdMin, settings.RenterIdMax); - var bikeId = PickId(settings.BikeIds, settings.BikeIdMin, settings.BikeIdMax); - var duration = Random.Shared.Next(settings.RentalDurationMinHours, settings.RentalDurationMaxHours + 1); - var daysBack = Random.Shared.Next(0, settings.RentalStartDaysBackMax + 1); - var hoursBack = Random.Shared.Next(0, 24); + var faker = CreateFaker(settings); + return faker.Generate(settings.BatchSize); + } - batch.Add(new LeaseCreateUpdateDto - { - RenterId = renterId, - BikeId = bikeId, - RentalDuration = duration, - RentalStartTime = DateTime.UtcNow.AddDays(-daysBack).AddHours(-hoursBack) - }); - } + private static Faker CreateFaker( + LeaseGenerationOptions settings) + { + return new Faker() + .RuleFor(x => x.RenterId, f => + f.Random.Int(settings.RenterIdMin, settings.RenterIdMax)) + .RuleFor(x => x.BikeId, f => + f.Random.Int(settings.BikeIdMin, settings.BikeIdMax)) + .RuleFor(x => x.RentalDuration, f => + f.Random.Int( + settings.RentalDurationMinHours, + settings.RentalDurationMaxHours)) + .RuleFor(x => x.RentalStartTime, f => + GeneratePastStartTime( + settings.RentalStartDaysBackMax, + f)); + } + + private static DateTime GeneratePastStartTime( + int maxDaysBack, + Faker f) + { + var daysBack = f.Random.Int(0, maxDaysBack); + var hoursBack = f.Random.Int(0, 23); - return batch; + return DateTime.UtcNow + .AddDays(-daysBack) + .AddHours(-hoursBack); } private static void Validate(LeaseGenerationOptions settings) { - if (settings.BikeIds.Count == 0 && settings.BikeIdMin > settings.BikeIdMax) - { - throw new InvalidOperationException("LeaseGeneration.BikeIdMin must be <= LeaseGeneration.BikeIdMax."); - } + if (settings.BatchSize <= 0) + throw new InvalidOperationException("BatchSize must be > 0."); - if (settings.RenterIds.Count == 0 && settings.RenterIdMin > settings.RenterIdMax) - { - throw new InvalidOperationException("LeaseGeneration.RenterIdMin must be <= LeaseGeneration.RenterIdMax."); - } + if (settings.BikeIdMin > settings.BikeIdMax) + throw new InvalidOperationException( + "BikeIdMin must be <= BikeIdMax."); - if (settings.BikeIds.Any(id => id <= 0)) - { - throw new InvalidOperationException("LeaseGeneration.BikeIds must contain only positive IDs."); - } + if (settings.RenterIdMin > settings.RenterIdMax) + throw new InvalidOperationException( + "RenterIdMin must be <= RenterIdMax."); - if (settings.RenterIds.Any(id => id <= 0)) - { - throw new InvalidOperationException("LeaseGeneration.RenterIds must contain only positive IDs."); - } - - if (settings.RentalDurationMinHours > settings.RentalDurationMaxHours) - { - throw new InvalidOperationException("LeaseGeneration.RentalDurationMinHours must be <= LeaseGeneration.RentalDurationMaxHours."); - } + if (settings.RentalDurationMinHours > + settings.RentalDurationMaxHours) + throw new InvalidOperationException( + "RentalDurationMinHours must be <= RentalDurationMaxHours."); if (settings.RentalStartDaysBackMax < 0) - { - throw new InvalidOperationException("LeaseGeneration.RentalStartDaysBackMax must be >= 0."); - } - } - - private static int PickId(IReadOnlyList ids, int min, int max) - { - if (ids.Count > 0) - { - return ids[Random.Shared.Next(ids.Count)]; - } - - return Random.Shared.Next(min, max + 1); + throw new InvalidOperationException( + "RentalStartDaysBackMax must be >= 0."); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs index 32b329fcf..0010a18d4 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs @@ -11,6 +11,4 @@ public sealed class LeaseGenerationOptions public int RentalDurationMinHours { get; init; } = 1; public int RentalDurationMaxHours { get; init; } = 72; public int RentalStartDaysBackMax { get; init; } = 10; - public IReadOnlyList BikeIds { get; init; } = Array.Empty(); - public IReadOnlyList RenterIds { get; init; } = Array.Empty(); } diff --git a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json index b6757810d..fcc2ddf05 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json +++ b/BikeRental/BikeRental.Generator.Nats.Host/appsettings.json @@ -17,10 +17,10 @@ }, "LeaseGeneration": { "BatchSize": 10, - "IntervalSeconds": 10, - "BikeIdMin": 1, + "IntervalSeconds": 0, + "BikeIdMin": 28, "BikeIdMax": 30, - "RenterIdMin": 1, + "RenterIdMin": 18, "RenterIdMax": 20, "RentalDurationMinHours": 1, "RentalDurationMaxHours": 72, From 983475d27553df02a0a3f1cca2716b8574d350e1 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 25 Dec 2025 17:14:26 +0400 Subject: [PATCH 46/48] made GetAll sorted --- .../BikeRental.Api/Controllers/BikeModelsController.cs | 3 ++- BikeRental/BikeRental.Api/Controllers/BikesController.cs | 5 +++-- BikeRental/BikeRental.Api/Controllers/LeasesController.cs | 3 ++- BikeRental/BikeRental.Api/Controllers/RentersController.cs | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs index 37a7f8139..3f9774286 100644 --- a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs +++ b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs @@ -21,7 +21,8 @@ public sealed class BikeModelsController(IBikeModelService bikeModelService) : C public async Task>> GetAll() { var models = await bikeModelService.GetAll(); - return Ok(models); + var sortedModels = models.OrderBy(model => model.Id).ToList(); + return Ok(sortedModels); } /// diff --git a/BikeRental/BikeRental.Api/Controllers/BikesController.cs b/BikeRental/BikeRental.Api/Controllers/BikesController.cs index 95c716422..8639cbbe5 100644 --- a/BikeRental/BikeRental.Api/Controllers/BikesController.cs +++ b/BikeRental/BikeRental.Api/Controllers/BikesController.cs @@ -17,9 +17,10 @@ public sealed class BikesController(IBikeService bikeService) : ControllerBase /// [HttpGet] public async Task>> GetAll() - { + { var bikes = await bikeService.GetAll(); - return Ok(bikes); + var sortedBikes = bikes.OrderBy(bike => bike.Id).ToList(); + return Ok(sortedBikes); } /// diff --git a/BikeRental/BikeRental.Api/Controllers/LeasesController.cs b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs index 3c6c5eeb8..dfba73ebe 100644 --- a/BikeRental/BikeRental.Api/Controllers/LeasesController.cs +++ b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs @@ -19,7 +19,8 @@ public sealed class LeasesController(ILeaseService leaseService) : ControllerBas public async Task>> GetAll() { var leases = await leaseService.GetAll(); - return Ok(leases); + var sortedLeases = leases.OrderBy(l => l.Id).ToList(); + return Ok(sortedLeases); } /// diff --git a/BikeRental/BikeRental.Api/Controllers/RentersController.cs b/BikeRental/BikeRental.Api/Controllers/RentersController.cs index d535d8c83..f172e210a 100644 --- a/BikeRental/BikeRental.Api/Controllers/RentersController.cs +++ b/BikeRental/BikeRental.Api/Controllers/RentersController.cs @@ -19,7 +19,8 @@ public sealed class RentersController(IRenterService renterService) : Controller public async Task>> GetAll() { var renters = await renterService.GetAll(); - return Ok(renters); + var sortedRenters = renters.OrderBy(renter => renter.Id).ToList(); + return Ok(sortedRenters); } /// From 11cdaa30aaafdf777ef59324074a4869ccbd9e24 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Thu, 25 Dec 2025 17:59:55 +0400 Subject: [PATCH 47/48] updated README.md --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 243dd7123..013504d3e 100644 --- a/README.md +++ b/README.md @@ -152,4 +152,45 @@ Bogus генерировал реалистичные тестовые данн * База данных: bike-rental - не обязательно ### Swagger -- Для получения доступа к backend: `http://localhost:5043/swagger/index.html` - клиент для тестирования \ No newline at end of file +- Для получения доступа к backend: `http://localhost:5043/swagger/index.html` - клиент для тестирования + +# Лабораторная 4 + +## Основные компоненты + +### `BikeRental.Generator.Nats.Host/Program.cs` +- Настройка конфигурации, подключение к NATS, регистрация генератора и воркера в DI +- Использование `NatsConnection` с URL из конфигурации + +### `BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs` +- Отправка батчей аренды в NATS JetStream +- Реализация механизма повторных попыток (retry) для подключения и публикации +- Автоматическое создание/обновление stream при необходимости +- Логирование операций и ошибок + +### `BikeRental.Api/Messaging/NatsLeaseConsumer.cs` +- Фоновый сервис для чтения батчей аренды из NATS JetStream +- Механизм подтверждения сообщений: `ack` только после успешной записи всего батча в БД +- Обработка невалидных данных: при несуществующих `BikeId`/`RenterId` сообщение завершается (`AckTerminateAsync`), чтобы избежать зацикливания +- Реализация повторных попыток подключения и обработки сообщений +- Использование транзакций для атомарного сохранения всего батча + +### `BikeRental.Generator.Nats.Host/LeaseBatchGenerator.cs` +- Генерация тестовых данных `LeaseCreateUpdateDto` с использованием Bogus +- Диапазоны ID сделала для наглядности: арендаторы (18-20), велосипеды (28-30) + +### `BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs` +- Фоновый воркер для периодической генерации и отправки данных +- Возможен только 1 запуск, если `IntervalSeconds ≤ 0` в конфигурации + +## Классы для типизированной конфигурации + +- `BikeRental.Generator.Nats.Host/NatsSettings.cs` - настройки подключения к NATS, имена stream/subject для генератора +- `BikeRental.Api/Messaging/NatsConsumerSettings.cs` - настройки потребителя NATS в API +- `BikeRental.Generator.Nats.Host/LeaseGenerationOptions.cs` - параметры генерации данных (диапазоны ID) + +## Конфигурация (`appsettings.json`) +- `IntervalSeconds` = `0` или отрицательное значение - однократная отправка при старте (в воркере) + + +## Проект запускается через Aspire AppHost From dce1348445aacc883542f083c8324d4d05af85c7 Mon Sep 17 00:00:00 2001 From: dariatsyganova Date: Fri, 26 Dec 2025 17:19:40 +0400 Subject: [PATCH 48/48] replaced CancellationToken, simplified collection initialization, cleaned up the code (reformatted), made GenerateBatch static --- BikeRental/AppHost/AppHost.csproj | 12 +- BikeRental/AppHost/Program.cs | 24 +- .../BikeRental.Api/BikeRental.Api.csproj | 42 +- .../Controllers/BikeModelsController.cs | 36 +- .../Controllers/BikesController.cs | 27 +- .../Controllers/LeasesController.cs | 25 +- .../Controllers/RentersController.cs | 25 +- .../BikeRental.Api/DependencyInjection.cs | 23 +- .../Extensions/DatabaseExtensions.cs | 10 +- .../Extensions/SeedDataExtensions.cs | 10 +- .../Messaging/NatsConsumerSettings.cs | 2 +- .../Messaging/NatsLeaseConsumer.cs | 57 +- .../Middleware/GlobalExceptionHandler.cs | 58 +- BikeRental/BikeRental.Api/Program.cs | 24 +- .../appsettings.Development.json | 2 - .../BikeRental.Application.Contracts.csproj | 2 +- .../Dtos/BikeCreateUpdateDto.cs | 6 +- .../Dtos/BikeDto.cs | 11 +- .../Dtos/BikeModelCreateUpdateDto.cs | 14 +- .../Dtos/BikeModelDto.cs | 18 +- .../Dtos/LeaseCreateUpdateDto.cs | 8 +- .../Dtos/LeaseDto.cs | 13 +- .../Dtos/RenterCreateUpdateDto.cs | 4 +- .../Dtos/RenterDto.cs | 8 +- .../BikeRental.Application.csproj | 2 +- .../Interfaces/IBikeModelService.cs | 11 +- .../Interfaces/IBikeService.cs | 3 +- .../Interfaces/ILeaseService.cs | 2 +- .../Interfaces/IRenterService.cs | 2 +- .../Mappings/BikeMappings.cs | 4 +- .../Mappings/BikeModelMappings.cs | 2 +- .../Mappings/LeaseMappings.cs | 6 +- .../Mappings/RenterMappings.cs | 2 +- .../Services/BikeModelService.cs | 23 +- .../Services/BikeService.cs | 36 +- .../Services/LeaseService.cs | 49 +- .../Services/RenterService.cs | 25 +- BikeRental/BikeRental.Domain/Enum/BikeType.cs | 14 +- .../Interfaces/IBikeModelRepository.cs | 2 +- .../Interfaces/IBikeRepository.cs | 2 +- .../Interfaces/ILeaseRepository.cs | 2 +- .../Interfaces/IRenterRepository.cs | 2 +- .../Interfaces/IRepository.cs | 17 +- BikeRental/BikeRental.Domain/Models/Bike.cs | 15 +- .../BikeRental.Domain/Models/BikeModel.cs | 20 +- BikeRental/BikeRental.Domain/Models/Lease.cs | 26 +- BikeRental/BikeRental.Domain/Models/Renter.cs | 8 +- .../BikeRental.Generator.Nats.Host.csproj | 4 +- .../BikeRentalNatsProducer.cs | 68 +-- .../Generator/LeaseBatchGenerator.cs | 20 +- .../Generator/LeaseGenerationOptions.cs | 2 +- .../LeaseBatchWorker.cs | 12 +- .../NatsSettings.cs | 7 +- .../BikeRental.Generator.Nats.Host/Program.cs | 14 +- .../Configurations/BikeConfiguration.cs | 4 +- .../Configurations/BikeModelConfiguration.cs | 4 +- .../Configurations/RenterConfiguration.cs | 4 +- .../Repositories/BikeModelRepository.cs | 2 +- .../Services/ISeedDataService.cs | 4 +- .../Services/Impl/SeedDataService.cs | 10 +- .../BikeRental.Tests/BikeRental.Tests.csproj | 8 +- .../BikeRental.Tests/BikeRentalTests.cs | 38 +- BikeRental/BikeRental.Tests/RentalFixture.cs | 506 ++++++++++-------- 63 files changed, 765 insertions(+), 678 deletions(-) diff --git a/BikeRental/AppHost/AppHost.csproj b/BikeRental/AppHost/AppHost.csproj index bcf809e7f..bbcf587dc 100644 --- a/BikeRental/AppHost/AppHost.csproj +++ b/BikeRental/AppHost/AppHost.csproj @@ -1,6 +1,6 @@ - + Exe @@ -11,9 +11,9 @@ - - - + + + @@ -23,8 +23,8 @@ - - + + diff --git a/BikeRental/AppHost/Program.cs b/BikeRental/AppHost/Program.cs index 707cf4351..e76e8ba22 100644 --- a/BikeRental/AppHost/Program.cs +++ b/BikeRental/AppHost/Program.cs @@ -1,28 +1,30 @@ -var builder = DistributedApplication.CreateBuilder(args); +using Projects; -var nats = builder.AddContainer("nats", "nats:2.10") +IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args); + +IResourceBuilder nats = builder.AddContainer("nats", "nats:2.10") .WithArgs("-js") .WithEndpoint(4222, 4222); -var bikeRentalDbPassword = builder.AddParameter( - name: "bike-rental-db-password", - value: "1234512345Aa$", +IResourceBuilder bikeRentalDbPassword = builder.AddParameter( + "bike-rental-db-password", + "1234512345Aa$", secret: true); -var bikeRentalSql = builder.AddMySql("bike-rental-db", - password: bikeRentalDbPassword) +IResourceBuilder bikeRentalSql = builder.AddMySql("bike-rental-db", + bikeRentalDbPassword) .WithAdminer() .WithDataVolume("bike-rental-volume"); -var bikeRentalDb = +IResourceBuilder bikeRentalDb = bikeRentalSql.AddDatabase("bike-rental"); -builder.AddProject("bike-rental-nats-generator") +builder.AddProject("bike-rental-nats-generator") .WaitFor(nats) .WithEnvironment("Nats__Url", "nats://localhost:4222") .WithEnvironment("Nats__StreamName", "bike-rental-stream") .WithEnvironment("Nats__SubjectName", "bike-rental.leases"); -builder.AddProject("bike-rental-api") +builder.AddProject("bike-rental-api") .WaitFor(bikeRentalDb) .WaitFor(nats) .WithReference(bikeRentalDb) @@ -30,4 +32,4 @@ .WithEnvironment("Nats__StreamName", "bike-rental-stream") .WithEnvironment("Nats__SubjectName", "bike-rental.leases"); -builder.Build().Run(); +builder.Build().Run(); \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/BikeRental.Api.csproj b/BikeRental/BikeRental.Api/BikeRental.Api.csproj index af587b3e0..ed573c19b 100644 --- a/BikeRental/BikeRental.Api/BikeRental.Api.csproj +++ b/BikeRental/BikeRental.Api/BikeRental.Api.csproj @@ -10,19 +10,19 @@ - + - - + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -31,20 +31,20 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -52,6 +52,6 @@ appsettings.json - + diff --git a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs index 3f9774286..6d58c4b04 100644 --- a/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs +++ b/BikeRental/BikeRental.Api/Controllers/BikeModelsController.cs @@ -5,33 +5,33 @@ namespace BikeRental.Api.Controllers; /// -/// Контроллер описывает конечные точки для работы с -/// ресурсом "BikeModel" (модель велосипеда) -/// "bikeModelService" - сервис для работы с ресурсом BikeModel -/// Зависимость от интерфейса, а не конкретной реализации в сервисе (SOLID - DIP) +/// Контроллер описывает конечные точки для работы с +/// ресурсом "BikeModel" (модель велосипеда) +/// "bikeModelService" - сервис для работы с ресурсом BikeModel +/// Зависимость от интерфейса, а не конкретной реализации в сервисе (SOLID - DIP) /// [ApiController] [Route("bike-models")] public sealed class BikeModelsController(IBikeModelService bikeModelService) : ControllerBase { /// - /// Получить все модели велосипедов + /// Получить все модели велосипедов /// [HttpGet] public async Task>> GetAll() { - var models = await bikeModelService.GetAll(); + IEnumerable models = await bikeModelService.GetAll(); var sortedModels = models.OrderBy(model => model.Id).ToList(); return Ok(sortedModels); } /// - /// Получить модель велосипеда по идентификатору + /// Получить модель велосипеда по идентификатору /// [HttpGet("{id:int}")] public async Task> GetById(int id) { - var model = await bikeModelService.GetById(id); + BikeModelDto? model = await bikeModelService.GetById(id); if (model is null) { return NotFound(); @@ -41,33 +41,34 @@ public async Task> GetById(int id) } /// - /// Создать новую модель велосипеда - /// "dto" - модель для создания модели велосипеда + /// Создать новую модель велосипеда + /// "dto" - модель для создания модели велосипеда /// [HttpPost] public async Task> Create([FromBody] BikeModelCreateUpdateDto dto) { - var created = await bikeModelService.Create(dto); - - return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + BikeModelDto created = await bikeModelService.Create(dto); + + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } /// - /// Обновить существующую модель велосипеда + /// Обновить существующую модель велосипеда /// [HttpPut("{id:int}")] public async Task> Update(int id, [FromBody] BikeModelCreateUpdateDto dto) { - var updated = await bikeModelService.Update(id, dto); + BikeModelDto? updated = await bikeModelService.Update(id, dto); if (updated is null) { return NotFound(); } + return Ok(updated); } /// - /// Удалить модель велосипеда по идентификатору + /// Удалить модель велосипеда по идентификатору /// [HttpDelete("{id:int}")] public async Task Delete(int id) @@ -77,6 +78,7 @@ public async Task Delete(int id) { return NotFound(); } - return NoContent(); + + return NoContent(); } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/BikesController.cs b/BikeRental/BikeRental.Api/Controllers/BikesController.cs index 8639cbbe5..6faf806e7 100644 --- a/BikeRental/BikeRental.Api/Controllers/BikesController.cs +++ b/BikeRental/BikeRental.Api/Controllers/BikesController.cs @@ -5,64 +5,66 @@ namespace BikeRental.Api.Controllers; /// -/// Контроллер описывает конечные точки для работы -/// с ресурсом "Bike" (велосипед) +/// Контроллер описывает конечные точки для работы +/// с ресурсом "Bike" (велосипед) /// [ApiController] [Route("bikes")] public sealed class BikesController(IBikeService bikeService) : ControllerBase { /// - /// Получить все ресурсы Bike + /// Получить все ресурсы Bike /// [HttpGet] public async Task>> GetAll() - { - var bikes = await bikeService.GetAll(); + { + IEnumerable bikes = await bikeService.GetAll(); var sortedBikes = bikes.OrderBy(bike => bike.Id).ToList(); return Ok(sortedBikes); } /// - /// Получить ресурс по идентификатору Bike + /// Получить ресурс по идентификатору Bike /// [HttpGet("{id:int}")] public async Task> GetById(int id) { - var bike = await bikeService.GetById(id); + BikeDto? bike = await bikeService.GetById(id); if (bike is null) { return NotFound(); } + return Ok(bike); } /// - /// Создать новый ресурс Bike + /// Создать новый ресурс Bike /// [HttpPost] public async Task> Create([FromBody] BikeCreateUpdateDto dto) { - var created = await bikeService.Create(dto); + BikeDto created = await bikeService.Create(dto); return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } /// - /// Обновить существующий ресурс Bike + /// Обновить существующий ресурс Bike /// [HttpPut("{id:int}")] public async Task> Update(int id, [FromBody] BikeCreateUpdateDto dto) { - var updated = await bikeService.Update(id, dto); + BikeDto? updated = await bikeService.Update(id, dto); if (updated is null) { return NotFound(); } + return Ok(updated); } /// - /// Удалить ресурс Bike + /// Удалить ресурс Bike /// [HttpDelete("{id:int}")] public async Task Delete(int id) @@ -72,6 +74,7 @@ public async Task Delete(int id) { return NotFound(); } + return NoContent(); } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/LeasesController.cs b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs index dfba73ebe..acecd1cf8 100644 --- a/BikeRental/BikeRental.Api/Controllers/LeasesController.cs +++ b/BikeRental/BikeRental.Api/Controllers/LeasesController.cs @@ -5,64 +5,66 @@ namespace BikeRental.Api.Controllers; /// -/// Контроллер описывает конечные точки для работы с ресурсом -/// "Lease" (договор на аренду велосипеда) +/// Контроллер описывает конечные точки для работы с ресурсом +/// "Lease" (договор на аренду велосипеда) /// [ApiController] [Route("leases")] public sealed class LeasesController(ILeaseService leaseService) : ControllerBase { /// - /// Получить все договора на аренду велосипедов + /// Получить все договора на аренду велосипедов /// [HttpGet] public async Task>> GetAll() { - var leases = await leaseService.GetAll(); + IEnumerable leases = await leaseService.GetAll(); var sortedLeases = leases.OrderBy(l => l.Id).ToList(); return Ok(sortedLeases); } /// - /// Получить договор на аренду велосипеда по идентификатору + /// Получить договор на аренду велосипеда по идентификатору /// [HttpGet("{id:int}")] public async Task> GetById(int id) { - var lease = await leaseService.GetById(id); + LeaseDto? lease = await leaseService.GetById(id); if (lease is null) { return NotFound(); } + return Ok(lease); } /// - /// Создать договор на аренду велосипеда + /// Создать договор на аренду велосипеда /// [HttpPost] public async Task> Create([FromBody] LeaseCreateUpdateDto dto) { - var created = await leaseService.Create(dto); + LeaseDto created = await leaseService.Create(dto); return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } /// - /// Обновить состояние текущего договора на аренду велосипеда + /// Обновить состояние текущего договора на аренду велосипеда /// [HttpPut("{id:int}")] public async Task> Update(int id, [FromBody] LeaseCreateUpdateDto dto) { - var updated = await leaseService.Update(id, dto); + LeaseDto? updated = await leaseService.Update(id, dto); if (updated is null) { return NotFound(); } + return Ok(updated); } /// - /// Удалить договор на аренду велосипеда по идентификатору + /// Удалить договор на аренду велосипеда по идентификатору /// [HttpDelete("{id:int}")] public async Task Delete(int id) @@ -72,6 +74,7 @@ public async Task Delete(int id) { return NotFound(); } + return NoContent(); } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Controllers/RentersController.cs b/BikeRental/BikeRental.Api/Controllers/RentersController.cs index f172e210a..7b68bf80e 100644 --- a/BikeRental/BikeRental.Api/Controllers/RentersController.cs +++ b/BikeRental/BikeRental.Api/Controllers/RentersController.cs @@ -5,68 +5,70 @@ namespace BikeRental.Api.Controllers; /// -/// Контроллер описывает конечные точки для работы с ресурсом -/// "Renter" (арендатор) +/// Контроллер описывает конечные точки для работы с ресурсом +/// "Renter" (арендатор) /// [ApiController] [Route("renters")] public sealed class RentersController(IRenterService renterService) : ControllerBase { /// - /// Получить всех арендаторов + /// Получить всех арендаторов /// [HttpGet] public async Task>> GetAll() { - var renters = await renterService.GetAll(); + IEnumerable renters = await renterService.GetAll(); var sortedRenters = renters.OrderBy(renter => renter.Id).ToList(); return Ok(sortedRenters); } /// - /// Получить арендатора по идентификатору + /// Получить арендатора по идентификатору /// /// [HttpGet("{id:int}")] public async Task> GetById(int id) { - var renter = await renterService.GetById(id); + RenterDto? renter = await renterService.GetById(id); if (renter is null) { return NotFound(); } + return Ok(renter); } /// - /// Создать нового арендатора + /// Создать нового арендатора /// /// [HttpPost] public async Task> Create([FromBody] RenterCreateUpdateDto dto) { - var created = await renterService.Create(dto); + RenterDto created = await renterService.Create(dto); return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); } /// - /// Обновить существующего арендатора + /// Обновить существующего арендатора /// /// /// [HttpPut("{id:int}")] public async Task> Update(int id, [FromBody] RenterCreateUpdateDto dto) { - var updated = await renterService.Update(id, dto); + RenterDto? updated = await renterService.Update(id, dto); if (updated is null) { return NotFound(); } + return Ok(updated); } /// - /// Удалить арендатора по идентификатору + /// Удалить арендатора по идентификатору /// /// [HttpDelete("{id:int}")] @@ -77,6 +79,7 @@ public async Task Delete(int id) { return NotFound(); } + return NoContent(); } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/DependencyInjection.cs b/BikeRental/BikeRental.Api/DependencyInjection.cs index ed7877d62..3e9100829 100644 --- a/BikeRental/BikeRental.Api/DependencyInjection.cs +++ b/BikeRental/BikeRental.Api/DependencyInjection.cs @@ -16,12 +16,12 @@ namespace BikeRental.Api; /// -/// Настройка зависимостей приложения +/// Настройка зависимостей приложения /// public static class DependencyInjection { /// - /// Зарегистрировать и настроить сервисы контроллеров + /// Зарегистрировать и настроить сервисы контроллеров /// public static void AddControllers(this WebApplicationBuilder builder) { @@ -32,17 +32,17 @@ public static void AddControllers(this WebApplicationBuilder builder) } /// - /// Зарегистрировать и настроить сервисы обработки ошибок + /// Зарегистрировать и настроить сервисы обработки ошибок /// public static void AddErrorHandling(this WebApplicationBuilder builder) { builder.Services.AddExceptionHandler(); - + builder.Services.AddProblemDetails(); } /// - /// Зарегистрировать и настроить сервисы OpenTelemetry + /// Зарегистрировать и настроить сервисы OpenTelemetry /// public static void AddObservability(this WebApplicationBuilder builder) { @@ -68,14 +68,13 @@ public static void AddObservability(this WebApplicationBuilder builder) // Настроить ведение журнала OpenTelemetry builder.Logging.AddOpenTelemetry(options => { - options.IncludeScopes = true; // Включить области + options.IncludeScopes = true; // Включить области options.IncludeFormattedMessage = true; // Включить форматированные сообщения }); - } /// - /// Зарегистрировать и настроить сервисы взаимодействия с базой данных + /// Зарегистрировать и настроить сервисы взаимодействия с базой данных /// public static void AddDatabase(this WebApplicationBuilder builder) { @@ -91,7 +90,7 @@ public static void AddDatabase(this WebApplicationBuilder builder) } /// - /// Зарегистрировать репозитории + /// Зарегистрировать репозитории /// public static void AddRepositories(this WebApplicationBuilder builder) { @@ -102,17 +101,17 @@ public static void AddRepositories(this WebApplicationBuilder builder) } /// - /// Регистрация сервисов общего назначения + /// Регистрация сервисов общего назначения /// public static void AddServices(this WebApplicationBuilder builder) { // Зарегистрировать сервис инициализации данных builder.Services.AddScoped(); - + // Зарегистрировать сервисы прикладного уровня builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs index f53924464..17286f012 100644 --- a/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/DatabaseExtensions.cs @@ -4,12 +4,12 @@ namespace BikeRental.Api.Extensions; /// -/// Предоставляет методы расширения для работы с базой данных +/// Предоставляет методы расширения для работы с базой данных /// public static class DatabaseExtensions { /// - /// Применить все миграции к базе данных + /// Применить все миграции к базе данных /// /// public static async Task ApplyMigrationsAsync(this WebApplication app) @@ -17,8 +17,8 @@ public static async Task ApplyMigrationsAsync(this WebApplication app) // мы не в HTTP запросе тк это запуск приложения // поэтому создаем Scope(один из уровней DI контейнера) вручную, как бы новую область видимости для DI // Scope гарантирует, что все зависимости будут правильно созданы и уничтожены - using var scope = app.Services.CreateScope(); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); + using IServiceScope scope = app.Services.CreateScope(); + await using ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService(); // scope.ServiceProvider - DI контейнер в рамках созданного Scope // GetRequiredService() - получить сервис типа T // Требует, чтобы сервис был зарегистрирован, иначе исключение @@ -35,4 +35,4 @@ public static async Task ApplyMigrationsAsync(this WebApplication app) throw; } } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs b/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs index a8ed9b333..4b761d2f9 100644 --- a/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs +++ b/BikeRental/BikeRental.Api/Extensions/SeedDataExtensions.cs @@ -3,19 +3,19 @@ namespace BikeRental.Api.Extensions; /// -/// Предоставляет методы расширения для выполнения -/// первичной инициализации данных в бд +/// Предоставляет методы расширения для выполнения +/// первичной инициализации данных в бд /// public static class SeedDataExtensions { /// - /// Проинициализировать данные в бд + /// Проинициализировать данные в бд /// /// public static async Task SeedData(this IApplicationBuilder app) { - using var scope = app.ApplicationServices.CreateScope(); - var seedDataService = scope.ServiceProvider.GetRequiredService(); + using IServiceScope scope = app.ApplicationServices.CreateScope(); + ISeedDataService seedDataService = scope.ServiceProvider.GetRequiredService(); await seedDataService.SeedDataAsync(); } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs b/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs index 2cef2226e..6d5e73266 100644 --- a/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs +++ b/BikeRental/BikeRental.Api/Messaging/NatsConsumerSettings.cs @@ -14,4 +14,4 @@ internal sealed class NatsConsumerSettings public int ConsumeMaxMsgs { get; init; } = 100; public int ConsumeExpiresSeconds { get; init; } = 30; public int ConsumeRetryDelayMs { get; init; } = 2000; -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs index e87119f2f..f5450db42 100644 --- a/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs +++ b/BikeRental/BikeRental.Api/Messaging/NatsLeaseConsumer.cs @@ -2,6 +2,7 @@ using BikeRental.Application.Contracts.Dtos; using BikeRental.Application.Interfaces; using BikeRental.Infrastructure.Database; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Options; using NATS.Client.Core; using NATS.Client.JetStream; @@ -16,8 +17,8 @@ internal sealed class NatsLeaseConsumer( IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { - private readonly NatsConsumerSettings _settings = settings.Value; private readonly INatsDeserialize _deserializer = BuildDeserializer(connection); + private readonly NatsConsumerSettings _settings = settings.Value; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -27,17 +28,18 @@ await ExecuteWithRetryAsync( "connect to NATS", _settings.ConnectRetryAttempts, TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), - stoppingToken, - async () => await connection.ConnectAsync()); + async () => await connection.ConnectAsync(), + stoppingToken); + + INatsJSContext context = connection.CreateJetStreamContext(); + var streamConfig = new StreamConfig(_settings.StreamName, [_settings.SubjectName]); - var context = connection.CreateJetStreamContext(); - var streamConfig = new StreamConfig(_settings.StreamName, new List { _settings.SubjectName }); await ExecuteWithRetryAsync( "create/update stream", _settings.ConnectRetryAttempts, TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), - stoppingToken, - async () => await context.CreateOrUpdateStreamAsync(streamConfig, stoppingToken)); + async () => await context.CreateOrUpdateStreamAsync(streamConfig, stoppingToken), + stoppingToken); var consumerConfig = new ConsumerConfig { @@ -51,12 +53,12 @@ await ExecuteWithRetryAsync( MaxDeliver = Math.Max(1, _settings.MaxDeliver) }; - var consumer = await ExecuteWithRetryAsync( + INatsJSConsumer consumer = await ExecuteWithRetryAsync( "create/update consumer", _settings.ConnectRetryAttempts, TimeSpan.FromMilliseconds(_settings.ConnectRetryDelayMs), - stoppingToken, - async () => await context.CreateOrUpdateConsumerAsync(_settings.StreamName, consumerConfig, stoppingToken)); + async () => await context.CreateOrUpdateConsumerAsync(_settings.StreamName, consumerConfig, stoppingToken), + stoppingToken); var consumeOptions = new NatsJSConsumeOpts { @@ -68,14 +70,16 @@ await ExecuteWithRetryAsync( { try { - await foreach (var msg in consumer.ConsumeAsync(_deserializer, consumeOptions, stoppingToken)) + await foreach (INatsJSMsg msg in consumer.ConsumeAsync(_deserializer, consumeOptions, + stoppingToken)) { await HandleMessageAsync(msg, stoppingToken); } } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { - logger.LogError(ex, "Error while consuming leases from NATS. Retrying in {delay}ms.", _settings.ConsumeRetryDelayMs); + logger.LogError(ex, "Error while consuming leases from NATS. Retrying in {delay}ms.", + _settings.ConsumeRetryDelayMs); await Task.Delay(Math.Max(0, _settings.ConsumeRetryDelayMs), stoppingToken); } } @@ -127,15 +131,16 @@ private async Task HandleMessageAsync(INatsJSMsg msg, CancellationToken private async Task SaveBatchAsync(IReadOnlyList leases, CancellationToken stoppingToken) { - await using var scope = scopeFactory.CreateAsyncScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var leaseService = scope.ServiceProvider.GetRequiredService(); + await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope(); + ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService(); + ILeaseService leaseService = scope.ServiceProvider.GetRequiredService(); - await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); - foreach (var lease in leases) + await using IDbContextTransaction transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + foreach (LeaseCreateUpdateDto lease in leases) { await leaseService.Create(lease); } + await transaction.CommitAsync(stoppingToken); } @@ -143,30 +148,30 @@ private async Task ExecuteWithRetryAsync( string operation, int attempts, TimeSpan baseDelay, - CancellationToken stoppingToken, - Func action) + Func action, + CancellationToken stoppingToken) { _ = await ExecuteWithRetryAsync( operation, attempts, baseDelay, - stoppingToken, async () => { await action(); return new object(); - }); + }, + stoppingToken); } private async Task ExecuteWithRetryAsync( string operation, int attempts, TimeSpan baseDelay, - CancellationToken stoppingToken, - Func> action) + Func> action, + CancellationToken stoppingToken) { var retries = Math.Max(1, attempts); - var delay = baseDelay; + TimeSpan delay = baseDelay; var backoff = _settings.RetryBackoffFactor <= 0 ? 2 : _settings.RetryBackoffFactor; for (var attempt = 1; attempt <= retries; attempt++) @@ -206,7 +211,7 @@ private async Task ExecuteWithRetryAsync( private static INatsDeserialize BuildDeserializer(INatsConnection connection) { - var registry = connection.Opts.SerializerRegistry; + INatsSerializerRegistry registry = connection.Opts.SerializerRegistry; return registry.GetDeserializer(); } @@ -222,4 +227,4 @@ private static void ValidateSettings(NatsConsumerSettings settings) throw new KeyNotFoundException("SubjectName is not configured in Nats section."); } } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs index a4f405c7d..b937180db 100644 --- a/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs +++ b/BikeRental/BikeRental.Api/Middleware/GlobalExceptionHandler.cs @@ -4,7 +4,7 @@ namespace BikeRental.Api.Middleware; /// -/// Глобальный обработчик исключений с логированием +/// Глобальный обработчик исключений с логированием /// public sealed class GlobalExceptionHandler( IProblemDetailsService problemDetailsService, @@ -12,7 +12,7 @@ public sealed class GlobalExceptionHandler( : IExceptionHandler { /// - /// Попытаться обработать исключение + /// Попытаться обработать исключение /// public async ValueTask TryHandleAsync( HttpContext httpContext, @@ -21,8 +21,8 @@ public async ValueTask TryHandleAsync( { // понятным сообщением сделать логи LogExceptionWithSimpleMessage(httpContext, exception); - - var problemDetails = CreateProblemDetails(exception); + + ProblemDetails problemDetails = CreateProblemDetails(exception); return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext { @@ -43,11 +43,11 @@ public async ValueTask TryHandleAsync( } /// - /// Логирование с короткими понятными сообщениями + /// Логирование с короткими понятными сообщениями /// private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception exception) { - var requestPath = httpContext.Request.Path; + PathString requestPath = httpContext.Request.Path; var method = httpContext.Request.Method; var exceptionType = exception.GetType().Name; @@ -86,12 +86,12 @@ private void LogExceptionWithSimpleMessage(HttpContext httpContext, Exception ex } /// - /// Создание ProblemDetails + /// Создание ProblemDetails /// private static ProblemDetails CreateProblemDetails(Exception exception) { var statusCode = GetStatusCode(exception); - + return new ProblemDetails { Title = GetTitle(exception), @@ -101,31 +101,37 @@ private static ProblemDetails CreateProblemDetails(Exception exception) } /// - /// Получение статус кода + /// Получение статус кода /// - private static int GetStatusCode(Exception exception) => exception switch + private static int GetStatusCode(Exception exception) { - KeyNotFoundException => StatusCodes.Status404NotFound, - ArgumentException => StatusCodes.Status400BadRequest, - InvalidOperationException => StatusCodes.Status400BadRequest, - UnauthorizedAccessException => StatusCodes.Status401Unauthorized, - _ => StatusCodes.Status500InternalServerError - }; + return exception switch + { + KeyNotFoundException => StatusCodes.Status404NotFound, + ArgumentException => StatusCodes.Status400BadRequest, + InvalidOperationException => StatusCodes.Status400BadRequest, + UnauthorizedAccessException => StatusCodes.Status401Unauthorized, + _ => StatusCodes.Status500InternalServerError + }; + } /// - /// Получение заголовка + /// Получение заголовка /// - private static string GetTitle(Exception exception) => exception switch + private static string GetTitle(Exception exception) { - KeyNotFoundException => "Resource not found", - ArgumentException => "Bad request", - InvalidOperationException => "Invalid operation", - UnauthorizedAccessException => "Unauthorized", - _ => "Internal server error" - }; + return exception switch + { + KeyNotFoundException => "Resource not found", + ArgumentException => "Bad request", + InvalidOperationException => "Invalid operation", + UnauthorizedAccessException => "Unauthorized", + _ => "Internal server error" + }; + } /// - /// Получение деталей + /// Получение деталей /// private static string GetDetail(Exception exception) { @@ -134,7 +140,7 @@ private static string GetDetail(Exception exception) { return exception.Message; } - + // Для серверных ошибок - общее сообщение return "An error occurred while processing your request. Please try again later."; } diff --git a/BikeRental/BikeRental.Api/Program.cs b/BikeRental/BikeRental.Api/Program.cs index d64fd9ee8..eeb8309db 100644 --- a/BikeRental/BikeRental.Api/Program.cs +++ b/BikeRental/BikeRental.Api/Program.cs @@ -1,11 +1,11 @@ using BikeRental.Api; -using BikeRental.Api.Messaging; using BikeRental.Api.Extensions; -using Microsoft.OpenApi.Models; +using BikeRental.Api.Messaging; using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; using NATS.Client.Core; -var builder = WebApplication.CreateBuilder(args); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.AddControllers(); builder.AddErrorHandling(); @@ -19,9 +19,9 @@ Version = "v1", Description = "API для управления сервисом проката велосипедов" }); - + var basePath = AppContext.BaseDirectory; - var xmlPathApi = Path.Combine(basePath, $"BikeRental.Api.xml"); + var xmlPathApi = Path.Combine(basePath, "BikeRental.Api.xml"); options.IncludeXmlComments(xmlPathApi); }); @@ -31,7 +31,7 @@ builder.AddRepositories(); builder.AddServices(); -var natsSettingsSection = builder.Configuration.GetSection("NatsConsumerSettings"); +IConfigurationSection natsSettingsSection = builder.Configuration.GetSection("NatsConsumerSettings"); if (natsSettingsSection.Exists()) { @@ -42,7 +42,7 @@ builder.Services.Configure(builder.Configuration.GetSection("NatsConsumerSettings")); builder.Services.AddSingleton(sp => { - var settings = sp.GetRequiredService>().Value; + NatsConsumerSettings settings = sp.GetRequiredService>().Value; var connectRetryDelayMs = Math.Max(0, settings.ConnectRetryDelayMs); var reconnectWaitMin = TimeSpan.FromMilliseconds(connectRetryDelayMs); var reconnectWaitMax = TimeSpan.FromMilliseconds( @@ -61,7 +61,7 @@ }); builder.Services.AddHostedService(); -var app = builder.Build(); +WebApplication app = builder.Build(); app.UseExceptionHandler(); @@ -75,12 +75,12 @@ c.RoutePrefix = "swagger"; c.ShowCommonExtensions(); }); - + await app.ApplyMigrationsAsync(); - + await app.SeedData(); } -app.MapControllers(); +app.MapControllers(); -await app.RunAsync(); +await app.RunAsync(); \ No newline at end of file diff --git a/BikeRental/BikeRental.Api/appsettings.Development.json b/BikeRental/BikeRental.Api/appsettings.Development.json index 031328052..72b97deee 100644 --- a/BikeRental/BikeRental.Api/appsettings.Development.json +++ b/BikeRental/BikeRental.Api/appsettings.Development.json @@ -5,11 +5,9 @@ "Logging": { "LogLevel": { "Default": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Warning", "Microsoft": "Error", "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None", - "BikeRental": "Information" } } diff --git a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj index 44ea37af1..e2354ae0c 100644 --- a/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj +++ b/BikeRental/BikeRental.Application.Contracts/BikeRental.Application.Contracts.csproj @@ -7,6 +7,6 @@ - + diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs index 06b0a8722..7d3f0b25f 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeCreateUpdateDto.cs @@ -5,21 +5,21 @@ namespace BikeRental.Application.Contracts.Dtos; public class BikeCreateUpdateDto { /// - /// Bike's serial number + /// Bike's serial number /// [Required] [StringLength(50, MinimumLength = 3, ErrorMessage = "Длина SerialNumber должна быть 3-50 символов.")] public required string SerialNumber { get; set; } /// - /// Bike's color + /// Bike's color /// [Required] [StringLength(20, ErrorMessage = "Макс. длина Color 20 символов.")] public required string Color { get; set; } /// - /// Bike's model + /// Bike's model /// [Required] [Range(1, int.MaxValue, ErrorMessage = "ModelId должно быть положительное число.")] diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs index da8a3f6fc..f4c0ac3c4 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeDto.cs @@ -1,28 +1,27 @@ namespace BikeRental.Application.Contracts.Dtos; /// -/// A class describing a bike for rent +/// A class describing a bike for rent /// public class BikeDto { /// - /// Bike's unique id + /// Bike's unique id /// public required int Id { get; set; } /// - /// Bike's serial number + /// Bike's serial number /// public required string SerialNumber { get; set; } /// - /// Bike's color + /// Bike's color /// public required string Color { get; set; } /// - /// Bike's model type + /// Bike's model type /// public required string ModelType { get; set; } - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs index ccc3eec1d..66d95ae6a 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelCreateUpdateDto.cs @@ -6,48 +6,48 @@ namespace BikeRental.Application.Contracts.Dtos; public class BikeModelCreateUpdateDto { /// - /// The type of bicycle: road, sport, mountain, hybrid + /// The type of bicycle: road, sport, mountain, hybrid /// [Required] public required BikeType Type { get; set; } /// - /// The size of the bicycle's wheels + /// The size of the bicycle's wheels /// [Required] [Range(12, 36, ErrorMessage = "Размер колес должен быть 12-36.")] public required int WheelSize { get; set; } /// - /// Maximum permissible cyclist weight + /// Maximum permissible cyclist weight /// [Required] [Range(30, 200, ErrorMessage = "Вес человека должен быть 30-200 кг.")] public required int MaxCyclistWeight { get; set; } /// - /// Weight of the bike model + /// Weight of the bike model /// [Required] [Range(3.0, 50.0, ErrorMessage = "Вес байка 3-50 кг.")] public required double Weight { get; set; } /// - /// The type of braking system used in this model of bike + /// The type of braking system used in this model of bike /// [Required] [StringLength(30, ErrorMessage = "Макс. длина 30 символов.")] public required string BrakeType { get; set; } /// - /// Year of manufacture of the bicycle model + /// Year of manufacture of the bicycle model /// [Required] [RegularExpression(@"^\d{4}$", ErrorMessage = "Год должен быть 4 цифры.")] public required string YearOfManufacture { get; set; } /// - /// Cost per hour rental + /// Cost per hour rental /// [Required] [Range(0.01, 1000, ErrorMessage = "Цена должна быть > 0.")] diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs index fff1780b3..967f65f31 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/BikeModelDto.cs @@ -3,47 +3,47 @@ namespace BikeRental.Application.Contracts.Dtos; /// -/// A class describing the models of bikes that can be rented +/// A class describing the models of bikes that can be rented /// public class BikeModelDto { /// - /// The unique id for bike model + /// The unique id for bike model /// public required int Id { get; set; } /// - /// The type of bicycle: road, sport, mountain, hybrid + /// The type of bicycle: road, sport, mountain, hybrid /// public required BikeType Type { get; set; } /// - /// The size of the bicycle's wheels + /// The size of the bicycle's wheels /// public required int WheelSize { get; set; } /// - /// Maximum permissible cyclist weight + /// Maximum permissible cyclist weight /// public required int MaxСyclistWeight { get; set; } /// - /// Weight of the bike model + /// Weight of the bike model /// public required double Weight { get; set; } /// - /// The type of braking system used in this model of bike + /// The type of braking system used in this model of bike /// public required string BrakeType { get; set; } /// - /// Year of manufacture of the bicycle model + /// Year of manufacture of the bicycle model /// public required string YearOfManufacture { get; set; } /// - /// Cost per hour rental + /// Cost per hour rental /// public required decimal RentPrice { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs index 75d1bfba3..1022041a3 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseCreateUpdateDto.cs @@ -5,27 +5,27 @@ namespace BikeRental.Application.Contracts.Dtos; public class LeaseCreateUpdateDto { /// - /// Person who rents a bike + /// Person who rents a bike /// [Required] [Range(1, int.MaxValue, ErrorMessage = "ID человека должно быть положительное число.")] public required int RenterId { get; set; } /// - /// Bike for rent + /// Bike for rent /// [Required] [Range(1, int.MaxValue, ErrorMessage = "ID велика должно быть положительное число.")] public required int BikeId { get; set; } /// - /// Rental start time + /// Rental start time /// [Required] public required DateTime RentalStartTime { get; set; } /// - /// Rental duration in hours + /// Rental duration in hours /// [Required] [Range(1, int.MaxValue, ErrorMessage = "Время должно быть от часа.")] diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs index a049174d0..f82aed252 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/LeaseDto.cs @@ -1,33 +1,32 @@ namespace BikeRental.Application.Contracts.Dtos; /// -/// A class describing a lease agreement +/// A class describing a lease agreement /// public class LeaseDto { /// - /// Lease ID + /// Lease ID /// public required int Id { get; set; } /// - /// Person who rents a bike + /// Person who rents a bike /// public required int RenterId { get; set; } /// - /// Bike for rent + /// Bike for rent /// public required int BikeId { get; set; } /// - /// Rental start time + /// Rental start time /// public required DateTime RentalStartTime { get; set; } /// - /// Rental duration in hours + /// Rental duration in hours /// public required int RentalDuration { get; set; } - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs index cfcab4901..b0114b4e6 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterCreateUpdateDto.cs @@ -5,14 +5,14 @@ namespace BikeRental.Application.Contracts.Dtos; public class RenterCreateUpdateDto { /// - /// Renter's full name + /// Renter's full name /// [Required] [StringLength(100, MinimumLength = 3, ErrorMessage = "Длина 3-100 символов.")] public required string FullName { get; set; } /// - /// Renter's phone number + /// Renter's phone number /// [Required] [Phone(ErrorMessage = "Неверный формат телефона.")] diff --git a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs index 6fc6ee42b..87507493a 100644 --- a/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs +++ b/BikeRental/BikeRental.Application.Contracts/Dtos/RenterDto.cs @@ -1,22 +1,22 @@ namespace BikeRental.Application.Contracts.Dtos; /// -/// A class describing a renter +/// A class describing a renter /// public class RenterDto { /// - /// Renter's id + /// Renter's id /// public required int Id { get; set; } /// - /// Renter's full name + /// Renter's full name /// public required string FullName { get; set; } /// - /// Renter's phone number + /// Renter's phone number /// public required string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/BikeRental.Application.csproj b/BikeRental/BikeRental.Application/BikeRental.Application.csproj index 7809b43c6..403374eaf 100644 --- a/BikeRental/BikeRental.Application/BikeRental.Application.csproj +++ b/BikeRental/BikeRental.Application/BikeRental.Application.csproj @@ -7,7 +7,7 @@ - + diff --git a/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs b/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs index 1e1e3f4ec..3d33f3452 100644 --- a/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs +++ b/BikeRental/BikeRental.Application/Interfaces/IBikeModelService.cs @@ -5,28 +5,27 @@ namespace BikeRental.Application.Interfaces; public interface IBikeModelService { /// - /// Returns all bike models. + /// Returns all bike models. /// public Task> GetAll(); /// - /// Returns a bike model by id. + /// Returns a bike model by id. /// public Task GetById(int id); /// - /// Creates a new bike model. + /// Creates a new bike model. /// public Task Create(BikeModelCreateUpdateDto dto); /// - /// Updates an existing bike model. + /// Updates an existing bike model. /// public Task Update(int id, BikeModelCreateUpdateDto dto); /// - /// Deletes a bike model. + /// Deletes a bike model. /// public Task Delete(int id); - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs b/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs index 10b453324..501fd2ff5 100644 --- a/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs +++ b/BikeRental/BikeRental.Application/Interfaces/IBikeService.cs @@ -3,7 +3,7 @@ namespace BikeRental.Application.Interfaces; /// -/// Service for managing bikes. +/// Service for managing bikes. /// public interface IBikeService { @@ -12,5 +12,4 @@ public interface IBikeService public Task Create(BikeCreateUpdateDto dto); public Task Update(int id, BikeCreateUpdateDto dto); public Task Delete(int id); - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs b/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs index e2e0a6ed6..2309dff05 100644 --- a/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs +++ b/BikeRental/BikeRental.Application/Interfaces/ILeaseService.cs @@ -3,7 +3,7 @@ namespace BikeRental.Application.Interfaces; /// -/// Service for managing bike leases. +/// Service for managing bike leases. /// public interface ILeaseService { diff --git a/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs b/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs index 49d4c19d2..b3941b982 100644 --- a/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs +++ b/BikeRental/BikeRental.Application/Interfaces/IRenterService.cs @@ -3,7 +3,7 @@ namespace BikeRental.Application.Interfaces; /// -/// Service for managing renters. +/// Service for managing renters. /// public interface IRenterService { diff --git a/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs index db162c0ad..a26b27700 100644 --- a/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/BikeMappings.cs @@ -15,7 +15,7 @@ public static BikeDto ToDto(this Bike entity) ModelType = entity.Model.BrakeType }; } - + public static Bike ToEntity(this BikeCreateUpdateDto dto, BikeModel model) { return new Bike @@ -23,7 +23,7 @@ public static Bike ToEntity(this BikeCreateUpdateDto dto, BikeModel model) SerialNumber = dto.SerialNumber, Color = dto.Color, ModelId = dto.ModelId, - Model = model, + Model = model }; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs b/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs index 25dc3f23b..4ea1bf9c3 100644 --- a/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/BikeModelMappings.cs @@ -19,7 +19,7 @@ public static BikeModelDto ToDto(this BikeModel entity) RentPrice = entity.RentPrice }; } - + public static BikeModel ToEntity(this BikeModelCreateUpdateDto dto) { return new BikeModel diff --git a/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs index d4632177d..95e4da1d9 100644 --- a/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/LeaseMappings.cs @@ -16,13 +16,13 @@ public static LeaseDto ToDto(this Lease entity) RentalDuration = entity.RentalDuration }; } - + public static Lease ToEntity(this LeaseCreateUpdateDto dto, Bike bike, Renter renter) { return new Lease { - BikeId = bike.Id, - RenterId = renter.Id, + BikeId = bike.Id, + RenterId = renter.Id, Bike = bike, Renter = renter, RentalStartTime = dto.RentalStartTime, diff --git a/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs b/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs index 310577440..acfe644e3 100644 --- a/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs +++ b/BikeRental/BikeRental.Application/Mappings/RenterMappings.cs @@ -14,7 +14,7 @@ public static RenterDto ToDto(this Renter entity) PhoneNumber = entity.PhoneNumber }; } - + public static Renter ToEntity(this RenterCreateUpdateDto dto) { return new Renter diff --git a/BikeRental/BikeRental.Application/Services/BikeModelService.cs b/BikeRental/BikeRental.Application/Services/BikeModelService.cs index b740af054..fc3dfdeec 100644 --- a/BikeRental/BikeRental.Application/Services/BikeModelService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeModelService.cs @@ -2,18 +2,18 @@ using BikeRental.Application.Interfaces; using BikeRental.Application.Mappings; using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; namespace BikeRental.Application.Services; /// -/// Application-сервис для работы с моделями велосипедов. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// Application-сервис для работы с моделями велосипедов. Инкапсулирует бизнес-логику и доступ к репозиторию. /// public sealed class BikeModelService(IBikeModelRepository bikeModelRepository) : IBikeModelService { public async Task> GetAll() { - return (await bikeModelRepository.GetAll()). - Select(bm => bm.ToDto()); + return (await bikeModelRepository.GetAll()).Select(bm => bm.ToDto()); } public async Task GetById(int id) @@ -26,36 +26,37 @@ public async Task Create(BikeModelCreateUpdateDto dto) var id = await bikeModelRepository.Add(dto.ToEntity()); if (id > 0) { - var createdEntity = await bikeModelRepository.GetById(id); + BikeModel? createdEntity = await bikeModelRepository.GetById(id); if (createdEntity != null) { return createdEntity.ToDto(); } } + throw new InvalidOperationException("Failed to create entity."); } public async Task Update(int id, BikeModelCreateUpdateDto dto) { - _ = await bikeModelRepository.GetById(id) + _ = await bikeModelRepository.GetById(id) ?? throw new KeyNotFoundException($"Entity with id {id} not found."); - - var entityToUpdate = dto.ToEntity(); + + BikeModel entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; await bikeModelRepository.Update(entityToUpdate); - var updatedEntity = await bikeModelRepository.GetById(id); + BikeModel? updatedEntity = await bikeModelRepository.GetById(id); return updatedEntity!.ToDto(); } public async Task Delete(int id) { - var entity = await bikeModelRepository.GetById(id); + BikeModel? entity = await bikeModelRepository.GetById(id); if (entity == null) { return false; } + await bikeModelRepository.Delete(entity); return true; } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Services/BikeService.cs b/BikeRental/BikeRental.Application/Services/BikeService.cs index e517a7c8c..b67a3f384 100644 --- a/BikeRental/BikeRental.Application/Services/BikeService.cs +++ b/BikeRental/BikeRental.Application/Services/BikeService.cs @@ -2,19 +2,19 @@ using BikeRental.Application.Interfaces; using BikeRental.Application.Mappings; using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; namespace BikeRental.Application.Services; /// -/// Application-сервис для работы с велосипедами. Инкапсулирует бизнес-логику и доступ к репозиторию. -/// На текущем этапе является тонкой обёрткой над IBikeRepository. +/// Application-сервис для работы с велосипедами. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над IBikeRepository. /// public sealed class BikeService(IBikeRepository bikeRepository, IBikeModelRepository modelRepository) : IBikeService { public async Task> GetAll() { - return (await bikeRepository.GetAll()). - Select(b => b.ToDto()); + return (await bikeRepository.GetAll()).Select(b => b.ToDto()); } public async Task GetById(int id) @@ -24,45 +24,47 @@ public async Task> GetAll() public async Task Create(BikeCreateUpdateDto dto) { - var model = await modelRepository.GetById(dto.ModelId) - ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); - + BikeModel model = await modelRepository.GetById(dto.ModelId) + ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); + var id = await bikeRepository.Add(dto.ToEntity(model)); if (id > 0) { - var createdEntity = await bikeRepository.GetById(id); + Bike? createdEntity = await bikeRepository.GetById(id); if (createdEntity != null) { return createdEntity.ToDto(); } } + throw new InvalidOperationException("Failed to create entity."); } public async Task Update(int id, BikeCreateUpdateDto dto) { - _ = await bikeRepository.GetById(id) + _ = await bikeRepository.GetById(id) ?? throw new KeyNotFoundException($"Bike with id {id} not found."); - - var model = await modelRepository.GetById(dto.ModelId) - ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); - - var entityToUpdate = dto.ToEntity(model); + + BikeModel model = await modelRepository.GetById(dto.ModelId) + ?? throw new ArgumentException($"Model with id {dto.ModelId} not found."); + + Bike entityToUpdate = dto.ToEntity(model); entityToUpdate.Id = id; await bikeRepository.Update(entityToUpdate); - var updatedEntity = await bikeRepository.GetById(id); + Bike? updatedEntity = await bikeRepository.GetById(id); return updatedEntity!.ToDto(); } public async Task Delete(int id) { - var entity = await bikeRepository.GetById(id); + Bike? entity = await bikeRepository.GetById(id); if (entity == null) { return false; } + await bikeRepository.Delete(entity); return true; } -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Services/LeaseService.cs b/BikeRental/BikeRental.Application/Services/LeaseService.cs index e95b9d371..fbe567c12 100644 --- a/BikeRental/BikeRental.Application/Services/LeaseService.cs +++ b/BikeRental/BikeRental.Application/Services/LeaseService.cs @@ -2,13 +2,14 @@ using BikeRental.Application.Interfaces; using BikeRental.Application.Mappings; using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; namespace BikeRental.Application.Services; /// -/// Application-сервис для работы с договорами аренды велосипедов. -/// Инкапсулирует бизнес-логику и доступ к репозиторию. -/// На текущем этапе является тонкой обёрткой над ILeaseRepository. +/// Application-сервис для работы с договорами аренды велосипедов. +/// Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над ILeaseRepository. /// public sealed class LeaseService( ILeaseRepository leaseRepository, @@ -17,8 +18,7 @@ public sealed class LeaseService( { public async Task> GetAll() { - return (await leaseRepository.GetAll()). - Select(l => l.ToDto()); + return (await leaseRepository.GetAll()).Select(l => l.ToDto()); } public async Task GetById(int id) @@ -28,21 +28,22 @@ public async Task> GetAll() public async Task Create(LeaseCreateUpdateDto dto) { - var bike = await bikeRepository.GetById(dto.BikeId) - ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); - - var renter = await renterRepository.GetById(dto.RenterId) - ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); - + Bike bike = await bikeRepository.GetById(dto.BikeId) + ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); + + Renter renter = await renterRepository.GetById(dto.RenterId) + ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); + var id = await leaseRepository.Add(dto.ToEntity(bike, renter)); if (id > 0) { - var createdEntity = await leaseRepository.GetById(id); + Lease? createdEntity = await leaseRepository.GetById(id); if (createdEntity != null) { return createdEntity.ToDto(); } } + throw new InvalidOperationException("Failed to create entity."); } @@ -50,29 +51,29 @@ public async Task Update(int id, LeaseCreateUpdateDto dto) { _ = await leaseRepository.GetById(id) ?? throw new KeyNotFoundException($"Lease with id {id} not found."); - - var bike = await bikeRepository.GetById(dto.BikeId) - ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); - - var renter = await renterRepository.GetById(dto.RenterId) - ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); - - var entityToUpdate = dto.ToEntity(bike, renter); + + Bike bike = await bikeRepository.GetById(dto.BikeId) + ?? throw new ArgumentException($"Bike with id {dto.BikeId} not found."); + + Renter renter = await renterRepository.GetById(dto.RenterId) + ?? throw new ArgumentException($"Renter with id {dto.RenterId} not found."); + + Lease entityToUpdate = dto.ToEntity(bike, renter); entityToUpdate.Id = id; await leaseRepository.Update(entityToUpdate); - var updatedEntity = await leaseRepository.GetById(id); + Lease? updatedEntity = await leaseRepository.GetById(id); return updatedEntity!.ToDto(); } public async Task Delete(int id) { - var entity = await leaseRepository.GetById(id); + Lease? entity = await leaseRepository.GetById(id); if (entity == null) { return false; } + await leaseRepository.Delete(entity); return true; } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Application/Services/RenterService.cs b/BikeRental/BikeRental.Application/Services/RenterService.cs index 29f0386f4..8912db5bf 100644 --- a/BikeRental/BikeRental.Application/Services/RenterService.cs +++ b/BikeRental/BikeRental.Application/Services/RenterService.cs @@ -2,19 +2,19 @@ using BikeRental.Application.Interfaces; using BikeRental.Application.Mappings; using BikeRental.Domain.Interfaces; +using BikeRental.Domain.Models; namespace BikeRental.Application.Services; /// -/// Application-сервис для работы с арендаторами. Инкапсулирует бизнес-логику и доступ к репозиторию. -/// На текущем этапе является тонкой обёрткой над IRenterRepository. +/// Application-сервис для работы с арендаторами. Инкапсулирует бизнес-логику и доступ к репозиторию. +/// На текущем этапе является тонкой обёрткой над IRenterRepository. /// public sealed class RenterService(IRenterRepository renterRepository) : IRenterService { public async Task> GetAll() { - return (await renterRepository.GetAll()). - Select(r => r.ToDto()); + return (await renterRepository.GetAll()).Select(r => r.ToDto()); } public async Task GetById(int id) @@ -27,36 +27,37 @@ public async Task Create(RenterCreateUpdateDto dto) var id = await renterRepository.Add(dto.ToEntity()); if (id > 0) { - var createdEntity = await renterRepository.GetById(id); + Renter? createdEntity = await renterRepository.GetById(id); if (createdEntity != null) { return createdEntity.ToDto(); } } + throw new InvalidOperationException("Failed to create entity."); } public async Task Update(int id, RenterCreateUpdateDto dto) { - _ = await renterRepository.GetById(id) + _ = await renterRepository.GetById(id) ?? throw new KeyNotFoundException($"Entity with id {id} not found."); - - var entityToUpdate = dto.ToEntity(); + + Renter entityToUpdate = dto.ToEntity(); entityToUpdate.Id = id; await renterRepository.Update(entityToUpdate); - var updatedEntity = await renterRepository.GetById(id); + Renter? updatedEntity = await renterRepository.GetById(id); return updatedEntity!.ToDto(); } public async Task Delete(int id) { - var entity = await renterRepository.GetById(id); + Renter? entity = await renterRepository.GetById(id); if (entity == null) { return false; } + await renterRepository.Delete(entity); return true; } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Enum/BikeType.cs b/BikeRental/BikeRental.Domain/Enum/BikeType.cs index e5eaceec8..593baac91 100644 --- a/BikeRental/BikeRental.Domain/Enum/BikeType.cs +++ b/BikeRental/BikeRental.Domain/Enum/BikeType.cs @@ -1,27 +1,27 @@ namespace BikeRental.Domain.Enum; /// -/// A class describing the types of bikes that can be rented +/// A class describing the types of bikes that can be rented /// public enum BikeType { /// - /// Road bike + /// Road bike /// Road, - + /// - /// Sports bike + /// Sports bike /// Sport, /// - /// Mountain bike + /// Mountain bike /// Mountain, - + /// - /// Hybrid bike - a bicycle that combines the qualities of a mountain bike and a road bike + /// Hybrid bike - a bicycle that combines the qualities of a mountain bike and a road bike /// Hybrid } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs index 67e5b7d23..e5ee0c9d7 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeModelRepository.cs @@ -3,6 +3,6 @@ namespace BikeRental.Domain.Interfaces; /// -/// Интерфейс репозитория описывает контракт для работы с моделями велосипедов +/// Интерфейс репозитория описывает контракт для работы с моделями велосипедов /// public interface IBikeModelRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs index bf74d8bf9..97cdc72b8 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IBikeRepository.cs @@ -3,6 +3,6 @@ namespace BikeRental.Domain.Interfaces; /// -/// Интерфейс репозитория описывает контракт для работы с велосипедами +/// Интерфейс репозитория описывает контракт для работы с велосипедами /// public interface IBikeRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs index 6eabbff71..cdd3a5155 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/ILeaseRepository.cs @@ -3,6 +3,6 @@ namespace BikeRental.Domain.Interfaces; /// -/// Интерфейс репозитория описывает контракт для работы с договорами на аренду велосипедов +/// Интерфейс репозитория описывает контракт для работы с договорами на аренду велосипедов /// public interface ILeaseRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs index 828acb64c..d0720e85b 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IRenterRepository.cs @@ -3,6 +3,6 @@ namespace BikeRental.Domain.Interfaces; /// -/// Интерфейс репозитория описывает контракт для работы с арендаторами +/// Интерфейс репозитория описывает контракт для работы с арендаторами /// public interface IRenterRepository : IRepository; \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs b/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs index b9fa0bf4e..6423f5af1 100644 --- a/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs +++ b/BikeRental/BikeRental.Domain/Interfaces/IRepository.cs @@ -1,34 +1,33 @@ namespace BikeRental.Domain.Interfaces; + /// -/// Generic repository interface that defines basic CRUD operations. +/// Generic repository interface that defines basic CRUD operations. /// public interface IRepository where TEntity : class { /// - /// Returns all entities. + /// Returns all entities. /// public Task> GetAll(); /// - /// Returns entity by id. + /// Returns entity by id. /// public Task GetById(int id); /// - /// Adds a new entity and returns its generated id. + /// Adds a new entity and returns its generated id. /// public Task Add(TEntity entity); /// - /// Updates existing entity. + /// Updates existing entity. /// public Task Update(TEntity entity); /// - /// Deletes existing entity. + /// Deletes existing entity. /// public Task Delete(TEntity entity); -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Bike.cs b/BikeRental/BikeRental.Domain/Models/Bike.cs index 77b9edc66..6df10dd3c 100644 --- a/BikeRental/BikeRental.Domain/Models/Bike.cs +++ b/BikeRental/BikeRental.Domain/Models/Bike.cs @@ -3,30 +3,29 @@ namespace BikeRental.Domain.Models; /// -/// A class describing a bike for rent +/// A class describing a bike for rent /// public class Bike { /// - /// Bike's unique id + /// Bike's unique id /// public int Id { get; set; } - - [ForeignKey(nameof(Model))] - public required int ModelId { get; set; } + + [ForeignKey(nameof(Model))] public required int ModelId { get; set; } /// - /// Bike's serial number + /// Bike's serial number /// public required string SerialNumber { get; set; } /// - /// Bike's color + /// Bike's color /// public required string Color { get; set; } /// - /// Bike's model + /// Bike's model /// public required BikeModel Model { get; init; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/BikeModel.cs b/BikeRental/BikeRental.Domain/Models/BikeModel.cs index 8f17a6edb..209d2e311 100644 --- a/BikeRental/BikeRental.Domain/Models/BikeModel.cs +++ b/BikeRental/BikeRental.Domain/Models/BikeModel.cs @@ -1,49 +1,49 @@ using BikeRental.Domain.Enum; namespace BikeRental.Domain.Models; + /// -/// A class describing the models of bikes that can be rented +/// A class describing the models of bikes that can be rented /// public class BikeModel { /// - /// The unique id for bike model + /// The unique id for bike model /// public int Id { get; set; } /// - /// The type of bicycle: road, sport, mountain, hybrid + /// The type of bicycle: road, sport, mountain, hybrid /// public required BikeType Type { get; set; } /// - /// The size of the bicycle's wheels + /// The size of the bicycle's wheels /// public required int WheelSize { get; set; } /// - /// Maximum permissible cyclist weight + /// Maximum permissible cyclist weight /// public required int MaxCyclistWeight { get; set; } /// - /// Weight of the bike model + /// Weight of the bike model /// public required double Weight { get; set; } /// - /// The type of braking system used in this model of bike + /// The type of braking system used in this model of bike /// public required string BrakeType { get; set; } /// - /// Year of manufacture of the bicycle model + /// Year of manufacture of the bicycle model /// public required string YearOfManufacture { get; set; } /// - /// Cost per hour rental + /// Cost per hour rental /// public required decimal RentPrice { get; set; } - } \ No newline at end of file diff --git a/BikeRental/BikeRental.Domain/Models/Lease.cs b/BikeRental/BikeRental.Domain/Models/Lease.cs index c35f6155d..dea90ee75 100644 --- a/BikeRental/BikeRental.Domain/Models/Lease.cs +++ b/BikeRental/BikeRental.Domain/Models/Lease.cs @@ -3,38 +3,36 @@ namespace BikeRental.Domain.Models; /// -/// A class describing a lease agreement +/// A class describing a lease agreement /// public class Lease { /// - /// Lease ID + /// Lease ID /// public int Id { get; set; } - - [ForeignKey(nameof(Bike))] - public int BikeId { get; set; } - - [ForeignKey(nameof(Renter))] - public int RenterId { get; set; } + + [ForeignKey(nameof(Bike))] public int BikeId { get; set; } + + [ForeignKey(nameof(Renter))] public int RenterId { get; set; } /// - /// Rental start time + /// Rental start time /// public required DateTime RentalStartTime { get; set; } /// - /// Rental duration in hours + /// Rental duration in hours /// public required int RentalDuration { get; set; } - + /// - /// Person who rents a bike + /// Person who rents a bike /// - public required Renter Renter { get; set; } + public required Renter Renter { get; set; } /// - /// Bike for rent + /// Bike for rent /// public required Bike Bike { get; set; } // сделала required тогда их айди автоматически должны установиться EF core diff --git a/BikeRental/BikeRental.Domain/Models/Renter.cs b/BikeRental/BikeRental.Domain/Models/Renter.cs index cb8d9612f..e3acb0ad5 100644 --- a/BikeRental/BikeRental.Domain/Models/Renter.cs +++ b/BikeRental/BikeRental.Domain/Models/Renter.cs @@ -1,22 +1,22 @@ namespace BikeRental.Domain.Models; /// -/// A class describing a renter +/// A class describing a renter /// public class Renter { /// - /// Renter's id + /// Renter's id /// public int Id { get; set; } /// - /// Renter's full name + /// Renter's full name /// public required string FullName { get; set; } /// - /// Renter's phone number + /// Renter's phone number /// public required string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj index 5b8795135..1a06d9934 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRental.Generator.Nats.Host.csproj @@ -8,13 +8,13 @@ - + - + diff --git a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs index 9842f3032..9e7363ac2 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/BikeRentalNatsProducer.cs @@ -13,16 +13,23 @@ public sealed class BikeRentalNatsProducer( INatsConnection connection, ILogger logger) { - private readonly string _streamName = GetRequired(settings.Value.StreamName, "StreamName"); - private readonly string _subjectName = GetRequired(settings.Value.SubjectName, "SubjectName"); - // для настройки повторных попыток - ретраи private readonly int _connectRetryAttempts = Math.Max(1, settings.Value.ConnectRetryAttempts); - private readonly TimeSpan _connectRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.ConnectRetryDelayMs)); + + private readonly TimeSpan _connectRetryDelay = + TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.ConnectRetryDelayMs)); + private readonly int _publishRetryAttempts = Math.Max(1, settings.Value.PublishRetryAttempts); - private readonly TimeSpan _publishRetryDelay = TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.PublishRetryDelayMs)); - private readonly double _retryBackoffFactor = settings.Value.RetryBackoffFactor <= 0 ? 2 : settings.Value.RetryBackoffFactor; - + + private readonly TimeSpan _publishRetryDelay = + TimeSpan.FromMilliseconds(Math.Max(0, settings.Value.PublishRetryDelayMs)); + + private readonly double _retryBackoffFactor = + settings.Value.RetryBackoffFactor <= 0 ? 2 : settings.Value.RetryBackoffFactor; + + private readonly string _streamName = GetRequired(settings.Value.StreamName, "StreamName"); + private readonly string _subjectName = GetRequired(settings.Value.SubjectName, "SubjectName"); + public async Task SendAsync(IList batch, CancellationToken cancellationToken) { @@ -38,23 +45,23 @@ public async Task SendAsync(IList batch, CancellationToken // вызов с повторными попытками await ExecuteWithRetryAsync( "connect to NATS", - _connectRetryAttempts,// сколько раз пытаться - _connectRetryDelay,// начальная задержка - cancellationToken,// токен отмены - async () => await connection.ConnectAsync()); + _connectRetryAttempts, // сколько раз пытаться + _connectRetryDelay, // начальная задержка + async () => await connection.ConnectAsync(), + cancellationToken); + + INatsJSContext context = connection.CreateJetStreamContext(); + + var streamConfig = new StreamConfig(_streamName, [_subjectName]); - var context = connection.CreateJetStreamContext(); - var streamConfig = new StreamConfig(_streamName, new List { _subjectName }); - - // await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken); await ExecuteWithRetryAsync( "create/update stream", _publishRetryAttempts, _publishRetryDelay, - cancellationToken, - async () => await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken)); + async () => await context.CreateOrUpdateStreamAsync(streamConfig, cancellationToken), + cancellationToken); var payload = JsonSerializer.SerializeToUtf8Bytes(batch); @@ -63,21 +70,21 @@ await ExecuteWithRetryAsync( "publish batch", _publishRetryAttempts, _publishRetryDelay, - cancellationToken, async () => await context.PublishAsync( - subject: _subjectName, - data: payload, - cancellationToken: cancellationToken)); + _subjectName, + payload, + cancellationToken: cancellationToken), + cancellationToken); logger.LogInformation( - "Sent a batch of {count} leases to {subject} of {stream}", + "Sent a batch of {count} leases to {subject} of {stream}", batch.Count, _subjectName, _streamName); } catch (Exception ex) { logger.LogError( - ex, - "Exception occurred during sending a batch of {count} leases to {stream}/{subject}", + ex, + "Exception occurred during sending a batch of {count} leases to {stream}/{subject}", batch.Count, _streamName, _subjectName); } } @@ -85,12 +92,12 @@ await ExecuteWithRetryAsync( // механизм повторных попыток private async Task ExecuteWithRetryAsync( string operation, - int attempts,// Максимальное количество попыток - TimeSpan baseDelay,// Начальная задержка - CancellationToken cancellationToken,// Токен отмены - Func action)// Операция для выполнения + int attempts, // Максимальное количество попыток + TimeSpan baseDelay, // Начальная задержка + Func action, // Операция для выполнения + CancellationToken cancellationToken) // Токен отмены { - var delay = baseDelay; + TimeSpan delay = baseDelay; for (var attempt = 1; attempt <= attempts; attempt++) { try @@ -111,7 +118,7 @@ private async Task ExecuteWithRetryAsync( attempts, delay); await Task.Delay(delay, cancellationToken); - + // увеличить задержку delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * _retryBackoffFactor); } @@ -134,6 +141,7 @@ private static string GetRequired(string? value, string key) //мини пров { throw new KeyNotFoundException($"{key} is not configured in Nats section."); } + return value; } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs index ddd08b170..b9b66501b 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseBatchGenerator.cs @@ -1,15 +1,15 @@ -using Bogus; using BikeRental.Application.Contracts.Dtos; +using Bogus; namespace BikeRental.Generator.Nats.Host.Generator; public sealed class LeaseBatchGenerator { - public IList GenerateBatch(LeaseGenerationOptions settings) + public static IList GenerateBatch(LeaseGenerationOptions settings) { Validate(settings); - var faker = CreateFaker(settings); + Faker faker = CreateFaker(settings); return faker.Generate(settings.BatchSize); } @@ -17,9 +17,9 @@ private static Faker CreateFaker( LeaseGenerationOptions settings) { return new Faker() - .RuleFor(x => x.RenterId, f => + .RuleFor(x => x.RenterId, f => f.Random.Int(settings.RenterIdMin, settings.RenterIdMax)) - .RuleFor(x => x.BikeId, f => + .RuleFor(x => x.BikeId, f => f.Random.Int(settings.BikeIdMin, settings.BikeIdMax)) .RuleFor(x => x.RentalDuration, f => f.Random.Int( @@ -46,23 +46,33 @@ private static DateTime GeneratePastStartTime( private static void Validate(LeaseGenerationOptions settings) { if (settings.BatchSize <= 0) + { throw new InvalidOperationException("BatchSize must be > 0."); + } if (settings.BikeIdMin > settings.BikeIdMax) + { throw new InvalidOperationException( "BikeIdMin must be <= BikeIdMax."); + } if (settings.RenterIdMin > settings.RenterIdMax) + { throw new InvalidOperationException( "RenterIdMin must be <= RenterIdMax."); + } if (settings.RentalDurationMinHours > settings.RentalDurationMaxHours) + { throw new InvalidOperationException( "RentalDurationMinHours must be <= RentalDurationMaxHours."); + } if (settings.RentalStartDaysBackMax < 0) + { throw new InvalidOperationException( "RentalStartDaysBackMax must be >= 0."); + } } } \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs index 0010a18d4..cadccb3a6 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Generator/LeaseGenerationOptions.cs @@ -11,4 +11,4 @@ public sealed class LeaseGenerationOptions public int RentalDurationMinHours { get; init; } = 1; public int RentalDurationMaxHours { get; init; } = 72; public int RentalStartDaysBackMax { get; init; } = 10; -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs index 3c66e5c6d..f24215af2 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/LeaseBatchWorker.cs @@ -1,16 +1,17 @@ -using Microsoft.Extensions.Options; +using BikeRental.Application.Contracts.Dtos; using BikeRental.Generator.Nats.Host.Generator; +using Microsoft.Extensions.Options; namespace BikeRental.Generator.Nats.Host; + public sealed class LeaseBatchWorker( - LeaseBatchGenerator generator, BikeRentalNatsProducer producer, IOptions options, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - var settings = options.Value; + LeaseGenerationOptions settings = options.Value; if (settings.BatchSize <= 0) { logger.LogError("LeaseGeneration.BatchSize must be greater than 0."); @@ -32,8 +33,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async Task SendBatchAsync(LeaseGenerationOptions settings, CancellationToken stoppingToken) { - var batch = generator.GenerateBatch(settings); + IList batch = LeaseBatchGenerator.GenerateBatch(settings); await producer.SendAsync(batch, stoppingToken); } -} - +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs index 14c1ee201..303b92fa8 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/NatsSettings.cs @@ -1,5 +1,8 @@ namespace BikeRental.Generator.Nats.Host; -// для типизированной конфигурации + +/// +/// Класс для типизированной конфигурации +/// public sealed class NatsSettings { public string Url { get; init; } = "nats://localhost:4222"; @@ -10,4 +13,4 @@ public sealed class NatsSettings public int PublishRetryAttempts { get; init; } = 3; public int PublishRetryDelayMs { get; init; } = 1000; public double RetryBackoffFactor { get; init; } = 2; -} +} \ No newline at end of file diff --git a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs index 62a1050ad..e01c9a705 100644 --- a/BikeRental/BikeRental.Generator.Nats.Host/Program.cs +++ b/BikeRental/BikeRental.Generator.Nats.Host/Program.cs @@ -1,13 +1,12 @@ using BikeRental.Generator.Nats.Host; +using BikeRental.Generator.Nats.Host.Generator; using Microsoft.Extensions.Options; using NATS.Client.Core; -using BikeRental.Generator.Nats.Host.Generator; - -var builder = Host.CreateApplicationBuilder(args); +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -var natsSettingsSection = builder.Configuration.GetSection("NatsSettings"); +IConfigurationSection natsSettingsSection = builder.Configuration.GetSection("NatsSettings"); if (natsSettingsSection.Exists()) { @@ -20,7 +19,7 @@ builder.Services.AddSingleton(sp => { - var settings = sp.GetRequiredService>().Value; + NatsSettings settings = sp.GetRequiredService>().Value; var connectRetryDelayMs = Math.Max(0, settings.ConnectRetryDelayMs); var reconnectWaitMin = TimeSpan.FromMilliseconds(connectRetryDelayMs); var reconnectWaitMax = TimeSpan.FromMilliseconds( @@ -42,6 +41,5 @@ builder.Services.AddSingleton(); builder.Services.AddHostedService(); -var host = builder.Build(); -await host.RunAsync(); -//host.Run(); +IHost host = builder.Build(); +await host.RunAsync(); \ No newline at end of file diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs index ef6c74326..e38e019a6 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeConfiguration.cs @@ -5,12 +5,12 @@ namespace BikeRental.Infrastructure.Database.Configurations; /// -/// Конфигурация сущности "Bike" +/// Конфигурация сущности "Bike" /// public class BikeConfiguration : IEntityTypeConfiguration { /// - /// Настройка сущности "Bike" + /// Настройка сущности "Bike" /// /// public void Configure(EntityTypeBuilder builder) diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs index 62c2bedb6..51405335e 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/BikeModelConfiguration.cs @@ -5,12 +5,12 @@ namespace BikeRental.Infrastructure.Database.Configurations; /// -/// Конфигурация сущности "BikeModel" +/// Конфигурация сущности "BikeModel" /// public class BikeModelConfiguration : IEntityTypeConfiguration { /// - /// Настройка сущности "BikeModel" + /// Настройка сущности "BikeModel" /// /// public void Configure(EntityTypeBuilder builder) diff --git a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs index 67089664d..985b99829 100644 --- a/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs +++ b/BikeRental/BikeRental.Infrastructure/Database/Configurations/RenterConfiguration.cs @@ -5,12 +5,12 @@ namespace BikeRental.Infrastructure.Database.Configurations; /// -/// Конфигурация сущности "Renter" +/// Конфигурация сущности "Renter" /// public class RenterConfiguration : IEntityTypeConfiguration { /// - /// Настройка сущности "Renter" + /// Настройка сущности "Renter" /// /// public void Configure(EntityTypeBuilder builder) diff --git a/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs index 77e75b8fa..945f27636 100644 --- a/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs +++ b/BikeRental/BikeRental.Infrastructure/Repositories/BikeModelRepository.cs @@ -6,7 +6,7 @@ namespace BikeRental.Infrastructure.Repositories; /// -/// Репозиторий для работы с моделями велосипедов. +/// Репозиторий для работы с моделями велосипедов. /// public sealed class BikeModelRepository(ApplicationDbContext dbContext) : IBikeModelRepository { diff --git a/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs index 312cb067d..47fcb7720 100644 --- a/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs +++ b/BikeRental/BikeRental.Infrastructure/Services/ISeedDataService.cs @@ -1,12 +1,12 @@ namespace BikeRental.Infrastructure.Services; /// -/// Интерфейс описывает сервис инициализации данных +/// Интерфейс описывает сервис инициализации данных /// public interface ISeedDataService { /// - /// Выполнить инициализацию данных + /// Выполнить инициализацию данных /// /// public Task SeedDataAsync(); diff --git a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs index 8bc228f41..85dfeac43 100644 --- a/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs +++ b/BikeRental/BikeRental.Infrastructure/Services/Impl/SeedDataService.cs @@ -7,13 +7,13 @@ namespace BikeRental.Infrastructure.Services.Impl; /// -/// Сервис инициализации данных +/// Сервис инициализации данных /// /// public class SeedDataService(ApplicationDbContext dbContext) : ISeedDataService { /// - /// Выполнить инициализацию данных + /// Выполнить инициализацию данных /// public async Task SeedDataAsync() { @@ -64,7 +64,7 @@ public async Task SeedDataAsync() // Создать велосипеды, если они отсутствуют if (!await dbContext.Bikes.AnyAsync()) { - var bikeModels = await dbContext.BikeModels.ToListAsync(); + List bikeModels = await dbContext.BikeModels.ToListAsync(); var bikes = new List(); for (var i = 0; i < 30; i++) { @@ -86,8 +86,8 @@ public async Task SeedDataAsync() // некоторых велосипедов if (!await dbContext.Leases.AnyAsync()) { - var renters = await dbContext.Renters.ToListAsync(); - var bikes = await dbContext.Bikes.ToListAsync(); + List renters = await dbContext.Renters.ToListAsync(); + List bikes = await dbContext.Bikes.ToListAsync(); var leases = new List(); for (var i = 0; i < 15; i++) diff --git a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj index 673dee290..abce81240 100644 --- a/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj +++ b/BikeRental/BikeRental.Tests/BikeRental.Tests.csproj @@ -7,16 +7,16 @@ - + - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/BikeRental/BikeRental.Tests/BikeRentalTests.cs b/BikeRental/BikeRental.Tests/BikeRentalTests.cs index 32a3d6ef6..e0da93d90 100644 --- a/BikeRental/BikeRental.Tests/BikeRentalTests.cs +++ b/BikeRental/BikeRental.Tests/BikeRentalTests.cs @@ -3,18 +3,17 @@ namespace BikeRental.Tests; /// -/// Class for unit-tests +/// Class for unit-tests /// public class BikeRentalTests(RentalFixture fixture) : IClassFixture { - /// - /// Displays information about all sports bikes + /// Displays information about all sports bikes /// [Fact] public void InfoAboutSportBikes() { - var expected = new List {3, 6, 9}; + var expected = new List { 3, 6, 9 }; var actual = fixture.Bikes .Where(b => b.Model.Type == BikeType.Sport) @@ -23,14 +22,14 @@ public void InfoAboutSportBikes() Assert.Equal(expected, actual); } - + /// - /// Displays the top 5 bike models ranked by rental revenue + /// Displays the top 5 bike models ranked by rental revenue /// [Fact] public void TopFiveModelsIncome() { - var expected = new List {5, 8, 2, 9, 3}; /// 9,7,3 have same result (= 60) + var expected = new List { 5, 8, 2, 9, 3 }; /// 9,7,3 have same result (= 60) var actual = fixture.Lease .GroupBy(lease => lease.Bike.Model.Id) @@ -45,14 +44,14 @@ public void TopFiveModelsIncome() .ToList(); Assert.Equal(expected, actual); } - + /// - /// Displays the top 5 bike models ranked by rental duration + /// Displays the top 5 bike models ranked by rental duration /// {5, 8, 2, 7, 3}; + var expected = new List { 5, 8, 2, 7, 3 }; var actual = fixture.Lease .GroupBy(lease => lease.Bike.Model.Id) @@ -70,7 +69,7 @@ public void TopFiveModelsDuration() } /// - /// Displays information about the minimum, maximum, and average rental time + /// Displays information about the minimum, maximum, and average rental time /// [Fact] public void MinMaxAvgRental() @@ -78,24 +77,23 @@ public void MinMaxAvgRental() var expectedMinimum = 1; var expectedMaximum = 8; var expectedAverage = 4.4; - + var durations = fixture.Lease.Select(rent => rent.RentalDuration).ToList(); - + Assert.Equal(expectedMinimum, durations.Min()); Assert.Equal(expectedMaximum, durations.Max()); Assert.Equal(expectedAverage, durations.Average()); } - - + + /// - /// Displays the total rental time for each bike type + /// Displays the total rental time for each bike type /// [Theory] [InlineData(BikeType.Road, 12)] [InlineData(BikeType.Sport, 9)] [InlineData(BikeType.Mountain, 8)] [InlineData(BikeType.Hybrid, 15)] - public void TotalRentalTimeByType(BikeType type, int expected) { var actual = fixture.Lease @@ -104,14 +102,14 @@ public void TotalRentalTimeByType(BikeType type, int expected) Assert.Equal(expected, actual); } - + /// - /// Displays information about customers who have rented bikes the most times + /// Displays information about customers who have rented bikes the most times /// [Fact] public void TopThreeRenters() { - var expected = new List {6, 7, 1}; + var expected = new List { 6, 7, 1 }; var actual = fixture.Lease .GroupBy(lease => lease.Renter.Id) diff --git a/BikeRental/BikeRental.Tests/RentalFixture.cs b/BikeRental/BikeRental.Tests/RentalFixture.cs index 9b5aef2a0..617bc80db 100644 --- a/BikeRental/BikeRental.Tests/RentalFixture.cs +++ b/BikeRental/BikeRental.Tests/RentalFixture.cs @@ -6,249 +6,301 @@ namespace BikeRental.Tests; public class RentalFixture { /// - /// A list of all bike models + /// A class for creating the data for testing + /// + public RentalFixture() + { + Models = GetBikeModels(); + Renters = GetRenters(); + Bikes = GetBikes(Models); + Lease = GetLeases(Bikes, Renters); + } + + /// + /// A list of all bike models /// public List Models { get; } /// - /// /// A list of all bikes for rent + /// /// A list of all bikes for rent /// public List Bikes { get; } - + /// - /// A list of all registered renters + /// A list of all registered renters /// public List Renters { get; } /// - /// A list of all leases + /// A list of all leases /// public List Lease { get; } - - /// - /// A class for creating the data for testing - /// - public RentalFixture() + + private static List GetBikeModels() { - Models = GetBikeModels(); - Renters = GetRenters(); - Bikes = GetBikes(Models); - Lease = GetLeases(Bikes, Renters); + return + [ + new() + { + Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxCyclistWeight = 95, Weight = 8.2, + BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 + }, + new() + { + Id = 2, Type = BikeType.Road, WheelSize = 27, MaxCyclistWeight = 115, Weight = 12.8, + BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 + }, + new() + { + Id = 3, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 85, Weight = 7.9, + BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 + }, + new() + { + Id = 4, Type = BikeType.Road, WheelSize = 29, MaxCyclistWeight = 105, Weight = 14.7, + BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 + }, + new() + { + Id = 5, Type = BikeType.Hybrid, WheelSize = 26, MaxCyclistWeight = 90, Weight = 6.8, + BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 + }, + new() + { + Id = 6, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 125, Weight = 13.5, + BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 + }, + new() + { + Id = 7, Type = BikeType.Mountain, WheelSize = 27, MaxCyclistWeight = 110, Weight = 12.2, + BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 + }, + new() + { + Id = 8, Type = BikeType.Hybrid, WheelSize = 29, MaxCyclistWeight = 100, Weight = 7.5, + BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 + }, + new() + { + Id = 9, Type = BikeType.Sport, WheelSize = 26, MaxCyclistWeight = 130, Weight = 15.8, + BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 + }, + new() + { + Id = 10, Type = BikeType.Road, WheelSize = 28, MaxCyclistWeight = 80, Weight = 9.3, + BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 + } + ]; } - - private static List GetBikeModels() => - [ - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 26, MaxCyclistWeight = 95, Weight = 8.2, BrakeType = "Carbon", YearOfManufacture = "2024", RentPrice = 18 }, - new() { Id = 2, Type = BikeType.Road, WheelSize = 27, MaxCyclistWeight = 115, Weight = 12.8, BrakeType = "Hydraulic", YearOfManufacture = "2023", RentPrice = 25 }, - new() { Id = 3, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 85, Weight = 7.9, BrakeType = "V-Brake", YearOfManufacture = "2024", RentPrice = 22 }, - new() { Id = 4, Type = BikeType.Road, WheelSize = 29, MaxCyclistWeight = 105, Weight = 14.7, BrakeType = "Mechanical", YearOfManufacture = "2023", RentPrice = 20 }, - new() { Id = 5, Type = BikeType.Hybrid, WheelSize = 26, MaxCyclistWeight = 90, Weight = 6.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 35 }, - new() { Id = 6, Type = BikeType.Sport, WheelSize = 28, MaxCyclistWeight = 125, Weight = 13.5, BrakeType = "Disc", YearOfManufacture = "2023", RentPrice = 28 }, - new() { Id = 7, Type = BikeType.Mountain, WheelSize = 27, MaxCyclistWeight = 110, Weight = 12.2, BrakeType = "V-Brake", YearOfManufacture = "2022", RentPrice = 16 }, - new() { Id = 8, Type = BikeType.Hybrid, WheelSize = 29, MaxCyclistWeight = 100, Weight = 7.5, BrakeType = "Carbon", YearOfManufacture = "2023", RentPrice = 32 }, - new() { Id = 9, Type = BikeType.Sport, WheelSize = 26, MaxCyclistWeight = 130, Weight = 15.8, BrakeType = "Hydraulic", YearOfManufacture = "2024", RentPrice = 24 }, - new() { Id = 10, Type = BikeType.Road, WheelSize = 28, MaxCyclistWeight = 80, Weight = 9.3, BrakeType = "Mechanical", YearOfManufacture = "2022", RentPrice = 19 }, - ]; - private static List GetRenters() => - [ - new() { Id = 1, FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, - new() { Id = 2, FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, - new() { Id = 3, FullName = "Григорьев Григорий", PhoneNumber = "+7 934 567 89 01" }, - new() { Id = 4, FullName = "Дмитриева Ольга", PhoneNumber = "+7 945 678 90 12" }, - new() { Id = 5, FullName = "Николаева Светлана", PhoneNumber = "+7 956 789 01 23" }, - new() { Id = 6, FullName = "Михайлов Сергей", PhoneNumber = "+7 967 890 12 34" }, - new() { Id = 7, FullName = "Романова Татьяна", PhoneNumber = "+7 978 901 23 45" }, - new() { Id = 8, FullName = "Павлов Дмитрий", PhoneNumber = "+7 989 012 34 56" }, - new() { Id = 9, FullName = "Фёдорова Екатерина", PhoneNumber = "+7 990 123 45 67" }, - new() { Id = 10, FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" }, - ]; + private static List GetRenters() + { + return + [ + new() { Id = 1, FullName = "Алексеев Алексей", PhoneNumber = "+7 912 345 67 89" }, + new() { Id = 2, FullName = "Васильев Василий", PhoneNumber = "+7 923 456 78 90" }, + new() { Id = 3, FullName = "Григорьев Григорий", PhoneNumber = "+7 934 567 89 01" }, + new() { Id = 4, FullName = "Дмитриева Ольга", PhoneNumber = "+7 945 678 90 12" }, + new() { Id = 5, FullName = "Николаева Светлана", PhoneNumber = "+7 956 789 01 23" }, + new() { Id = 6, FullName = "Михайлов Сергей", PhoneNumber = "+7 967 890 12 34" }, + new() { Id = 7, FullName = "Романова Татьяна", PhoneNumber = "+7 978 901 23 45" }, + new() { Id = 8, FullName = "Павлов Дмитрий", PhoneNumber = "+7 989 012 34 56" }, + new() { Id = 9, FullName = "Фёдорова Екатерина", PhoneNumber = "+7 990 123 45 67" }, + new() { Id = 10, FullName = "Андреева Наталья", PhoneNumber = "+7 901 234 56 78" } + ]; + } - private static List GetBikes(List models) => - [ - new() - { - Id = 1, - SerialNumber = "R001", - Color = "Silver", - Model = models[0], - ModelId = 0 - }, - new() - { - Id = 2, - SerialNumber = "R002", - Color = "Navy", - Model = models[1], - ModelId = 0 - }, - new() - { - Id = 3, - SerialNumber = "R003", - Color = "Charcoal", - Model = models[2], - ModelId = 0 - }, - new() - { - Id = 4, - SerialNumber = "R004", - Color = "Beige", - Model = models[3], - ModelId = 0 - }, - new() - { - Id = 5, - SerialNumber = "R005", - Color = "Burgundy", - Model = models[4], - ModelId = 0 - }, - new() - { - Id = 6, - SerialNumber = "R006", - Color = "Teal", - Model = models[5], - ModelId = 0 - }, - new() - { - Id = 7, - SerialNumber = "R007", - Color = "Coral", - Model = models[6], - ModelId = 0 - }, - new() - { - Id = 8, - SerialNumber = "R008", - Color = "Indigo", - Model = models[7], - ModelId = 0 - }, - new() - { - Id = 9, - SerialNumber = "R009", - Color = "Bronze", - Model = models[8], - ModelId = 0 - }, - new() - { - Id = 10, - SerialNumber = "R010", - Color = "Lavender", - Model = models[9], - ModelId = 0 - }, - ]; + private static List GetBikes(List models) + { + return + [ + new() + { + Id = 1, + SerialNumber = "R001", + Color = "Silver", + Model = models[0], + ModelId = 0 + }, + new() + { + Id = 2, + SerialNumber = "R002", + Color = "Navy", + Model = models[1], + ModelId = 0 + }, + new() + { + Id = 3, + SerialNumber = "R003", + Color = "Charcoal", + Model = models[2], + ModelId = 0 + }, + new() + { + Id = 4, + SerialNumber = "R004", + Color = "Beige", + Model = models[3], + ModelId = 0 + }, + new() + { + Id = 5, + SerialNumber = "R005", + Color = "Burgundy", + Model = models[4], + ModelId = 0 + }, + new() + { + Id = 6, + SerialNumber = "R006", + Color = "Teal", + Model = models[5], + ModelId = 0 + }, + new() + { + Id = 7, + SerialNumber = "R007", + Color = "Coral", + Model = models[6], + ModelId = 0 + }, + new() + { + Id = 8, + SerialNumber = "R008", + Color = "Indigo", + Model = models[7], + ModelId = 0 + }, + new() + { + Id = 9, + SerialNumber = "R009", + Color = "Bronze", + Model = models[8], + ModelId = 0 + }, + new() + { + Id = 10, + SerialNumber = "R010", + Color = "Lavender", + Model = models[9], + ModelId = 0 + } + ]; + } - private static List GetLeases(List bikes, List renters) => - [ - new() - { - Id = 1, - Bike = bikes[0], - Renter = renters[0], - RentalStartTime = DateTime.Now.AddHours(-12), - RentalDuration = 3, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 2, - Bike = bikes[1], - Renter = renters[1], - RentalStartTime = DateTime.Now.AddHours(-8), - RentalDuration = 6, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 3, - Bike = bikes[2], - Renter = renters[5], - RentalStartTime = DateTime.Now.AddHours(-15), - RentalDuration = 4, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 4, - Bike = bikes[3], - Renter = renters[5], - RentalStartTime = DateTime.Now.AddHours(-5), - RentalDuration = 2, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 5, - Bike = bikes[4], - Renter = renters[4], - RentalStartTime = DateTime.Now.AddHours(-20), - RentalDuration = 8, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 6, - Bike = bikes[5], - Renter = renters[5], - RentalStartTime = DateTime.Now.AddHours(-3), - RentalDuration = 1, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 7, - Bike = bikes[6], - Renter = renters[6], - RentalStartTime = DateTime.Now.AddHours(-18), - RentalDuration = 5, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 8, - Bike = bikes[7], - Renter = renters[6], - RentalStartTime = DateTime.Now.AddHours(-7), - RentalDuration = 7, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 9, - Bike = bikes[8], - Renter = renters[8], - RentalStartTime = DateTime.Now.AddHours(-10), - RentalDuration = 4, - BikeId = 0, - RenterId = 0 - }, - new() - { - Id = 10, - Bike = bikes[9], - Renter = renters[9], - RentalStartTime = DateTime.Now.AddHours(-2), - RentalDuration = 4, - BikeId = 0, - RenterId = 0 - }, - ]; + private static List GetLeases(List bikes, List renters) + { + return + [ + new() + { + Id = 1, + Bike = bikes[0], + Renter = renters[0], + RentalStartTime = DateTime.Now.AddHours(-12), + RentalDuration = 3, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 2, + Bike = bikes[1], + Renter = renters[1], + RentalStartTime = DateTime.Now.AddHours(-8), + RentalDuration = 6, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 3, + Bike = bikes[2], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-15), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 4, + Bike = bikes[3], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-5), + RentalDuration = 2, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 5, + Bike = bikes[4], + Renter = renters[4], + RentalStartTime = DateTime.Now.AddHours(-20), + RentalDuration = 8, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 6, + Bike = bikes[5], + Renter = renters[5], + RentalStartTime = DateTime.Now.AddHours(-3), + RentalDuration = 1, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 7, + Bike = bikes[6], + Renter = renters[6], + RentalStartTime = DateTime.Now.AddHours(-18), + RentalDuration = 5, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 8, + Bike = bikes[7], + Renter = renters[6], + RentalStartTime = DateTime.Now.AddHours(-7), + RentalDuration = 7, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 9, + Bike = bikes[8], + Renter = renters[8], + RentalStartTime = DateTime.Now.AddHours(-10), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + }, + new() + { + Id = 10, + Bike = bikes[9], + Renter = renters[9], + RentalStartTime = DateTime.Now.AddHours(-2), + RentalDuration = 4, + BikeId = 0, + RenterId = 0 + } + ]; + } } \ No newline at end of file