From ac1213c90fb8886e7ad21d726a08560be5b4c8e4 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 16 Sep 2025 22:57:30 +0400 Subject: [PATCH 01/41] Initial commit --- Bikes/Bikes.Domain/Bike.cs | 5 +++++ Bikes/Bikes.Domain/Bikes.Domain.csproj | 9 +++++++++ Bikes/Bikes.Domain/Renter.cs | 5 +++++ Bikes/Bikes.sln | 25 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 Bikes/Bikes.Domain/Bike.cs create mode 100644 Bikes/Bikes.Domain/Bikes.Domain.csproj create mode 100644 Bikes/Bikes.Domain/Renter.cs create mode 100644 Bikes/Bikes.sln diff --git a/Bikes/Bikes.Domain/Bike.cs b/Bikes/Bikes.Domain/Bike.cs new file mode 100644 index 000000000..4029851f6 --- /dev/null +++ b/Bikes/Bikes.Domain/Bike.cs @@ -0,0 +1,5 @@ +namespace Bikes.Domain; +public class Bike +{ + +} diff --git a/Bikes/Bikes.Domain/Bikes.Domain.csproj b/Bikes/Bikes.Domain/Bikes.Domain.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Bikes/Bikes.Domain/Bikes.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Bikes/Bikes.Domain/Renter.cs b/Bikes/Bikes.Domain/Renter.cs new file mode 100644 index 000000000..0a13d677d --- /dev/null +++ b/Bikes/Bikes.Domain/Renter.cs @@ -0,0 +1,5 @@ +namespace Bikes.Domain; +public class Renter +{ + +} diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln new file mode 100644 index 000000000..a9b7ae5a1 --- /dev/null +++ b/Bikes/Bikes.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35806.99 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Domain", "Bikes.Domain\Bikes.Domain.csproj", "{B6E3E827-ADA9-4B0E-B704-484513A051A8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E4862602-E7E8-42DA-A694-8CBA919A75FA} + EndGlobalSection +EndGlobal From 0f7b2c8c4d0c301c7e63edb3f18046f6f48924fb Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 28 Sep 2025 23:38:00 +0400 Subject: [PATCH 02/41] Add classes --- Bikes/Bikes.Domain/Bike.cs | 5 ----- Bikes/Bikes.Domain/Models/Bike.cs | 10 ++++++++++ Bikes/Bikes.Domain/Models/BikeModel.cs | 13 +++++++++++++ Bikes/Bikes.Domain/Models/BikeType.cs | 8 ++++++++ Bikes/Bikes.Domain/Models/Rent.cs | 10 ++++++++++ Bikes/Bikes.Domain/Models/Renter.cs | 16 ++++++++++++++++ Bikes/Bikes.Domain/Renter.cs | 5 ----- 7 files changed, 57 insertions(+), 10 deletions(-) delete mode 100644 Bikes/Bikes.Domain/Bike.cs create mode 100644 Bikes/Bikes.Domain/Models/Bike.cs create mode 100644 Bikes/Bikes.Domain/Models/BikeModel.cs create mode 100644 Bikes/Bikes.Domain/Models/BikeType.cs create mode 100644 Bikes/Bikes.Domain/Models/Rent.cs create mode 100644 Bikes/Bikes.Domain/Models/Renter.cs delete mode 100644 Bikes/Bikes.Domain/Renter.cs diff --git a/Bikes/Bikes.Domain/Bike.cs b/Bikes/Bikes.Domain/Bike.cs deleted file mode 100644 index 4029851f6..000000000 --- a/Bikes/Bikes.Domain/Bike.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Bikes.Domain; -public class Bike -{ - -} diff --git a/Bikes/Bikes.Domain/Models/Bike.cs b/Bikes/Bikes.Domain/Models/Bike.cs new file mode 100644 index 000000000..029741d04 --- /dev/null +++ b/Bikes/Bikes.Domain/Models/Bike.cs @@ -0,0 +1,10 @@ +namespace Bikes.Domain.Models; +public class Bike +{ + public required int Id { get; set; } + + public required string SerialNumber { get; set; } + + public required string Color { get; set; } + public required BikeModel Model { get; set; } +} diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs new file mode 100644 index 000000000..e1fe54b63 --- /dev/null +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -0,0 +1,13 @@ +namespace Bikes.Domain.Models; + +public class BikeModel +{ + public required int Id { get; set; } + public required BikeType Type { get; set; } + public required int WheelSize { get; set; } + public required int MaxPassengerWeight { get; set; } + public required int Weight { get; set; } + public required string BrakeType { get; set; } + public required string Year { get; set; } + public required int RentPrice { get; set; } +} diff --git a/Bikes/Bikes.Domain/Models/BikeType.cs b/Bikes/Bikes.Domain/Models/BikeType.cs new file mode 100644 index 000000000..94e29167c --- /dev/null +++ b/Bikes/Bikes.Domain/Models/BikeType.cs @@ -0,0 +1,8 @@ +namespace Bikes.Domain.Models; + +public enum BikeType +{ + Sport, + Mountain, + City +} diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs new file mode 100644 index 000000000..0b4f44de8 --- /dev/null +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -0,0 +1,10 @@ +namespace Bikes.Domain.Models; + +public class Rent +{ + public required int Id { get; set; } + public required DateTime RentalStartTime { get; set; } + public required int RentalDuration { get; set; } + public required Renter Renter { get; set; } + public required Bike Bike { get; set; } +} diff --git a/Bikes/Bikes.Domain/Models/Renter.cs b/Bikes/Bikes.Domain/Models/Renter.cs new file mode 100644 index 000000000..b23285ba6 --- /dev/null +++ b/Bikes/Bikes.Domain/Models/Renter.cs @@ -0,0 +1,16 @@ +namespace Bikes.Domain.Models; +public class Renter +{ + /// + /// The unique id for the renter + /// + public required int Id { get; set; } + /// + /// Renter's full name + /// + public required string FullName { get; set; } + /// + /// Renter's phone number + /// + public required string Number { get; set; } +} diff --git a/Bikes/Bikes.Domain/Renter.cs b/Bikes/Bikes.Domain/Renter.cs deleted file mode 100644 index 0a13d677d..000000000 --- a/Bikes/Bikes.Domain/Renter.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Bikes.Domain; -public class Renter -{ - -} From 7fb6b3cfe69888f58e1733d63fbd918beef3e3a8 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 30 Sep 2025 14:01:40 +0400 Subject: [PATCH 03/41] Fix classes, add comments --- Bikes/Bikes.Domain/Models/Bike.cs | 17 ++++++++++++++-- Bikes/Bikes.Domain/Models/BikeModel.cs | 27 ++++++++++++++++++++++++++ Bikes/Bikes.Domain/Models/BikeType.cs | 12 ++++++++++++ Bikes/Bikes.Domain/Models/Rent.cs | 18 +++++++++++++++++ Bikes/Bikes.Domain/Models/Renter.cs | 6 +++++- 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/Bikes/Bikes.Domain/Models/Bike.cs b/Bikes/Bikes.Domain/Models/Bike.cs index 029741d04..70c31704a 100644 --- a/Bikes/Bikes.Domain/Models/Bike.cs +++ b/Bikes/Bikes.Domain/Models/Bike.cs @@ -1,10 +1,23 @@ namespace Bikes.Domain.Models; +/// +/// A class describing a bike +/// public class Bike { + /// + /// 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 BikeModel Model { get; set; } } diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs index e1fe54b63..cd6bc9bf2 100644 --- a/Bikes/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -1,13 +1,40 @@ namespace Bikes.Domain.Models; +/// +/// A class describing a bike's model +/// public class BikeModel { + /// + /// Model's unique id + /// public required int Id { get; set; } + /// + /// Model's type + /// public required BikeType Type { get; set; } + /// + /// Model's size of wheel + /// public required int WheelSize { get; set; } + /// + /// Maximum allowable passenger weight + /// public required int MaxPassengerWeight { get; set; } + /// + /// Model's weight + /// public required int Weight { get; set; } + /// + /// Model's type of brake + /// public required string BrakeType { get; set; } + /// + /// Model's production year + /// public required string Year { get; set; } + /// + /// The price of an hour of rent + /// public required int RentPrice { get; set; } } diff --git a/Bikes/Bikes.Domain/Models/BikeType.cs b/Bikes/Bikes.Domain/Models/BikeType.cs index 94e29167c..98790b7db 100644 --- a/Bikes/Bikes.Domain/Models/BikeType.cs +++ b/Bikes/Bikes.Domain/Models/BikeType.cs @@ -1,8 +1,20 @@ namespace Bikes.Domain.Models; +/// +/// A enum describing bike's type +/// public enum BikeType { + /// + /// Sports bike + /// Sport, + /// + /// Mountain bike + /// Mountain, + /// + /// City bike + /// City } diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs index 0b4f44de8..daac9f26c 100644 --- a/Bikes/Bikes.Domain/Models/Rent.cs +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -1,10 +1,28 @@ namespace Bikes.Domain.Models; +/// +/// A class describing a rent +/// public class Rent { + /// + /// Rent's unique id + /// public required int Id { get; set; } + /// + /// Rental start time + /// public required DateTime RentalStartTime { get; set; } + /// + /// Rental duration + /// public required int RentalDuration { get; set; } + /// + /// Renter + /// public required Renter Renter { get; set; } + /// + /// Bike + /// public required Bike Bike { get; set; } } diff --git a/Bikes/Bikes.Domain/Models/Renter.cs b/Bikes/Bikes.Domain/Models/Renter.cs index b23285ba6..cc4f2c7af 100644 --- a/Bikes/Bikes.Domain/Models/Renter.cs +++ b/Bikes/Bikes.Domain/Models/Renter.cs @@ -1,8 +1,12 @@ namespace Bikes.Domain.Models; + +/// +/// A class describing a renter +/// public class Renter { /// - /// The unique id for the renter + /// Renter's unique id /// public required int Id { get; set; } /// From a053bfd8b93add58b5ef2cddca5bbb1c1c2159d7 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 1 Oct 2025 23:14:20 +0400 Subject: [PATCH 04/41] Add test data --- Bikes/Bikes.Domain/Models/BikeModel.cs | 1 - Bikes/Bikes.Domain/Models/BikeType.cs | 1 - Bikes/Bikes.Domain/Models/Rent.cs | 1 - Bikes/Bikes.Domain/Models/Renter.cs | 1 - Bikes/Bikes.Tests/Bikes.Tests.csproj | 27 ++++++++++ Bikes/Bikes.Tests/BikesFixture.cs | 73 ++++++++++++++++++++++++++ Bikes/Bikes.sln | 8 ++- 7 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 Bikes/Bikes.Tests/Bikes.Tests.csproj create mode 100644 Bikes/Bikes.Tests/BikesFixture.cs diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs index cd6bc9bf2..aa22d6d65 100644 --- a/Bikes/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -1,5 +1,4 @@ namespace Bikes.Domain.Models; - /// /// A class describing a bike's model /// diff --git a/Bikes/Bikes.Domain/Models/BikeType.cs b/Bikes/Bikes.Domain/Models/BikeType.cs index 98790b7db..f9860ec7c 100644 --- a/Bikes/Bikes.Domain/Models/BikeType.cs +++ b/Bikes/Bikes.Domain/Models/BikeType.cs @@ -1,5 +1,4 @@ namespace Bikes.Domain.Models; - /// /// A enum describing bike's type /// diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs index daac9f26c..3b6e615d1 100644 --- a/Bikes/Bikes.Domain/Models/Rent.cs +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -1,5 +1,4 @@ namespace Bikes.Domain.Models; - /// /// A class describing a rent /// diff --git a/Bikes/Bikes.Domain/Models/Renter.cs b/Bikes/Bikes.Domain/Models/Renter.cs index cc4f2c7af..68172d626 100644 --- a/Bikes/Bikes.Domain/Models/Renter.cs +++ b/Bikes/Bikes.Domain/Models/Renter.cs @@ -1,5 +1,4 @@ namespace Bikes.Domain.Models; - /// /// A class describing a renter /// diff --git a/Bikes/Bikes.Tests/Bikes.Tests.csproj b/Bikes/Bikes.Tests/Bikes.Tests.csproj new file mode 100644 index 000000000..3a1bf7982 --- /dev/null +++ b/Bikes/Bikes.Tests/Bikes.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs new file mode 100644 index 000000000..04d7e7989 --- /dev/null +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -0,0 +1,73 @@ +using Bikes.Domain.Models; + +namespace Bikes.Tests; + +public class BikesFixture +{ + + public List BikeModels => + [ + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, + new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, + new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 }, + new() { Id = 4, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 125, Weight = 15, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 750 }, + new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, + new() { Id = 6, Type = BikeType.City, WheelSize = 27, MaxPassengerWeight = 135, Weight = 17, BrakeType = "Дисковые механические", Year = "2023", RentPrice = 550 }, + new() { Id = 7, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 13, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 800 }, + new() { Id = 8, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 10, BrakeType = "Ободные v-brake", Year = "2023", RentPrice = 950 }, + new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 600 }, + new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } + ]; + + public List Bikes => + [ + new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = BikeModels[0] }, + new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = BikeModels[1] }, + new() { Id = 3, SerialNumber = "CTY202203001", Color = "Синий", Model = BikeModels[2] }, + new() { Id = 4, SerialNumber = "MTB202302001", Color = "Зеленый", Model = BikeModels[3] }, + new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = BikeModels[4] }, + new() { Id = 6, SerialNumber = "CTY202304001", Color = "Белый", Model = BikeModels[5] }, + new() { Id = 7, SerialNumber = "MTB202404001", Color = "Оранжевый", Model = BikeModels[6] }, + new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = BikeModels[7] }, + new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", Model = BikeModels[8] }, + new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = BikeModels[9] } + ]; + + public List Renters => + [ + new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, + new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, + new() { Id = 3, FullName = "Сидорова Анна Владимировна", Number = "+7 (934) 567-89-01" }, + new() { Id = 4, FullName = "Кузнецов Алексей Дмитриевич", Number = "+7 (945) 678-90-12" }, + new() { Id = 5, FullName = "Смирнова Екатерина Олеговна", Number = "+7 (956) 789-01-23" }, + new() { Id = 6, FullName = "Попов Денис Андреевич", Number = "+7 (967) 890-12-34" }, + new() { Id = 7, FullName = "Васильева Мария Игоревна", Number = "+7 (978) 901-23-45" }, + new() { Id = 8, FullName = "Николаев Сергей Викторович", Number = "+7 (989) 012-34-56" }, + new() { Id = 9, FullName = "Орлова Ольга Павловна", Number = "+7 (990) 123-45-67" }, + new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } + ]; + + public List Rents => + [ + new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[0] }, + new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[1] }, + new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[2] }, + new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = Renters[3], Bike = Bikes[3] }, + new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = Renters[4], Bike = Bikes[4] }, + new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = Renters[5], Bike = Bikes[5] }, + new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = Renters[6], Bike = Bikes[6] }, + new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = Renters[7], Bike = Bikes[7] }, + new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = Renters[8], Bike = Bikes[8] }, + new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = Renters[9], Bike = Bikes[9] }, + new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[1] }, + new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = Renters[1], Bike = Bikes[2] }, + new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = Renters[2], Bike = Bikes[3] }, + new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = Renters[3], Bike = Bikes[4] }, + new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = Renters[4], Bike = Bikes[5] }, + new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[6] }, + new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = Renters[6], Bike = Bikes[7] }, + new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = Renters[7], Bike = Bikes[8] }, + new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[8], Bike = Bikes[9] }, + new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[9], Bike = Bikes[0] } + ]; +} diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index a9b7ae5a1..bdf5353bf 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.13.35806.99 d17.13 +VisualStudioVersion = 17.13.35806.99 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Domain", "Bikes.Domain\Bikes.Domain.csproj", "{B6E3E827-ADA9-4B0E-B704-484513A051A8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{54F042E8-6681-4802-B300-ADFE25207ACB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {B6E3E827-ADA9-4B0E-B704-484513A051A8}.Release|Any CPU.Build.0 = Release|Any CPU + {54F042E8-6681-4802-B300-ADFE25207ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54F042E8-6681-4802-B300-ADFE25207ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54F042E8-6681-4802-B300-ADFE25207ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54F042E8-6681-4802-B300-ADFE25207ACB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a8a15a65afac8f7d43561c2615930261d23dcd94 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Thu, 2 Oct 2025 22:17:35 +0400 Subject: [PATCH 05/41] Add tests --- Bikes/Bikes.Tests/BikesFixture.cs | 40 ++++---- Bikes/Bikes.Tests/BikesTests.cs | 164 ++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 Bikes/Bikes.Tests/BikesTests.cs diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 04d7e7989..7e7ed411c 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -5,7 +5,7 @@ namespace Bikes.Tests; public class BikesFixture { - public List BikeModels => + public List BikeModels = [ new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, @@ -50,24 +50,24 @@ public class BikesFixture public List Rents => [ new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[0] }, - new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[1] }, - new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[2] }, - new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = Renters[3], Bike = Bikes[3] }, - new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = Renters[4], Bike = Bikes[4] }, - new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = Renters[5], Bike = Bikes[5] }, - new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = Renters[6], Bike = Bikes[6] }, - new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = Renters[7], Bike = Bikes[7] }, - new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = Renters[8], Bike = Bikes[8] }, - new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = Renters[9], Bike = Bikes[9] }, - new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[1] }, - new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = Renters[1], Bike = Bikes[2] }, - new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = Renters[2], Bike = Bikes[3] }, - new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = Renters[3], Bike = Bikes[4] }, - new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = Renters[4], Bike = Bikes[5] }, - new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[6] }, - new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = Renters[6], Bike = Bikes[7] }, - new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = Renters[7], Bike = Bikes[8] }, - new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[8], Bike = Bikes[9] }, - new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[9], Bike = Bikes[0] } + new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[0] }, + new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[0] }, + new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = Renters[3], Bike = Bikes[1] }, + new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = Renters[4], Bike = Bikes[1] }, + new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = Renters[5], Bike = Bikes[1] }, + new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = Renters[6], Bike = Bikes[2] }, + new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = Renters[7], Bike = Bikes[2] }, + new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = Renters[8], Bike = Bikes[3] }, + new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = Renters[9], Bike = Bikes[3] }, + new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[4] }, + new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = Renters[0], Bike = Bikes[4] }, + new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = Renters[0], Bike = Bikes[5] }, + new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = Renters[0], Bike = Bikes[6] }, + new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = Renters[1], Bike = Bikes[7] }, + new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = Renters[1], Bike = Bikes[8] }, + new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[9] }, + new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = Renters[5], Bike = Bikes[9] }, + new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[9] }, + new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[9] } ]; } diff --git a/Bikes/Bikes.Tests/BikesTests.cs b/Bikes/Bikes.Tests/BikesTests.cs new file mode 100644 index 000000000..0bc3a7d62 --- /dev/null +++ b/Bikes/Bikes.Tests/BikesTests.cs @@ -0,0 +1,164 @@ +using Bikes.Domain.Models; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using Xunit.Sdk; + +namespace Bikes.Tests; + +public class BikesTests(BikesFixture fixture) : IClassFixture +{ + [Fact] + public void InformationAboutSportBikes() + { + var expected = new List + { + new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = fixture.BikeModels[1] }, + new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = fixture.BikeModels[4] }, + new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = fixture.BikeModels[7] }, + }; + + var actual = fixture.Bikes + .Where(bike => bike.Model.Type == BikeType.Sport) + .Select(bike => bike.Id) + .ToList(); + + + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].Id, actual[i]); + } + } + + [Fact] + public void TopFiveModelsRentDuration() + { + var expected = new List + { + new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 }, + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, + new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, + new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, + new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 } + }; + + var actual = fixture.Rents + .GroupBy(rent => rent.Bike.Model.Id) + .Select(group => new + { + ModelId = group.Key, + Model = fixture.BikeModels.First(m => m.Id == group.Key), + TotalDuration = group.Sum(rent => rent.RentalDuration) + }) + .OrderByDescending(x => x.TotalDuration) + .Select(x => x.Model.Id) + .Take(5) + .ToList(); + + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].Id, actual[i]); + } + } + + [Fact] + public void TopFiveModelsProfit() + { + var expected = new List + { + new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 }, + new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, + new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, + new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 } + }; + + var actual = fixture.Rents + .GroupBy(rent => rent.Bike.Model.Id) + .Select(group => new + { + ModelId = group.Key, + Model = fixture.BikeModels.First(m => m.Id == group.Key), + TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) + }) + .OrderByDescending(x => x.TotalProfit) + .Select(x => x.Model.Id) + .Take(5) + .ToList(); + + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].Id, actual[i]); + } + } + + [Fact] + public void MinMaxAvgRentalDuration() + { + const int expectedMin = 1; + const int expectedMax = 5; + const double expectedAvg = 2.95; + + var actualMin = fixture.Rents.Min(rent => rent.RentalDuration); + var actualMax = fixture.Rents.Max(rent => rent.RentalDuration); + var actualAvg = fixture.Rents.Average(rent => rent.RentalDuration); + + Assert.Equal(expectedMin, actualMin); + Assert.Equal(expectedMax, actualMax); + Assert.Equal(expectedAvg, actualAvg); + } + + [Fact] + public void TotalRentalTimeByType() + { + var expectedSportRentalTime = 17; + var expectedMountainRentaltime = 30; + var expectedCityRentaltime = 12; + + var actual = fixture.Rents + .GroupBy(rent => rent.Bike.Model.Type) + .Select(group => new + { + ModelType = group.Key, + TotalRentalTime = group.Sum(rent => rent.RentalDuration) + }) + .Select(x => x) + .ToList(); + + var actualSportRentalTime = actual.Where(x => x.ModelType == BikeType.Sport).Select(x => x.TotalRentalTime).First(); + var actualMountainRentaltime = actual.Where(x => x.ModelType == BikeType.Mountain).Select(x => x.TotalRentalTime).First(); + var actualCityRentaltime = actual.Where(x => x.ModelType == BikeType.City).Select(x => x.TotalRentalTime).First(); + + Assert.Equal(expectedSportRentalTime, actualSportRentalTime); + Assert.Equal(expectedMountainRentaltime, actualMountainRentaltime); + Assert.Equal(expectedCityRentaltime, actualCityRentaltime); + } + + [Fact] + public void TopThreeRenters() + { + var expectedTopRenters = new List + { + new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, + new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, + new() { Id = 6, FullName = "Попов Денис Андреевич", Number = "+7 (967) 890-12-34" }, + }; + + var actualTopRenters = fixture.Rents + .GroupBy(rent => rent.Renter.Id) + .Select(group => new + { + RenterId = group.Key, + RenterName = fixture.Renters.First(r => r.Id == group.Key), + TotalRentals = group.Count() + }) + .OrderByDescending(r => r.TotalRentals) + .Select(x => x.RenterId) + .Take(3) + .ToList(); + + for (var i = 0; i < actualTopRenters.Count; i++) + { + Assert.Equal(expectedTopRenters[i].Id, actualTopRenters[i]); + } + } +} From cad8d29e6b16c2f74f70e68216f6fb0bf3676c06 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:05:26 +0400 Subject: [PATCH 06/41] Fix tests and add comments --- Bikes/Bikes.Domain/Models/Bike.cs | 1 + Bikes/Bikes.Domain/Models/BikeModel.cs | 1 + Bikes/Bikes.Domain/Models/BikeType.cs | 3 + Bikes/Bikes.Domain/Models/Rent.cs | 1 + Bikes/Bikes.Domain/Models/Renter.cs | 1 + Bikes/Bikes.Tests/BikesFixture.cs | 20 +++++- Bikes/Bikes.Tests/BikesTests.cs | 94 ++++++++++---------------- 7 files changed, 61 insertions(+), 60 deletions(-) diff --git a/Bikes/Bikes.Domain/Models/Bike.cs b/Bikes/Bikes.Domain/Models/Bike.cs index 70c31704a..45ad2a1ae 100644 --- a/Bikes/Bikes.Domain/Models/Bike.cs +++ b/Bikes/Bikes.Domain/Models/Bike.cs @@ -1,4 +1,5 @@ namespace Bikes.Domain.Models; + /// /// A class describing a bike /// diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs index aa22d6d65..cd6bc9bf2 100644 --- a/Bikes/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -1,4 +1,5 @@ namespace Bikes.Domain.Models; + /// /// A class describing a bike's model /// diff --git a/Bikes/Bikes.Domain/Models/BikeType.cs b/Bikes/Bikes.Domain/Models/BikeType.cs index f9860ec7c..95f8e45a6 100644 --- a/Bikes/Bikes.Domain/Models/BikeType.cs +++ b/Bikes/Bikes.Domain/Models/BikeType.cs @@ -1,4 +1,5 @@ namespace Bikes.Domain.Models; + /// /// A enum describing bike's type /// @@ -8,10 +9,12 @@ public enum BikeType /// Sports bike /// Sport, + /// /// Mountain bike /// Mountain, + /// /// City bike /// diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs index 3b6e615d1..daac9f26c 100644 --- a/Bikes/Bikes.Domain/Models/Rent.cs +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -1,4 +1,5 @@ namespace Bikes.Domain.Models; + /// /// A class describing a rent /// diff --git a/Bikes/Bikes.Domain/Models/Renter.cs b/Bikes/Bikes.Domain/Models/Renter.cs index 68172d626..cc4f2c7af 100644 --- a/Bikes/Bikes.Domain/Models/Renter.cs +++ b/Bikes/Bikes.Domain/Models/Renter.cs @@ -1,4 +1,5 @@ namespace Bikes.Domain.Models; + /// /// A class describing a renter /// diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 7e7ed411c..564fac64a 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -2,10 +2,15 @@ namespace Bikes.Tests; +/// +/// A class for creating the data needed for testing +/// public class BikesFixture { - - public List BikeModels = + /// + /// List of bike models + /// + public List BikeModels => [ new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, @@ -19,6 +24,9 @@ public class BikesFixture new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } ]; + /// + /// List of bikes + /// public List Bikes => [ new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = BikeModels[0] }, @@ -33,6 +41,9 @@ public class BikesFixture new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = BikeModels[9] } ]; + /// + /// List of renters + /// public List Renters => [ new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, @@ -47,6 +58,9 @@ public class BikesFixture new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } ]; + /// + /// List of rents + /// public List Rents => [ new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[0] }, @@ -70,4 +84,4 @@ public class BikesFixture new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[9] }, new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[9] } ]; -} +} \ No newline at end of file diff --git a/Bikes/Bikes.Tests/BikesTests.cs b/Bikes/Bikes.Tests/BikesTests.cs index 0bc3a7d62..8e8dd578d 100644 --- a/Bikes/Bikes.Tests/BikesTests.cs +++ b/Bikes/Bikes.Tests/BikesTests.cs @@ -1,47 +1,37 @@ using Bikes.Domain.Models; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using Xunit.Sdk; namespace Bikes.Tests; +/// +/// A class that implements a set of unit tests +/// public class BikesTests(BikesFixture fixture) : IClassFixture { + /// + /// A test that outputs information about all sports bikes + /// [Fact] public void InformationAboutSportBikes() { - var expected = new List - { - new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = fixture.BikeModels[1] }, - new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = fixture.BikeModels[4] }, - new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = fixture.BikeModels[7] }, - }; - - var actual = fixture.Bikes + var expectedModelIds = new List {2, 5, 8}; + + var actualIds = fixture.Bikes .Where(bike => bike.Model.Type == BikeType.Sport) .Select(bike => bike.Id) .ToList(); - - for (var i = 0; i < expected.Count; i++) - { - Assert.Equal(expected[i].Id, actual[i]); - } + Assert.Equal(expectedModelIds, actualIds); } + /// + /// A test that outputs the top 5 bike models by rental duration + /// [Fact] - public void TopFiveModelsRentDuration() + public void TopFiveModelsRentDurationIds() { - var expected = new List - { - new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 }, - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, - new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, - new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, - new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 } - }; + var expectedModelIds = new List {10, 1, 2, 5, 3}; - var actual = fixture.Rents + var actualIds = fixture.Rents .GroupBy(rent => rent.Bike.Model.Id) .Select(group => new { @@ -54,25 +44,18 @@ public void TopFiveModelsRentDuration() .Take(5) .ToList(); - for (var i = 0; i < expected.Count; i++) - { - Assert.Equal(expected[i].Id, actual[i]); - } + Assert.Equal(expectedModelIds, actualIds); } + /// + /// A test that outputs the top 5 bike models in terms of rental income + /// [Fact] public void TopFiveModelsProfit() { - var expected = new List - { - new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 }, - new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, - new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, - new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 } - }; + var expectedModelIds = new List {10, 5, 2, 1, 3}; - var actual = fixture.Rents + var actualIds = fixture.Rents .GroupBy(rent => rent.Bike.Model.Id) .Select(group => new { @@ -84,13 +67,12 @@ public void TopFiveModelsProfit() .Select(x => x.Model.Id) .Take(5) .ToList(); - - for (var i = 0; i < expected.Count; i++) - { - Assert.Equal(expected[i].Id, actual[i]); - } + Assert.Equal(expectedModelIds, actualIds); } + /// + /// A test that outputs information about the minimum, maximum, and average bike rental time. + /// [Fact] public void MinMaxAvgRentalDuration() { @@ -107,6 +89,9 @@ public void MinMaxAvgRentalDuration() Assert.Equal(expectedAvg, actualAvg); } + /// + /// A test that outputs the total rental time of each type of bike + /// [Fact] public void TotalRentalTimeByType() { @@ -133,17 +118,15 @@ public void TotalRentalTimeByType() Assert.Equal(expectedCityRentaltime, actualCityRentaltime); } + /// + /// A test that outputs information about the customers who have rented bicycles the most times. + /// [Fact] public void TopThreeRenters() { - var expectedTopRenters = new List - { - new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, - new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, - new() { Id = 6, FullName = "Попов Денис Андреевич", Number = "+7 (967) 890-12-34" }, - }; - - var actualTopRenters = fixture.Rents + var expectedTopRentersIds = new List {1, 2, 6}; + + var actualTopRentersIds = fixture.Rents .GroupBy(rent => rent.Renter.Id) .Select(group => new { @@ -156,9 +139,6 @@ public void TopThreeRenters() .Take(3) .ToList(); - for (var i = 0; i < actualTopRenters.Count; i++) - { - Assert.Equal(expectedTopRenters[i].Id, actualTopRenters[i]); - } + Assert.Equal(expectedTopRentersIds, actualTopRentersIds); } -} +} \ No newline at end of file From fff23039eab106422e55f032c9815a6a90dcb0bd Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:31:01 +0400 Subject: [PATCH 07/41] Add dotnet_tests.yml for automatic testing --- .github/workflows/dotnet_tests.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/dotnet_tests.yml diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml new file mode 100644 index 000000000..18f0f6f37 --- /dev/null +++ b/.github/workflows/dotnet_tests.yml @@ -0,0 +1,29 @@ +name: .NET Tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + test: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Run Bikes.Tests + run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file From f13636693193459c3fed37e3901c2f92d4a4edc4 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:35:12 +0400 Subject: [PATCH 08/41] Fix dotnet_tests --- .github/workflows/dotnet_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml index 18f0f6f37..58304dfd0 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -5,7 +5,7 @@ on: branches: ["main"] pull_request: branches: ["main"] - + jobs: test: runs-on: windows-latest From 73ae2ec8743eba6c8fd81d17814fc352346d161b Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:43:49 +0400 Subject: [PATCH 09/41] Fix dotnet_tests again --- .github/workflows/dotnet_tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml index 58304dfd0..6727bc3c5 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -20,10 +20,10 @@ jobs: dotnet-version: '8.x' - name: Restore dependencies - run: dotnet restore + run: dotnet restore ./Bikes.sln - name: Build - run: dotnet build --no-restore --configuration Release + run: dotnet build --no-restore --configuration Release ./Bikes.sln - - name: Run Bikes.Tests - run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file + - name: Run tests + run: dotnet test ./Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file From 795031b77924f039bca38e0fd35772b2c3e27180 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:49:01 +0400 Subject: [PATCH 10/41] Move github directory --- .../.github}/DISCUSSION_TEMPLATE/questions.yml | 0 ...\260\321\202\320\276\321\200\320\275\320\276\320\271.md" | 0 {.github => Bikes/.github}/PULL_REQUEST_TEMPLATE.md | 0 {.github => Bikes/.github}/workflows/dotnet_tests.yml | 6 +++--- {.github => Bikes/.github}/workflows/setup_pr.yml | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {.github => Bikes/.github}/DISCUSSION_TEMPLATE/questions.yml (100%) rename ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" => "Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" (100%) rename {.github => Bikes/.github}/PULL_REQUEST_TEMPLATE.md (100%) rename {.github => Bikes/.github}/workflows/dotnet_tests.yml (67%) rename {.github => Bikes/.github}/workflows/setup_pr.yml (100%) diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/Bikes/.github/DISCUSSION_TEMPLATE/questions.yml similarity index 100% rename from .github/DISCUSSION_TEMPLATE/questions.yml rename to Bikes/.github/DISCUSSION_TEMPLATE/questions.yml diff --git "a/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" "b/Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" similarity index 100% rename from ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" rename to "Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/Bikes/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to Bikes/.github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/dotnet_tests.yml b/Bikes/.github/workflows/dotnet_tests.yml similarity index 67% rename from .github/workflows/dotnet_tests.yml rename to Bikes/.github/workflows/dotnet_tests.yml index 6727bc3c5..7fb91df18 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/Bikes/.github/workflows/dotnet_tests.yml @@ -20,10 +20,10 @@ jobs: dotnet-version: '8.x' - name: Restore dependencies - run: dotnet restore ./Bikes.sln + run: dotnet restore Bikes.sln - name: Build - run: dotnet build --no-restore --configuration Release ./Bikes.sln + run: dotnet build --no-restore --configuration Release Bikes.sln - name: Run tests - run: dotnet test ./Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file + run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file diff --git a/.github/workflows/setup_pr.yml b/Bikes/.github/workflows/setup_pr.yml similarity index 100% rename from .github/workflows/setup_pr.yml rename to Bikes/.github/workflows/setup_pr.yml From 2d3e3433920c0d7117d7a288f410ca578216ffc9 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 00:52:29 +0400 Subject: [PATCH 11/41] Move github directory and change dotnet_tests file again x2 --- .../.github => .github}/DISCUSSION_TEMPLATE/questions.yml | 0 ...\260\321\202\320\276\321\200\320\275\320\276\320\271.md" | 0 {Bikes/.github => .github}/PULL_REQUEST_TEMPLATE.md | 0 {Bikes/.github => .github}/workflows/dotnet_tests.yml | 6 +++--- {Bikes/.github => .github}/workflows/setup_pr.yml | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {Bikes/.github => .github}/DISCUSSION_TEMPLATE/questions.yml (100%) rename "Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" => ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" (100%) rename {Bikes/.github => .github}/PULL_REQUEST_TEMPLATE.md (100%) rename {Bikes/.github => .github}/workflows/dotnet_tests.yml (76%) rename {Bikes/.github => .github}/workflows/setup_pr.yml (100%) diff --git a/Bikes/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml similarity index 100% rename from Bikes/.github/DISCUSSION_TEMPLATE/questions.yml rename to .github/DISCUSSION_TEMPLATE/questions.yml diff --git "a/Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" "b/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" similarity index 100% rename from "Bikes/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" rename to ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" diff --git a/Bikes/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from Bikes/.github/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/Bikes/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml similarity index 76% rename from Bikes/.github/workflows/dotnet_tests.yml rename to .github/workflows/dotnet_tests.yml index 7fb91df18..ec55587ad 100644 --- a/Bikes/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -20,10 +20,10 @@ jobs: dotnet-version: '8.x' - name: Restore dependencies - run: dotnet restore Bikes.sln + run: dotnet restore Bikes/Bikes.sln - name: Build - run: dotnet build --no-restore --configuration Release Bikes.sln + run: dotnet build --no-restore --configuration Release Bikes/Bikes.sln - name: Run tests - run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file + run: dotnet test Bikes/Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release \ No newline at end of file diff --git a/Bikes/.github/workflows/setup_pr.yml b/.github/workflows/setup_pr.yml similarity index 100% rename from Bikes/.github/workflows/setup_pr.yml rename to .github/workflows/setup_pr.yml From 1a4f80aae9bbf7255f0edb81111e9aa63736fd3c Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 3 Oct 2025 01:01:25 +0400 Subject: [PATCH 12/41] Small fixes --- Bikes/Bikes.Domain/Models/Bike.cs | 3 +++ Bikes/Bikes.Domain/Models/BikeModel.cs | 7 +++++++ Bikes/Bikes.Domain/Models/Rent.cs | 4 ++++ Bikes/Bikes.Domain/Models/Renter.cs | 2 ++ 4 files changed, 16 insertions(+) diff --git a/Bikes/Bikes.Domain/Models/Bike.cs b/Bikes/Bikes.Domain/Models/Bike.cs index 45ad2a1ae..f686c1a91 100644 --- a/Bikes/Bikes.Domain/Models/Bike.cs +++ b/Bikes/Bikes.Domain/Models/Bike.cs @@ -9,14 +9,17 @@ public class Bike /// 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 /// diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs index cd6bc9bf2..fd56af753 100644 --- a/Bikes/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -9,30 +9,37 @@ public class BikeModel /// Model's unique id /// public required int Id { get; set; } + /// /// Model's type /// public required BikeType Type { get; set; } + /// /// Model's size of wheel /// public required int WheelSize { get; set; } + /// /// Maximum allowable passenger weight /// public required int MaxPassengerWeight { get; set; } + /// /// Model's weight /// public required int Weight { get; set; } + /// /// Model's type of brake /// public required string BrakeType { get; set; } + /// /// Model's production year /// public required string Year { get; set; } + /// /// The price of an hour of rent /// diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs index daac9f26c..0d2b173f9 100644 --- a/Bikes/Bikes.Domain/Models/Rent.cs +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -9,18 +9,22 @@ public class Rent /// Rent's unique id /// public required int Id { get; set; } + /// /// Rental start time /// public required DateTime RentalStartTime { get; set; } + /// /// Rental duration /// public required int RentalDuration { get; set; } + /// /// Renter /// public required Renter Renter { get; set; } + /// /// Bike /// diff --git a/Bikes/Bikes.Domain/Models/Renter.cs b/Bikes/Bikes.Domain/Models/Renter.cs index cc4f2c7af..fb43cba2a 100644 --- a/Bikes/Bikes.Domain/Models/Renter.cs +++ b/Bikes/Bikes.Domain/Models/Renter.cs @@ -9,10 +9,12 @@ public class Renter /// Renter's unique id /// public required int Id { get; set; } + /// /// Renter's full name /// public required string FullName { get; set; } + /// /// Renter's phone number /// From 2593bc01535df49a200bbd350a1bcda8f68ced41 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Oct 2025 16:04:58 +0400 Subject: [PATCH 13/41] Corrected all comments --- Bikes/Bikes.Tests/BikesFixture.cs | 50 +++++++++++++++++++++++++++---- Bikes/Bikes.Tests/BikesTests.cs | 45 ++++++++++------------------ 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 564fac64a..2f3823c06 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -1,4 +1,5 @@ using Bikes.Domain.Models; +using System.Collections.Generic; namespace Bikes.Tests; @@ -10,7 +11,37 @@ public class BikesFixture /// /// List of bike models /// - public List BikeModels => + public readonly List BikeModels; + + /// + /// List of bikes + /// + public readonly List Bikes; + + /// + /// List of renters + /// + public readonly List Renters; + + /// + /// List of rents + /// + public readonly List Rents; + + public BikesFixture() + { + BikeModels = InitializeBikeModels(); + Renters = InitializeRenters(); + Bikes = InitializeBikes(); + Rents = InitializeRents(); + } + + /// + /// List of bike models + /// + private List InitializeBikeModels() + { + return [ new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, @@ -23,11 +54,14 @@ public class BikesFixture new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 600 }, new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } ]; + } /// /// List of bikes /// - public List Bikes => + private List InitializeBikes() + { + return [ new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = BikeModels[0] }, new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = BikeModels[1] }, @@ -40,11 +74,14 @@ public class BikesFixture new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", Model = BikeModels[8] }, new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = BikeModels[9] } ]; + } /// /// List of renters /// - public List Renters => + private List InitializeRenters() + { + return [ new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, @@ -57,11 +94,13 @@ public class BikesFixture new() { Id = 9, FullName = "Орлова Ольга Павловна", Number = "+7 (990) 123-45-67" }, new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } ]; - + } /// /// List of rents /// - public List Rents => + private List InitializeRents() + { + return [ new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[0] }, new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[0] }, @@ -84,4 +123,5 @@ public class BikesFixture new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[9] }, new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[9] } ]; + } } \ No newline at end of file diff --git a/Bikes/Bikes.Tests/BikesTests.cs b/Bikes/Bikes.Tests/BikesTests.cs index 8e8dd578d..7a0462e4a 100644 --- a/Bikes/Bikes.Tests/BikesTests.cs +++ b/Bikes/Bikes.Tests/BikesTests.cs @@ -36,11 +36,10 @@ public void TopFiveModelsRentDurationIds() .Select(group => new { ModelId = group.Key, - Model = fixture.BikeModels.First(m => m.Id == group.Key), TotalDuration = group.Sum(rent => rent.RentalDuration) }) .OrderByDescending(x => x.TotalDuration) - .Select(x => x.Model.Id) + .Select(x => x.ModelId) .Take(5) .ToList(); @@ -60,11 +59,10 @@ public void TopFiveModelsProfit() .Select(group => new { ModelId = group.Key, - Model = fixture.BikeModels.First(m => m.Id == group.Key), TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) }) .OrderByDescending(x => x.TotalProfit) - .Select(x => x.Model.Id) + .Select(x => x.ModelId) .Take(5) .ToList(); Assert.Equal(expectedModelIds, actualIds); @@ -80,9 +78,10 @@ public void MinMaxAvgRentalDuration() const int expectedMax = 5; const double expectedAvg = 2.95; - var actualMin = fixture.Rents.Min(rent => rent.RentalDuration); - var actualMax = fixture.Rents.Max(rent => rent.RentalDuration); - var actualAvg = fixture.Rents.Average(rent => rent.RentalDuration); + var durations = fixture.Rents.Select(rent => rent.RentalDuration).ToList(); + var actualMin = durations.Min(); + var actualMax = durations.Max(); + var actualAvg = durations.Average(); Assert.Equal(expectedMin, actualMin); Assert.Equal(expectedMax, actualMax); @@ -92,30 +91,17 @@ public void MinMaxAvgRentalDuration() /// /// A test that outputs the total rental time of each type of bike /// - [Fact] - public void TotalRentalTimeByType() + [Theory] + [InlineData(BikeType.Sport, 17)] + [InlineData(BikeType.Mountain, 30)] + [InlineData(BikeType.City, 12)] + public void TotalRentalTimeByType(BikeType bikeType, int expectedRentalTime) { - var expectedSportRentalTime = 17; - var expectedMountainRentaltime = 30; - var expectedCityRentaltime = 12; - - var actual = fixture.Rents - .GroupBy(rent => rent.Bike.Model.Type) - .Select(group => new - { - ModelType = group.Key, - TotalRentalTime = group.Sum(rent => rent.RentalDuration) - }) - .Select(x => x) - .ToList(); - - var actualSportRentalTime = actual.Where(x => x.ModelType == BikeType.Sport).Select(x => x.TotalRentalTime).First(); - var actualMountainRentaltime = actual.Where(x => x.ModelType == BikeType.Mountain).Select(x => x.TotalRentalTime).First(); - var actualCityRentaltime = actual.Where(x => x.ModelType == BikeType.City).Select(x => x.TotalRentalTime).First(); + var actualRentalTime = fixture.Rents + .Where(rent => rent.Bike.Model.Type == bikeType) + .Sum(rent => rent.RentalDuration); - Assert.Equal(expectedSportRentalTime, actualSportRentalTime); - Assert.Equal(expectedMountainRentaltime, actualMountainRentaltime); - Assert.Equal(expectedCityRentaltime, actualCityRentaltime); + Assert.Equal(expectedRentalTime, actualRentalTime); } /// @@ -131,7 +117,6 @@ public void TopThreeRenters() .Select(group => new { RenterId = group.Key, - RenterName = fixture.Renters.First(r => r.Id == group.Key), TotalRentals = group.Count() }) .OrderByDescending(r => r.TotalRentals) From cc14fcb3dba1e0a0559337fdf351109c0f00b695 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Oct 2025 17:02:53 +0400 Subject: [PATCH 14/41] Updated readme and comments --- Bikes/Bikes.Tests/BikesFixture.cs | 12 ++- README.md | 147 +++++------------------------- 2 files changed, 30 insertions(+), 129 deletions(-) diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 2f3823c06..d976ed5da 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -28,6 +28,9 @@ public class BikesFixture /// public readonly List Rents; + /// + /// Initializes a new instance of the BikesFixture class. + /// public BikesFixture() { BikeModels = InitializeBikeModels(); @@ -37,7 +40,7 @@ public BikesFixture() } /// - /// List of bike models + /// A method that initializes list of bike models /// private List InitializeBikeModels() { @@ -57,7 +60,7 @@ private List InitializeBikeModels() } /// - /// List of bikes + /// A method that initializes list of bikes /// private List InitializeBikes() { @@ -77,7 +80,7 @@ private List InitializeBikes() } /// - /// List of renters + /// A method that initializes list of renters /// private List InitializeRenters() { @@ -95,8 +98,9 @@ private List InitializeRenters() new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } ]; } + /// - /// List of rents + /// A method that initializes list of rents /// private List InitializeRents() { diff --git a/README.md b/README.md index 39c9a8443..d36ce50ef 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,33 @@ # Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) -## Задание -### Цель +# Цель Реализация проекта сервисно-ориентированного приложения. -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. +# Задание "Пункт велопроката" -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. +## Лабораторная работа 1 - "Классы" +В рамках первой лабораторной работы была добавлена доменная модель с основными сущностями пункта велопроката и реализованы юнит-тесты. -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 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. +### Классы +* [Bike](https://github.com/comandir26/enterprise-development/blob/main/Bikes/Bikes.Domain/Models/Bike.cs) - характеризует велосипед +* [BikeModel](https://github.com/comandir26/enterprise-development/blob/main/Bikes/Bikes.Domain/Models/BikeModel.cs) - информация о модели велосипеда +* [Renter](https://github.com/comandir26/enterprise-development/blob/main/Bikes/Bikes.Domain/Models/Renter.cs) - информация об арендаторе +* [Rent](https://github.com/comandir26/enterprise-development/blob/main/Bikes/Bikes.Domain/Models/Rent.cs) - информация об аренде велосипеда, класс содержит как велосипед, так и арендатора -Внимательно прочитайте [дискуссии](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 - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл - -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas). +### Тесты +[BikeTests](https://github.com/comandir26/enterprise-development/blob/main/Bikes/Bikes.Tests/BikesTests.cs) - юнит-тесты +1. InformationAboutSportBikes - Вывести информацию обо всех спортивных велосипедах. +2. TopFiveModelsProfit, TopFiveModelsRentDurationIds - Вывести топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). +3. MinMaxAvgRentalDuration - Вывести информацию о минимальном, максимальном и среднем времени аренды велосипедов. +4. TotalRentalTimeByType - Вывести суммарное время аренды велосипедов каждого типа. +5. TopThreeRenters - Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз. \ No newline at end of file From ad639de41b11ab672075ce2f070ee97fbbf41e1a Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 6 Oct 2025 19:21:24 +0400 Subject: [PATCH 15/41] Marked methods as static where needed --- Bikes/Bikes.Tests/BikesFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index d976ed5da..fbf9445e6 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -42,7 +42,7 @@ public BikesFixture() /// /// A method that initializes list of bike models /// - private List InitializeBikeModels() + private static List InitializeBikeModels() { return [ @@ -82,7 +82,7 @@ private List InitializeBikes() /// /// A method that initializes list of renters /// - private List InitializeRenters() + private static List InitializeRenters() { return [ From b9942556e4374799dd031e763c23f69e2246d575 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 29 Oct 2025 02:30:02 +0400 Subject: [PATCH 16/41] Add IRepository interface class for inmemory repo classes, also add Bikes.Infrastructure.InMemory project for repo classes --- .../Bikes.Domain/Repositories/IRepository.cs | 14 +++ .../Bikes.Infrastructure.InMemory.csproj | 14 +++ Bikes/Bikes.Infrastructure.InMemory/Class1.cs | 6 ++ .../InMemoryBikeModelRepository.cs | 10 ++ .../Repositories/InMemoryBikeRepository.cs | 56 +++++++++++ .../Repositories/InMemoryRentRepository.cs | 10 ++ .../Repositories/InMemoryRenterRepository.cs | 10 ++ .../Seeders/InMemorySeeder.cs | 94 +++++++++++++++++++ Bikes/Bikes.sln | 6 ++ 9 files changed, 220 insertions(+) create mode 100644 Bikes/Bikes.Domain/Repositories/IRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Class1.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs diff --git a/Bikes/Bikes.Domain/Repositories/IRepository.cs b/Bikes/Bikes.Domain/Repositories/IRepository.cs new file mode 100644 index 000000000..0d72a0927 --- /dev/null +++ b/Bikes/Bikes.Domain/Repositories/IRepository.cs @@ -0,0 +1,14 @@ +namespace Bikes.Domain.Repositories; + +public interface IRepository +{ + public TKey Create(TEntity entity); + + public List ReadAll(); + + public TEntity? Read(TKey id); + + public TEntity? Update(TKey id, TEntity entity); + + public bool Delete(TKey id); +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj b/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj new file mode 100644 index 000000000..6cacc3bdd --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Bikes/Bikes.Infrastructure.InMemory/Class1.cs b/Bikes/Bikes.Infrastructure.InMemory/Class1.cs new file mode 100644 index 000000000..775f07334 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Class1.cs @@ -0,0 +1,6 @@ +namespace Bikes.Infrastructure.InMemory; + +public class Class1 +{ + +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs new file mode 100644 index 000000000..ab0ef2db3 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bikes.Infrastructure.InMemory.Repositories; +internal class InMemoryBikeModelRepository +{ +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs new file mode 100644 index 000000000..df4b796c8 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs @@ -0,0 +1,56 @@ +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Seeders; + +namespace Bikes.Infrastructure.InMemory.Repositories; + +public class InMemoryBikeRepository : IRepository +{ + private readonly List _items = []; + + private int _currentId; + + public InMemoryBikeRepository() + { + _items = InMemorySeeder.GetBikes(); + _currentId = _items.Count > 0 ? _items.Count : 0; + } + + public int Create(Bike entity) + { + entity.Id = ++_currentId; + _items.Add(entity); + return entity.Id; + } + + public List ReadAll() + { + return _items; + } + + public Bike? Read(int id) + { + return _items.FirstOrDefault(b => b.Id == id); + } + + public Bike? Update(int id, Bike entity) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return null; + + exsitingEntity.SerialNumber = entity.SerialNumber; + exsitingEntity.Color = entity.Color; + exsitingEntity.Model = entity.Model; + + return exsitingEntity; + } + + public bool Delete(int id) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return false; + + _items.Remove(exsitingEntity); + return true; + } +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs new file mode 100644 index 000000000..df4070e8b --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bikes.Infrastructure.InMemory.Repositories; +internal class InMemoryRentRepository +{ +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs new file mode 100644 index 000000000..3052e629f --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bikes.Infrastructure.InMemory.Repositories; +internal class InMemoryRenterRepository +{ +} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs new file mode 100644 index 000000000..ddc6637c3 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs @@ -0,0 +1,94 @@ +using Bikes.Domain.Models; + +namespace Bikes.Infrastructure.InMemory.Seeders; + +public static class InMemorySeeder +{ + public static List GetBikeModels() + { + return + [ + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, + new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, + new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 }, + new() { Id = 4, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 125, Weight = 15, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 750 }, + new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, + new() { Id = 6, Type = BikeType.City, WheelSize = 27, MaxPassengerWeight = 135, Weight = 17, BrakeType = "Дисковые механические", Year = "2023", RentPrice = 550 }, + new() { Id = 7, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 13, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 800 }, + new() { Id = 8, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 10, BrakeType = "Ободные v-brake", Year = "2023", RentPrice = 950 }, + new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 600 }, + new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } + ]; + } + + public static List GetBikes() + { + var models = GetBikeModels(); + + return + [ + new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = models[0] }, + new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = models[1] }, + new() { Id = 3, SerialNumber = "CTY202203001", Color = "Синий", Model = models[2] }, + new() { Id = 4, SerialNumber = "MTB202302001", Color = "Зеленый", Model = models[3] }, + new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = models[4] }, + new() { Id = 6, SerialNumber = "CTY202304001", Color = "Белый", Model = models[5] }, + new() { Id = 7, SerialNumber = "MTB202404001", Color = "Оранжевый", Model = models[6] }, + new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = models[7] }, + new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", Model = models[8] }, + new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = models[9] } + ]; + } + + public static List GetRenters() + { + return + [ + new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, + new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, + new() { Id = 3, FullName = "Сидорова Анна Владимировна", Number = "+7 (934) 567-89-01" }, + new() { Id = 4, FullName = "Кузнецов Алексей Дмитриевич", Number = "+7 (945) 678-90-12" }, + new() { Id = 5, FullName = "Смирнова Екатерина Олеговна", Number = "+7 (956) 789-01-23" }, + new() { Id = 6, FullName = "Попов Денис Андреевич", Number = "+7 (967) 890-12-34" }, + new() { Id = 7, FullName = "Васильева Мария Игоревна", Number = "+7 (978) 901-23-45" }, + new() { Id = 8, FullName = "Николаев Сергей Викторович", Number = "+7 (989) 012-34-56" }, + new() { Id = 9, FullName = "Орлова Ольга Павловна", Number = "+7 (990) 123-45-67" }, + new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } + ]; + } + + /// + /// A method that initializes list of rents + /// + public static List GetRents() + { + var bikes = GetBikes(); + var renters = GetRenters(); + + return + [ + new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = renters[0], Bike = bikes[0] }, + new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = renters[1], Bike = bikes[0] }, + new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = renters[2], Bike = bikes[0] }, + new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = renters[3], Bike = bikes[1] }, + new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = renters[4], Bike = bikes[1] }, + new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = renters[5], Bike = bikes[1] }, + new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = renters[6], Bike = bikes[2] }, + new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = renters[7], Bike = bikes[2] }, + new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = renters[8], Bike = bikes[3] }, + new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = renters[9], Bike = bikes[3] }, + new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = renters[0], Bike = bikes[4] }, + new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = renters[0], Bike = bikes[4] }, + new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = renters[0], Bike = bikes[5] }, + new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = renters[0], Bike = bikes[6] }, + new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = renters[1], Bike = bikes[7] }, + new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = renters[1], Bike = bikes[8] }, + new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = renters[1], Bike = bikes[9] }, + new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = renters[5], Bike = bikes[9] }, + new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = renters[5], Bike = bikes[9] }, + new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = renters[2], Bike = bikes[9] } + ]; + } + +} + diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index bdf5353bf..bc3545ab4 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Domain", "Bikes.Domai EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{54F042E8-6681-4802-B300-ADFE25207ACB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.InMemory", "Bikes.Infrastructure.InMemory\Bikes.Infrastructure.InMemory.csproj", "{610AD524-2BD4-429B-AAC9-D5AD48B8C50D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {54F042E8-6681-4802-B300-ADFE25207ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU {54F042E8-6681-4802-B300-ADFE25207ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU {54F042E8-6681-4802-B300-ADFE25207ACB}.Release|Any CPU.Build.0 = Release|Any CPU + {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c16fb279717760d328e52bda5aa13d4a624a6d90 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 9 Nov 2025 22:36:55 +0400 Subject: [PATCH 17/41] Implement inmemory repo classess --- .../Bikes.Application.csproj | 14 +++++ Bikes/Bikes.Infrastructure.InMemory/Class1.cs | 6 -- .../InMemoryBikeModelRepository.cs | 62 +++++++++++++++++-- .../Repositories/InMemoryRentRepository.cs | 59 ++++++++++++++++-- .../Repositories/InMemoryRenterRepository.cs | 57 +++++++++++++++-- Bikes/Bikes.sln | 9 +++ 6 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 Bikes/Bikes.Application/Bikes.Application.csproj delete mode 100644 Bikes/Bikes.Infrastructure.InMemory/Class1.cs diff --git a/Bikes/Bikes.Application/Bikes.Application.csproj b/Bikes/Bikes.Application/Bikes.Application.csproj new file mode 100644 index 000000000..9041d8392 --- /dev/null +++ b/Bikes/Bikes.Application/Bikes.Application.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Bikes/Bikes.Infrastructure.InMemory/Class1.cs b/Bikes/Bikes.Infrastructure.InMemory/Class1.cs deleted file mode 100644 index 775f07334..000000000 --- a/Bikes/Bikes.Infrastructure.InMemory/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bikes.Infrastructure.InMemory; - -public class Class1 -{ - -} diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs index ab0ef2db3..21c49f4c6 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs @@ -1,10 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Seeders; namespace Bikes.Infrastructure.InMemory.Repositories; -internal class InMemoryBikeModelRepository + +public class InMemoryBikeModelRepository : IRepository { + private readonly List _items = []; + + private int _currentId; + + public InMemoryBikeModelRepository() + { + _items = InMemorySeeder.GetBikeModels(); + _currentId = _items.Count > 0 ? _items.Count : 0; + } + + public int Create(BikeModel entity) + { + entity.Id = ++_currentId; + _items.Add(entity); + return entity.Id; + } + + public List ReadAll() + { + return _items; + } + + public BikeModel? Read(int id) + { + return _items.FirstOrDefault(b => b.Id == id); + } + + public BikeModel? Update(int id, BikeModel entity) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return null; + + exsitingEntity.Type = entity.Type; + exsitingEntity.WheelSize = entity.WheelSize; + exsitingEntity.MaxPassengerWeight = entity.MaxPassengerWeight; + exsitingEntity.Weight = entity.Weight; + exsitingEntity.BrakeType = entity.BrakeType; + exsitingEntity.Year = entity.Year; + exsitingEntity.RentPrice = entity.RentPrice; + + return exsitingEntity; + } + + public bool Delete(int id) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return false; + + _items.Remove(exsitingEntity); + return true; + } } diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs index df4070e8b..ea1e521b9 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs @@ -1,10 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Seeders; namespace Bikes.Infrastructure.InMemory.Repositories; -internal class InMemoryRentRepository + +public class InMemoryRentRepository : IRepository { + private readonly List _items = []; + + private int _currentId; + + public InMemoryRentRepository() + { + _items = InMemorySeeder.GetRents(); + _currentId = _items.Count > 0 ? _items.Count : 0; + } + + public int Create(Rent entity) + { + entity.Id = ++_currentId; + _items.Add(entity); + return entity.Id; + } + + public List ReadAll() + { + return _items; + } + + public Rent? Read(int id) + { + return _items.FirstOrDefault(b => b.Id == id); + } + + public Rent? Update(int id, Rent entity) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return null; + + exsitingEntity.RentalStartTime = entity.RentalStartTime; + exsitingEntity.RentalDuration = entity.RentalDuration; + exsitingEntity.Renter = entity.Renter; + exsitingEntity.Bike = entity.Bike; + + return exsitingEntity; + } + + public bool Delete(int id) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return false; + + _items.Remove(exsitingEntity); + return true; + } } diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs index 3052e629f..6dbfd7a5e 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs @@ -1,10 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Seeders; namespace Bikes.Infrastructure.InMemory.Repositories; -internal class InMemoryRenterRepository + +public class InMemoryRenterRepository : IRepository { + private readonly List _items = []; + + private int _currentId; + + public InMemoryRenterRepository() + { + _items = InMemorySeeder.GetRenters(); + _currentId = _items.Count > 0 ? _items.Count : 0; + } + + public int Create(Renter entity) + { + entity.Id = ++_currentId; + _items.Add(entity); + return entity.Id; + } + + public List ReadAll() + { + return _items; + } + + public Renter? Read(int id) + { + return _items.FirstOrDefault(b => b.Id == id); + } + + public Renter? Update(int id, Renter entity) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return null; + + exsitingEntity.FullName = entity.FullName; + exsitingEntity.Number = entity.Number; + + return exsitingEntity; + } + + public bool Delete(int id) + { + var exsitingEntity = Read(id); + if (exsitingEntity == null) return false; + + _items.Remove(exsitingEntity); + return true; + } } diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index bc3545ab4..f25e50730 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -9,6 +9,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Tests", "Bikes.Tests\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.InMemory", "Bikes.Infrastructure.InMemory\Bikes.Infrastructure.InMemory.csproj", "{610AD524-2BD4-429B-AAC9-D5AD48B8C50D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes.Application\Bikes.Application.csproj", "{619487B4-C5C4-4396-B2DF-18B83CD522CE}" + ProjectSection(ProjectDependencies) = postProject + {B6E3E827-ADA9-4B0E-B704-484513A051A8} = {B6E3E827-ADA9-4B0E-B704-484513A051A8} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +32,10 @@ Global {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Debug|Any CPU.Build.0 = Debug|Any CPU {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Release|Any CPU.ActiveCfg = Release|Any CPU {610AD524-2BD4-429B-AAC9-D5AD48B8C50D}.Release|Any CPU.Build.0 = Release|Any CPU + {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From cdc9362607419ab8de352111f748f9e58fca7aa2 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 10 Nov 2025 01:01:17 +0400 Subject: [PATCH 18/41] Add DTO classes, add interfaces for service classes and their implementation --- .../Bikes.Application.csproj | 3 +- Bikes/Bikes.Application/Dto/BikeDto.cs | 8 ++ Bikes/Bikes.Application/Dto/BikeModelDto.cs | 14 +++ Bikes/Bikes.Application/Dto/RentDto.cs | 9 ++ Bikes/Bikes.Application/Dto/RenterDto.cs | 7 ++ .../Services/AnalyticsService.cs | 109 ++++++++++++++++++ .../Services/BikeModelService.cs | 51 ++++++++ .../Bikes.Application/Services/BikeService.cs | 58 ++++++++++ .../Services/IAnalyticsService.cs | 13 +++ .../Services/IBikeModelService.cs | 13 +++ .../Services/IBikeService.cs | 13 +++ .../Services/IRentService.cs | 13 +++ .../Services/IRenterService.cs | 13 +++ .../Bikes.Application/Services/RentService.cs | 70 +++++++++++ .../Services/RenterService.cs | 46 ++++++++ 15 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 Bikes/Bikes.Application/Dto/BikeDto.cs create mode 100644 Bikes/Bikes.Application/Dto/BikeModelDto.cs create mode 100644 Bikes/Bikes.Application/Dto/RentDto.cs create mode 100644 Bikes/Bikes.Application/Dto/RenterDto.cs create mode 100644 Bikes/Bikes.Application/Services/AnalyticsService.cs create mode 100644 Bikes/Bikes.Application/Services/BikeModelService.cs create mode 100644 Bikes/Bikes.Application/Services/BikeService.cs create mode 100644 Bikes/Bikes.Application/Services/IAnalyticsService.cs create mode 100644 Bikes/Bikes.Application/Services/IBikeModelService.cs create mode 100644 Bikes/Bikes.Application/Services/IBikeService.cs create mode 100644 Bikes/Bikes.Application/Services/IRentService.cs create mode 100644 Bikes/Bikes.Application/Services/IRenterService.cs create mode 100644 Bikes/Bikes.Application/Services/RentService.cs create mode 100644 Bikes/Bikes.Application/Services/RenterService.cs diff --git a/Bikes/Bikes.Application/Bikes.Application.csproj b/Bikes/Bikes.Application/Bikes.Application.csproj index 9041d8392..30da6d385 100644 --- a/Bikes/Bikes.Application/Bikes.Application.csproj +++ b/Bikes/Bikes.Application/Bikes.Application.csproj @@ -7,8 +7,7 @@ - - + diff --git a/Bikes/Bikes.Application/Dto/BikeDto.cs b/Bikes/Bikes.Application/Dto/BikeDto.cs new file mode 100644 index 000000000..0562b39fe --- /dev/null +++ b/Bikes/Bikes.Application/Dto/BikeDto.cs @@ -0,0 +1,8 @@ +namespace Bikes.Application.Dto; + +public class BikeDto +{ + public required string SerialNumber { get; set; } + public required string Color { get; set; } + public required int ModelId { get; set; } +} diff --git a/Bikes/Bikes.Application/Dto/BikeModelDto.cs b/Bikes/Bikes.Application/Dto/BikeModelDto.cs new file mode 100644 index 000000000..84428c5a9 --- /dev/null +++ b/Bikes/Bikes.Application/Dto/BikeModelDto.cs @@ -0,0 +1,14 @@ +using Bikes.Domain.Models; + +namespace Bikes.Application.Dto; + +public class BikeModelDto +{ + public required BikeType Type { get; set; } + public required int WheelSize { get; set; } + public required int MaxPassengerWeight { get; set; } + public required int Weight { get; set; } + public required string BrakeType { get; set; } + public required string Year { get; set; } + public required int RentPrice { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Dto/RentDto.cs b/Bikes/Bikes.Application/Dto/RentDto.cs new file mode 100644 index 000000000..4b393a477 --- /dev/null +++ b/Bikes/Bikes.Application/Dto/RentDto.cs @@ -0,0 +1,9 @@ +namespace Bikes.Application.Dto; + +public class RentDto +{ + public required DateTime RentalStartTime { get; set; } + public required int RentalDuration { get; set; } + public required int RenterId { get; set; } + public required int BikeId { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Dto/RenterDto.cs b/Bikes/Bikes.Application/Dto/RenterDto.cs new file mode 100644 index 000000000..31c07edf0 --- /dev/null +++ b/Bikes/Bikes.Application/Dto/RenterDto.cs @@ -0,0 +1,7 @@ +namespace Bikes.Application.Dto; + +public class RenterDto +{ + public required string FullName { get; set; } + public required string Number { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs new file mode 100644 index 000000000..85d7f7969 --- /dev/null +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -0,0 +1,109 @@ +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Application.Services; + +public class AnalyticsService : IAnalyticsService +{ + private readonly IRepository _bikeRepository; + private readonly IRepository _bikeModelRepository; + private readonly IRepository _rentRepository; + private readonly IRepository _renterRepository; + + public AnalyticsService( + IRepository bikeRepository, + IRepository bikeModelRepository, + IRepository rentRepository, + IRepository renterRepository) + { + _bikeRepository = bikeRepository; + _bikeModelRepository = bikeModelRepository; + _rentRepository = rentRepository; + _renterRepository = renterRepository; + } + + public List GetSportBikes() + { + return _bikeRepository.ReadAll() + .Where(bike => bike.Model.Type == BikeType.Sport) + .ToList(); + } + + public List GetTopFiveModelsByRentDuration() + { + var rents = _rentRepository.ReadAll(); + var models = _bikeModelRepository.ReadAll(); + + return rents + .GroupBy(rent => rent.Bike.Model.Id) + .Select(group => new + { + ModelId = group.Key, + TotalDuration = group.Sum(rent => rent.RentalDuration) + }) + .OrderByDescending(x => x.TotalDuration) + .Take(5) + .Join(models, + x => x.ModelId, + model => model.Id, + (x, model) => model) + .ToList(); + } + + public List GetTopFiveModelsByProfit() + { + var rents = _rentRepository.ReadAll(); + var models = _bikeModelRepository.ReadAll(); + + return rents + .GroupBy(rent => rent.Bike.Model.Id) + .Select(group => new + { + ModelId = group.Key, + TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) + }) + .OrderByDescending(x => x.TotalProfit) + .Take(5) + .Join(models, x => x.ModelId, model => model.Id, (x, model) => model) + .ToList(); + } + + public (int min, int max, double avg) GetRentalDurationStats() + { + var durations = _rentRepository.ReadAll() + .Select(rent => rent.RentalDuration) + .ToList(); + + return (durations.Min(), durations.Max(), durations.Average()); + } + + public Dictionary GetTotalRentalTimeByType() + { + return _rentRepository.ReadAll() + .GroupBy(rent => rent.Bike.Model.Type) + .ToDictionary( + group => group.Key, + group => group.Sum(rent => rent.RentalDuration) + ); + } + + public List GetTopThreeRenters() + { + var renters = _renterRepository.ReadAll(); + + return _rentRepository.ReadAll() + .GroupBy(rent => rent.Renter.Id) + .Select(group => new + { + RenterId = group.Key, + TotalRentals = group.Count() + }) + .OrderByDescending(r => r.TotalRentals) + .Take(3) + .Join(renters, + x => x.RenterId, + renter => renter.Id, + (x, renter) => renter) + .ToList(); + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeModelService.cs b/Bikes/Bikes.Application/Services/BikeModelService.cs new file mode 100644 index 000000000..528bd2032 --- /dev/null +++ b/Bikes/Bikes.Application/Services/BikeModelService.cs @@ -0,0 +1,51 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Application.Services; + +public class BikeModelService : IBikeModelService +{ + private readonly IRepository _bikeModelRepository; + + public BikeModelService(IRepository bikeModelRepository) + { + _bikeModelRepository = bikeModelRepository; + } + + private static BikeModel MapToDomain(BikeModelDto dto, int id = 0) + { + return new BikeModel + { + Id = id, + Type = dto.Type, + WheelSize = dto.WheelSize, + MaxPassengerWeight = dto.MaxPassengerWeight, + Weight = dto.Weight, + BrakeType = dto.BrakeType, + Year = dto.Year, + RentPrice = dto.RentPrice + }; + } + + public int CreateBikeModel(BikeModelDto bikeModelDto) + { + var bikeModel = MapToDomain(bikeModelDto); + return _bikeModelRepository.Create(bikeModel); + } + + public List GetAllBikeModels() => _bikeModelRepository.ReadAll(); + + public BikeModel? GetBikeModelById(int id) => _bikeModelRepository.Read(id); + + public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto) + { + var existingModel = _bikeModelRepository.Read(id); + if (existingModel == null) return null; + + var updatedModel = MapToDomain(bikeModelDto, id); + return _bikeModelRepository.Update(id, updatedModel); + } + + public bool DeleteBikeModel(int id) => _bikeModelRepository.Delete(id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs new file mode 100644 index 000000000..6b9129ac8 --- /dev/null +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -0,0 +1,58 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Application.Services; + +public class BikeService : IBikeService +{ + private readonly IRepository _bikeRepository; + private readonly IRepository _bikeModelRepository; + + public BikeService( + IRepository bikeRepository, + IRepository bikeModelRepository) + { + _bikeRepository = bikeRepository; + _bikeModelRepository = bikeModelRepository; + } + + private static Bike MapToDomain(BikeDto dto, BikeModel model, int id = 0) + { + return new Bike + { + Id = id, + SerialNumber = dto.SerialNumber, + Color = dto.Color, + Model = model + }; + } + + public int CreateBike(BikeDto bikeDto) + { + var model = _bikeModelRepository.Read(bikeDto.ModelId); + if (model == null) + throw new ArgumentException($"BikeModel with id {bikeDto.ModelId} not found"); + + var bike = MapToDomain(bikeDto, model); + return _bikeRepository.Create(bike); + } + + public List GetAllBikes() => _bikeRepository.ReadAll(); + + public Bike? GetBikeById(int id) => _bikeRepository.Read(id); + + public Bike? UpdateBike(int id, BikeDto bikeDto) + { + var existingBike = _bikeRepository.Read(id); + if (existingBike == null) return null; + + var model = _bikeModelRepository.Read(bikeDto.ModelId); + if (model == null) return null; + + var updatedBike = MapToDomain(bikeDto, model, id); + return _bikeRepository.Update(id, updatedBike); + } + + public bool DeleteBike(int id) => _bikeRepository.Delete(id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IAnalyticsService.cs b/Bikes/Bikes.Application/Services/IAnalyticsService.cs new file mode 100644 index 000000000..1ae40810d --- /dev/null +++ b/Bikes/Bikes.Application/Services/IAnalyticsService.cs @@ -0,0 +1,13 @@ +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +public interface IAnalyticsService +{ + public List GetSportBikes(); + public List GetTopFiveModelsByRentDuration(); + public List GetTopFiveModelsByProfit(); + public (int min, int max, double avg) GetRentalDurationStats(); + public Dictionary GetTotalRentalTimeByType(); + public List GetTopThreeRenters(); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeModelService.cs b/Bikes/Bikes.Application/Services/IBikeModelService.cs new file mode 100644 index 000000000..e63b79f56 --- /dev/null +++ b/Bikes/Bikes.Application/Services/IBikeModelService.cs @@ -0,0 +1,13 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +public interface IBikeModelService +{ + public int CreateBikeModel(BikeModelDto bikeModelDto); + public List GetAllBikeModels(); + public BikeModel? GetBikeModelById(int id); + public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto); + public bool DeleteBikeModel(int id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeService.cs b/Bikes/Bikes.Application/Services/IBikeService.cs new file mode 100644 index 000000000..cbe49c568 --- /dev/null +++ b/Bikes/Bikes.Application/Services/IBikeService.cs @@ -0,0 +1,13 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +public interface IBikeService +{ + public int CreateBike(BikeDto bikeDto); + public List GetAllBikes(); + public Bike? GetBikeById(int id); + public Bike? UpdateBike(int id, BikeDto bikeDto); + public bool DeleteBike(int id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IRentService.cs b/Bikes/Bikes.Application/Services/IRentService.cs new file mode 100644 index 000000000..59775741b --- /dev/null +++ b/Bikes/Bikes.Application/Services/IRentService.cs @@ -0,0 +1,13 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +public interface IRentService +{ + public int CreateRent(RentDto rentDto); + public List GetAllRents(); + public Rent? GetRentById(int id); + public Rent? UpdateRent(int id, RentDto rentDto); + public bool DeleteRent(int id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IRenterService.cs b/Bikes/Bikes.Application/Services/IRenterService.cs new file mode 100644 index 000000000..fe5283ca0 --- /dev/null +++ b/Bikes/Bikes.Application/Services/IRenterService.cs @@ -0,0 +1,13 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +public interface IRenterService +{ + public int CreateRenter(RenterDto renterDto); + public List GetAllRenters(); + public Renter? GetRenterById(int id); + public Renter? UpdateRenter(int id, RenterDto renterDto); + public bool DeleteRenter(int id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs new file mode 100644 index 000000000..065d505c4 --- /dev/null +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -0,0 +1,70 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Application.Services; + +public class RentService : IRentService +{ + private readonly IRepository _rentRepository; + private readonly IRepository _bikeRepository; + private readonly IRepository _renterRepository; + + public RentService( + IRepository rentRepository, + IRepository bikeRepository, + IRepository renterRepository) + { + _rentRepository = rentRepository; + _bikeRepository = bikeRepository; + _renterRepository = renterRepository; + } + + + private static Rent MapToDomain(RentDto dto, Bike bike, Renter renter, int id = 0) + { + return new Rent + { + Id = id, + RentalStartTime = dto.RentalStartTime, + RentalDuration = dto.RentalDuration, + Bike = bike, + Renter = renter + }; + } + + public int CreateRent(RentDto rentDto) + { + var bike = _bikeRepository.Read(rentDto.BikeId); + if (bike == null) + throw new ArgumentException($"Bike with id {rentDto.BikeId} not found"); + + var renter = _renterRepository.Read(rentDto.RenterId); + if (renter == null) + throw new ArgumentException($"Renter with id {rentDto.RenterId} not found"); + + var rent = MapToDomain(rentDto, bike, renter); + return _rentRepository.Create(rent); + } + + public List GetAllRents() => _rentRepository.ReadAll(); + + public Rent? GetRentById(int id) => _rentRepository.Read(id); + + public Rent? UpdateRent(int id, RentDto rentDto) + { + var existingRent = _rentRepository.Read(id); + if (existingRent == null) return null; + + var bike = _bikeRepository.Read(rentDto.BikeId); + if (bike == null) return null; + + var renter = _renterRepository.Read(rentDto.RenterId); + if (renter == null) return null; + + var updatedRent = MapToDomain(rentDto, bike, renter, id); + return _rentRepository.Update(id, updatedRent); + } + + public bool DeleteRent(int id) => _rentRepository.Delete(id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/RenterService.cs b/Bikes/Bikes.Application/Services/RenterService.cs new file mode 100644 index 000000000..20cbd5f9a --- /dev/null +++ b/Bikes/Bikes.Application/Services/RenterService.cs @@ -0,0 +1,46 @@ +using Bikes.Application.Dto; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Application.Services; + +public class RenterService : IRenterService +{ + private readonly IRepository _renterRepository; + + public RenterService(IRepository renterRepository) + { + _renterRepository = renterRepository; + } + + private static Renter MapToDomain(RenterDto dto, int id = 0) + { + return new Renter + { + Id = id, + FullName = dto.FullName, + Number = dto.Number + }; + } + + public int CreateRenter(RenterDto renterDto) + { + var renter = MapToDomain(renterDto); + return _renterRepository.Create(renter); + } + + public List GetAllRenters() => _renterRepository.ReadAll(); + + public Renter? GetRenterById(int id) => _renterRepository.Read(id); + + public Renter? UpdateRenter(int id, RenterDto renterDto) + { + var existingRenter = _renterRepository.Read(id); + if (existingRenter == null) return null; + + var updatedRenter = MapToDomain(renterDto, id); + return _renterRepository.Update(id, updatedRenter); + } + + public bool DeleteRenter(int id) => _renterRepository.Delete(id); +} From 77d50335764fbf396eb94670078ce72b667fdae6 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 11 Nov 2025 00:16:59 +0400 Subject: [PATCH 19/41] Add Bikes.Api.Host project and comments --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 18 +++ .../Controllers/AnalyticsController.cs | 117 +++++++++++++++ .../Controllers/BikeModelsController.cs | 128 ++++++++++++++++ .../Controllers/BikesController.cs | 140 ++++++++++++++++++ .../Controllers/RentersController.cs | 128 ++++++++++++++++ .../Controllers/RentsController.cs | 139 +++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 24 +++ Bikes/Bikes.Api.Host/Program.cs | 25 ++++ .../Properties/launchSettings.json | 41 +++++ .../appsettings.Development.json | 8 + Bikes/Bikes.Api.Host/appsettings.json | 9 ++ Bikes/Bikes.Application/Dto/BikeDto.cs | 14 ++ Bikes/Bikes.Application/Dto/BikeModelDto.cs | 30 ++++ Bikes/Bikes.Application/Dto/RentDto.cs | 18 +++ Bikes/Bikes.Application/Dto/RenterDto.cs | 10 ++ .../Services/AnalyticsService.cs | 28 ++++ .../Services/BikeModelService.cs | 38 +++++ .../Bikes.Application/Services/BikeService.cs | 40 +++++ .../Services/IAnalyticsService.cs | 26 ++++ .../Services/IBikeModelService.cs | 32 ++++ .../Services/IBikeService.cs | 32 ++++ .../Services/IRentService.cs | 32 ++++ .../Services/IRenterService.cs | 32 ++++ .../Bikes.Application/Services/RentService.cs | 44 +++++- .../Services/RenterService.cs | 38 +++++ .../Bikes.Domain/Repositories/IRepository.cs | 30 ++++ .../InMemoryBikeModelRepository.cs | 31 ++++ .../Repositories/InMemoryBikeRepository.cs | 31 ++++ .../Repositories/InMemoryRentRepository.cs | 31 ++++ .../Repositories/InMemoryRenterRepository.cs | 31 ++++ .../Seeders/InMemorySeeder.cs | 14 +- Bikes/Bikes.Tests/BikesFixture.cs | 1 - Bikes/Bikes.sln | 6 + 33 files changed, 1363 insertions(+), 3 deletions(-) create mode 100644 Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj create mode 100644 Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs create mode 100644 Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs create mode 100644 Bikes/Bikes.Api.Host/Controllers/BikesController.cs create mode 100644 Bikes/Bikes.Api.Host/Controllers/RentersController.cs create mode 100644 Bikes/Bikes.Api.Host/Controllers/RentsController.cs create mode 100644 Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs create mode 100644 Bikes/Bikes.Api.Host/Program.cs create mode 100644 Bikes/Bikes.Api.Host/Properties/launchSettings.json create mode 100644 Bikes/Bikes.Api.Host/appsettings.Development.json create mode 100644 Bikes/Bikes.Api.Host/appsettings.json diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj new file mode 100644 index 000000000..5016e3ad7 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..9223b9ed8 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -0,0 +1,117 @@ +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AnalyticsController(IAnalyticsService service, ILogger logger) : ControllerBase +{ + [HttpGet("sport-bikes")] + public IActionResult GetSportBikes() + { + try + { + logger.LogInformation("Getting sport bikes"); + var bikes = service.GetSportBikes(); + logger.LogInformation("Retrieved {Count} sport bikes", bikes.Count); + return Ok(bikes); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting sport bikes"); + return StatusCode(500, new { error = "An error occurred while retrieving sport bikes." }); + } + } + + [HttpGet("top-models/duration")] + public IActionResult GetTopModelsByDuration() + { + try + { + logger.LogInformation("Getting top models by rent duration"); + var models = service.GetTopFiveModelsByRentDuration(); + logger.LogInformation("Retrieved top {Count} models by duration", models.Count); + return Ok(models); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting top models by duration"); + return StatusCode(500, new { error = "An error occurred while retrieving top models by duration." }); + } + } + + [HttpGet("top-models/profit")] + public IActionResult GetTopModelsByProfit() + { + try + { + logger.LogInformation("Getting top models by profit"); + var models = service.GetTopFiveModelsByProfit(); + logger.LogInformation("Retrieved top {Count} models by profit", models.Count); + return Ok(models); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting top models by profit"); + return StatusCode(500, new { error = "An error occurred while retrieving top models by profit." }); + } + } + + [HttpGet("stats/duration")] + public IActionResult GetRentalDurationStats() + { + try + { + logger.LogInformation("Getting rental duration statistics"); + var stats = service.GetRentalDurationStats(); + logger.LogInformation("Retrieved rental duration stats: Min={Min}, Max={Max}, Avg={Avg}", + stats.min, stats.max, stats.avg); + return Ok(new + { + min = stats.min, + max = stats.max, + average = stats.avg + }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting rental duration statistics"); + return StatusCode(500, new { error = "An error occurred while retrieving rental duration statistics." }); + } + } + + [HttpGet("stats/rental-time-by-type")] + public IActionResult GetTotalRentalTimeByType() + { + try + { + logger.LogInformation("Getting total rental time by bike type"); + var stats = service.GetTotalRentalTimeByType(); + logger.LogInformation("Retrieved rental time by type for {Count} bike types", stats.Count); + return Ok(stats); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting total rental time by type"); + return StatusCode(500, new { error = "An error occurred while retrieving total rental time by type." }); + } + } + + [HttpGet("top-renters")] + public IActionResult GetTopRenters() + { + try + { + logger.LogInformation("Getting top renters"); + var renters = service.GetTopThreeRenters(); + logger.LogInformation("Retrieved top {Count} renters", renters.Count); + return Ok(renters); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting top renters"); + return StatusCode(500, new { error = "An error occurred while retrieving top renters." }); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs new file mode 100644 index 000000000..c5deba11e --- /dev/null +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -0,0 +1,128 @@ +using Bikes.Application.Dto; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BikeModelsController(IBikeModelService service, ILogger logger) : ControllerBase +{ + [HttpGet] + public IActionResult GetAllBikeModels() + { + try + { + logger.LogInformation("Getting all bike models"); + var models = service.GetAllBikeModels(); + logger.LogInformation("Retrieved {Count} bike models", models.Count); + return Ok(models); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting all bike models"); + return StatusCode(500, new { error = "An error occurred while retrieving bike models." }); + } + } + + [HttpGet("{id:int}")] + public IActionResult GetBikeModel(int id) + { + try + { + logger.LogInformation("Getting bike model with ID {ModelId}", id); + var model = service.GetBikeModelById(id); + + if (model == null) + { + logger.LogWarning("Bike model with ID {ModelId} not found", id); + return NotFound(new { error = $"Bike model with ID {id} not found." }); + } + + return Ok(model); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting bike model with ID {ModelId}", id); + return StatusCode(500, new { error = "An error occurred while retrieving the bike model." }); + } + } + + [HttpPost] + public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) + { + try + { + logger.LogInformation("Creating new bike model of type {BikeType}", bikeModelDto.Type); + + if (!ModelState.IsValid) + { + logger.LogWarning("Invalid bike model data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var id = service.CreateBikeModel(bikeModelDto); + logger.LogInformation("Created bike model with ID {ModelId}", id); + + return CreatedAtAction(nameof(GetBikeModel), new { id }, new { id, message = "Bike model created successfully." }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating bike model"); + return StatusCode(500, new { error = "An error occurred while creating the bike model." }); + } + } + + [HttpPut("{id:int}")] + public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDto) + { + try + { + logger.LogInformation("Updating bike model with ID {ModelId}", id); + + if (!ModelState.IsValid) + { + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var model = service.UpdateBikeModel(id, bikeModelDto); + if (model == null) + { + logger.LogWarning("Bike model with ID {ModelId} not found for update", id); + return NotFound(new { error = $"Bike model with ID {id} not found." }); + } + + logger.LogInformation("Updated bike model with ID {ModelId}", id); + return Ok(new { message = "Bike model updated successfully.", model }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating bike model with ID {ModelId}", id); + return StatusCode(500, new { error = "An error occurred while updating the bike model." }); + } + } + + [HttpDelete("{id:int}")] + public IActionResult DeleteBikeModel(int id) + { + try + { + logger.LogInformation("Deleting bike model with ID {ModelId}", id); + var result = service.DeleteBikeModel(id); + + if (!result) + { + logger.LogWarning("Bike model with ID {ModelId} not found for deletion", id); + return NotFound(new { error = $"Bike model with ID {id} not found." }); + } + + logger.LogInformation("Deleted bike model with ID {ModelId}", id); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting bike model with ID {ModelId}", id); + return StatusCode(500, new { error = "An error occurred while deleting the bike model." }); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs new file mode 100644 index 000000000..3cd5a4133 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -0,0 +1,140 @@ +using Bikes.Application.Dto; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BikesController(IBikeService service, ILogger logger) : ControllerBase +{ + [HttpGet] + public IActionResult GetAllBikes() + { + try + { + logger.LogInformation("Getting all bikes"); + var bikes = service.GetAllBikes(); + logger.LogInformation("Retrieved {Count} bikes", bikes.Count); + return Ok(bikes); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting all bikes"); + return StatusCode(500, new { error = "An error occurred while retrieving bikes." }); + } + } + + [HttpGet("{id:int}")] + public IActionResult GetBike(int id) + { + try + { + logger.LogInformation("Getting bike with ID {BikeId}", id); + var bike = service.GetBikeById(id); + + if (bike == null) + { + logger.LogWarning("Bike with ID {BikeId} not found", id); + return NotFound(new { error = $"Bike with ID {id} not found." }); + } + + logger.LogInformation("Retrieved bike with ID {BikeId}", id); + return Ok(bike); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting bike with ID {BikeId}", id); + return StatusCode(500, new { error = "An error occurred while retrieving the bike." }); + } + } + + [HttpPost] + public IActionResult CreateBike([FromBody] BikeDto bikeDto) + { + try + { + logger.LogInformation("Creating new bike with serial number {SerialNumber}", bikeDto.SerialNumber); + + if (!ModelState.IsValid) + { + logger.LogWarning("Invalid bike data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var id = service.CreateBike(bikeDto); + logger.LogInformation("Created bike with ID {BikeId}", id); + + return CreatedAtAction(nameof(GetBike), new { id }, new { id, message = "Bike created successfully." }); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error creating bike: {ErrorMessage}", ex.Message); + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating bike"); + return StatusCode(500, new { error = "An error occurred while creating the bike." }); + } + } + + [HttpPut("{id:int}")] + public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) + { + try + { + logger.LogInformation("Updating bike with ID {BikeId}", id); + + if (!ModelState.IsValid) + { + logger.LogWarning("Invalid bike data for update: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var bike = service.UpdateBike(id, bikeDto); + if (bike == null) + { + logger.LogWarning("Bike with ID {BikeId} not found for update", id); + return NotFound(new { error = $"Bike with ID {id} not found." }); + } + + logger.LogInformation("Updated bike with ID {BikeId}", id); + return Ok(new { message = "Bike updated successfully.", bike }); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error updating bike: {ErrorMessage}", ex.Message); + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating bike with ID {BikeId}", id); + return StatusCode(500, new { error = "An error occurred while updating the bike." }); + } + } + + [HttpDelete("{id:int}")] + public IActionResult DeleteBike(int id) + { + try + { + logger.LogInformation("Deleting bike with ID {BikeId}", id); + var result = service.DeleteBike(id); + + if (!result) + { + logger.LogWarning("Bike with ID {BikeId} not found for deletion", id); + return NotFound(new { error = $"Bike with ID {id} not found." }); + } + + logger.LogInformation("Deleted bike with ID {BikeId}", id); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting bike with ID {BikeId}", id); + return StatusCode(500, new { error = "An error occurred while deleting the bike." }); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs new file mode 100644 index 000000000..b2eff5e10 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -0,0 +1,128 @@ +using Bikes.Application.Dto; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RentersController(IRenterService service, ILogger logger) : ControllerBase +{ + [HttpGet] + public IActionResult GetAllRenters() + { + try + { + logger.LogInformation("Getting all renters"); + var renters = service.GetAllRenters(); + logger.LogInformation("Retrieved {Count} renters", renters.Count); + return Ok(renters); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting all renters"); + return StatusCode(500, new { error = "An error occurred while retrieving renters." }); + } + } + + [HttpGet("{id:int}")] + public IActionResult GetRenter(int id) + { + try + { + logger.LogInformation("Getting renter with ID {RenterId}", id); + var renter = service.GetRenterById(id); + + if (renter == null) + { + logger.LogWarning("Renter with ID {RenterId} not found", id); + return NotFound(new { error = $"Renter with ID {id} not found." }); + } + + return Ok(renter); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting renter with ID {RenterId}", id); + return StatusCode(500, new { error = "An error occurred while retrieving the renter." }); + } + } + + [HttpPost] + public IActionResult CreateRenter([FromBody] RenterDto renterDto) + { + try + { + logger.LogInformation("Creating new renter: {FullName}", renterDto.FullName); + + if (!ModelState.IsValid) + { + logger.LogWarning("Invalid renter data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var id = service.CreateRenter(renterDto); + logger.LogInformation("Created renter with ID {RenterId}", id); + + return CreatedAtAction(nameof(GetRenter), new { id }, new { id, message = "Renter created successfully." }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating renter"); + return StatusCode(500, new { error = "An error occurred while creating the renter." }); + } + } + + [HttpPut("{id:int}")] + public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) + { + try + { + logger.LogInformation("Updating renter with ID {RenterId}", id); + + if (!ModelState.IsValid) + { + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var renter = service.UpdateRenter(id, renterDto); + if (renter == null) + { + logger.LogWarning("Renter with ID {RenterId} not found for update", id); + return NotFound(new { error = $"Renter with ID {id} not found." }); + } + + logger.LogInformation("Updated renter with ID {RenterId}", id); + return Ok(new { message = "Renter updated successfully.", renter }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating renter with ID {RenterId}", id); + return StatusCode(500, new { error = "An error occurred while updating the renter." }); + } + } + + [HttpDelete("{id:int}")] + public IActionResult DeleteRenter(int id) + { + try + { + logger.LogInformation("Deleting renter with ID {RenterId}", id); + var result = service.DeleteRenter(id); + + if (!result) + { + logger.LogWarning("Renter with ID {RenterId} not found for deletion", id); + return NotFound(new { error = $"Renter with ID {id} not found." }); + } + + logger.LogInformation("Deleted renter with ID {RenterId}", id); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting renter with ID {RenterId}", id); + return StatusCode(500, new { error = "An error occurred while deleting the renter." }); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs new file mode 100644 index 000000000..d7512a789 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -0,0 +1,139 @@ +using Bikes.Application.Dto; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RentsController(IRentService service, ILogger logger) : ControllerBase +{ + [HttpGet] + public IActionResult GetAllRents() + { + try + { + logger.LogInformation("Getting all rents"); + var rents = service.GetAllRents(); + logger.LogInformation("Retrieved {Count} rents", rents.Count); + return Ok(rents); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting all rents"); + return StatusCode(500, new { error = "An error occurred while retrieving rents." }); + } + } + + [HttpGet("{id:int}")] + public IActionResult GetRent(int id) + { + try + { + logger.LogInformation("Getting rent with ID {RentId}", id); + var rent = service.GetRentById(id); + + if (rent == null) + { + logger.LogWarning("Rent with ID {RentId} not found", id); + return NotFound(new { error = $"Rent with ID {id} not found." }); + } + + return Ok(rent); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting rent with ID {RentId}", id); + return StatusCode(500, new { error = "An error occurred while retrieving the rent." }); + } + } + + [HttpPost] + public IActionResult CreateRent([FromBody] RentDto rentDto) + { + try + { + logger.LogInformation("Creating new rent for bike {BikeId} by renter {RenterId}", + rentDto.BikeId, rentDto.RenterId); + + if (!ModelState.IsValid) + { + logger.LogWarning("Invalid rent data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var id = service.CreateRent(rentDto); + logger.LogInformation("Created rent with ID {RentId}", id); + + return CreatedAtAction(nameof(GetRent), new { id }, new { id, message = "Rent created successfully." }); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error creating rent: {ErrorMessage}", ex.Message); + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating rent"); + return StatusCode(500, new { error = "An error occurred while creating the rent." }); + } + } + + [HttpPut("{id:int}")] + public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) + { + try + { + logger.LogInformation("Updating rent with ID {RentId}", id); + + if (!ModelState.IsValid) + { + return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + } + + var rent = service.UpdateRent(id, rentDto); + if (rent == null) + { + logger.LogWarning("Rent with ID {RentId} not found for update", id); + return NotFound(new { error = $"Rent with ID {id} not found." }); + } + + logger.LogInformation("Updated rent with ID {RentId}", id); + return Ok(new { message = "Rent updated successfully.", rent }); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error updating rent: {ErrorMessage}", ex.Message); + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating rent with ID {RentId}", id); + return StatusCode(500, new { error = "An error occurred while updating the rent." }); + } + } + + [HttpDelete("{id:int}")] + public IActionResult DeleteRent(int id) + { + try + { + logger.LogInformation("Deleting rent with ID {RentId}", id); + var result = service.DeleteRent(id); + + if (!result) + { + logger.LogWarning("Rent with ID {RentId} not found for deletion", id); + return NotFound(new { error = $"Rent with ID {id} not found." }); + } + + logger.LogInformation("Deleted rent with ID {RentId}", id); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting rent with ID {RentId}", id); + return StatusCode(500, new { error = "An error occurred while deleting the rent." }); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..2dfd3b661 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Bikes.Application.Services; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Repositories; + +namespace Bikes.Api.Host.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddBikeRentalServices(this IServiceCollection services) + { + services.AddSingleton, InMemoryBikeRepository>(); + services.AddSingleton, InMemoryBikeModelRepository>(); + services.AddSingleton, InMemoryRenterRepository>(); + services.AddSingleton, InMemoryRentRepository>(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Program.cs b/Bikes/Bikes.Api.Host/Program.cs new file mode 100644 index 000000000..4c3d44bbd --- /dev/null +++ b/Bikes/Bikes.Api.Host/Program.cs @@ -0,0 +1,25 @@ +using Bikes.Api.Host.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); + +builder.Services.AddBikeRentalServices(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Properties/launchSettings.json b/Bikes/Bikes.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..4afeec7d7 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22424", + "sslPort": 44322 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5094", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7262;http://localhost:5094", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Bikes/Bikes.Api.Host/appsettings.Development.json b/Bikes/Bikes.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Bikes/Bikes.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Bikes/Bikes.Api.Host/appsettings.json b/Bikes/Bikes.Api.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Bikes/Bikes.Api.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Bikes/Bikes.Application/Dto/BikeDto.cs b/Bikes/Bikes.Application/Dto/BikeDto.cs index 0562b39fe..48f8c0ed9 100644 --- a/Bikes/Bikes.Application/Dto/BikeDto.cs +++ b/Bikes/Bikes.Application/Dto/BikeDto.cs @@ -1,8 +1,22 @@ namespace Bikes.Application.Dto; +/// +/// DTO class for the Bike class +/// public class BikeDto { + /// + /// 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; } } diff --git a/Bikes/Bikes.Application/Dto/BikeModelDto.cs b/Bikes/Bikes.Application/Dto/BikeModelDto.cs index 84428c5a9..6231c76db 100644 --- a/Bikes/Bikes.Application/Dto/BikeModelDto.cs +++ b/Bikes/Bikes.Application/Dto/BikeModelDto.cs @@ -2,13 +2,43 @@ namespace Bikes.Application.Dto; +/// +/// DTO class for the BikeModel class +/// public class BikeModelDto { + /// + /// Model's type + /// public required BikeType Type { get; set; } + + /// + /// Model's size of wheel + /// public required int WheelSize { get; set; } + + /// + /// Maximum allowable passenger weight + /// public required int MaxPassengerWeight { get; set; } + + /// + /// Model's weight + /// public required int Weight { get; set; } + + /// + /// Model's type of brake + /// public required string BrakeType { get; set; } + + /// + /// Model's production year + /// public required string Year { get; set; } + + /// + /// The price of an hour of rent + /// public required int RentPrice { get; set; } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Dto/RentDto.cs b/Bikes/Bikes.Application/Dto/RentDto.cs index 4b393a477..856bc8946 100644 --- a/Bikes/Bikes.Application/Dto/RentDto.cs +++ b/Bikes/Bikes.Application/Dto/RentDto.cs @@ -1,9 +1,27 @@ namespace Bikes.Application.Dto; +/// +/// DTO class for the Rent class +/// public class RentDto { + /// + /// Rental start time + /// public required DateTime RentalStartTime { get; set; } + + /// + /// Rental duration + /// public required int RentalDuration { get; set; } + + /// + /// Renter's id + /// public required int RenterId { get; set; } + + /// + /// Bike's id + /// public required int BikeId { get; set; } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Dto/RenterDto.cs b/Bikes/Bikes.Application/Dto/RenterDto.cs index 31c07edf0..7228ca991 100644 --- a/Bikes/Bikes.Application/Dto/RenterDto.cs +++ b/Bikes/Bikes.Application/Dto/RenterDto.cs @@ -1,7 +1,17 @@ namespace Bikes.Application.Dto; +/// +/// DTO class for the Renter class +/// public class RenterDto { + /// + /// 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/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs index 85d7f7969..7b4fd88f5 100644 --- a/Bikes/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -3,6 +3,9 @@ namespace Bikes.Application.Services; +/// +/// A class that implements the interface of the AnalyticsService class +/// public class AnalyticsService : IAnalyticsService { private readonly IRepository _bikeRepository; @@ -10,6 +13,13 @@ public class AnalyticsService : IAnalyticsService private readonly IRepository _rentRepository; private readonly IRepository _renterRepository; + /// + /// The constructor that initializes repositories + /// + /// + /// + /// + /// public AnalyticsService( IRepository bikeRepository, IRepository bikeModelRepository, @@ -22,6 +32,9 @@ public AnalyticsService( _renterRepository = renterRepository; } + /// + /// A method that returns information about all sports bikes + /// public List GetSportBikes() { return _bikeRepository.ReadAll() @@ -29,6 +42,9 @@ public List GetSportBikes() .ToList(); } + /// + /// A method that returns the top 5 bike models by rental duration + /// public List GetTopFiveModelsByRentDuration() { var rents = _rentRepository.ReadAll(); @@ -50,6 +66,9 @@ public List GetTopFiveModelsByRentDuration() .ToList(); } + /// + /// A method that returns the top 5 bike models in terms of rental income + /// public List GetTopFiveModelsByProfit() { var rents = _rentRepository.ReadAll(); @@ -68,6 +87,9 @@ public List GetTopFiveModelsByProfit() .ToList(); } + /// + /// A method that returns information about the minimum, maximum, and average bike rental time. + /// public (int min, int max, double avg) GetRentalDurationStats() { var durations = _rentRepository.ReadAll() @@ -77,6 +99,9 @@ public List GetTopFiveModelsByProfit() return (durations.Min(), durations.Max(), durations.Average()); } + /// + /// A method that returns the total rental time of each type of bike + /// public Dictionary GetTotalRentalTimeByType() { return _rentRepository.ReadAll() @@ -87,6 +112,9 @@ public Dictionary GetTotalRentalTimeByType() ); } + /// + /// A method that returns information about the customers who have rented bicycles the most times. + /// public List GetTopThreeRenters() { var renters = _renterRepository.ReadAll(); diff --git a/Bikes/Bikes.Application/Services/BikeModelService.cs b/Bikes/Bikes.Application/Services/BikeModelService.cs index 528bd2032..eca3d9065 100644 --- a/Bikes/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes/Bikes.Application/Services/BikeModelService.cs @@ -4,15 +4,28 @@ namespace Bikes.Application.Services; +/// +/// A class that implements the interface of the BikeModelService class +/// public class BikeModelService : IBikeModelService { private readonly IRepository _bikeModelRepository; + /// + /// The constructor that initializes repositories + /// + /// public BikeModelService(IRepository bikeModelRepository) { _bikeModelRepository = bikeModelRepository; } + /// + /// A method that maps a DTO object to a domain object + /// + /// + /// + /// private static BikeModel MapToDomain(BikeModelDto dto, int id = 0) { return new BikeModel @@ -28,16 +41,36 @@ private static BikeModel MapToDomain(BikeModelDto dto, int id = 0) }; } + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateBikeModel(BikeModelDto bikeModelDto) { var bikeModel = MapToDomain(bikeModelDto); return _bikeModelRepository.Create(bikeModel); } + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllBikeModels() => _bikeModelRepository.ReadAll(); + /// + /// Returns object by id + /// + /// + /// public BikeModel? GetBikeModelById(int id) => _bikeModelRepository.Read(id); + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto) { var existingModel = _bikeModelRepository.Read(id); @@ -47,5 +80,10 @@ public int CreateBikeModel(BikeModelDto bikeModelDto) return _bikeModelRepository.Update(id, updatedModel); } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteBikeModel(int id) => _bikeModelRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs index 6b9129ac8..83b97bb91 100644 --- a/Bikes/Bikes.Application/Services/BikeService.cs +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -4,11 +4,19 @@ namespace Bikes.Application.Services; +/// +/// A class that implements the interface of the BikeService class +/// public class BikeService : IBikeService { private readonly IRepository _bikeRepository; private readonly IRepository _bikeModelRepository; + /// + /// The constructor that initializes repositories + /// + /// + /// public BikeService( IRepository bikeRepository, IRepository bikeModelRepository) @@ -17,6 +25,13 @@ public BikeService( _bikeModelRepository = bikeModelRepository; } + /// + /// A method that maps a DTO object to a domain object + /// + /// + /// + /// + /// private static Bike MapToDomain(BikeDto dto, BikeModel model, int id = 0) { return new Bike @@ -28,6 +43,11 @@ private static Bike MapToDomain(BikeDto dto, BikeModel model, int id = 0) }; } + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateBike(BikeDto bikeDto) { var model = _bikeModelRepository.Read(bikeDto.ModelId); @@ -38,10 +58,25 @@ public int CreateBike(BikeDto bikeDto) return _bikeRepository.Create(bike); } + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllBikes() => _bikeRepository.ReadAll(); + /// + /// Returns object by id + /// + /// + /// public Bike? GetBikeById(int id) => _bikeRepository.Read(id); + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public Bike? UpdateBike(int id, BikeDto bikeDto) { var existingBike = _bikeRepository.Read(id); @@ -54,5 +89,10 @@ public int CreateBike(BikeDto bikeDto) return _bikeRepository.Update(id, updatedBike); } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteBike(int id) => _bikeRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IAnalyticsService.cs b/Bikes/Bikes.Application/Services/IAnalyticsService.cs index 1ae40810d..72c9f8fbc 100644 --- a/Bikes/Bikes.Application/Services/IAnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/IAnalyticsService.cs @@ -2,12 +2,38 @@ namespace Bikes.Application.Services; +/// +/// Interface for the Analytics service class +/// public interface IAnalyticsService { + /// + /// A method that returns information about all sports bikes + /// public List GetSportBikes(); + + /// + /// A method that returns the top 5 bike models by rental duration + /// public List GetTopFiveModelsByRentDuration(); + + /// + /// A method that returns the top 5 bike models in terms of rental income + /// public List GetTopFiveModelsByProfit(); + + /// + /// A method that returns information about the minimum, maximum, and average bike rental time. + /// public (int min, int max, double avg) GetRentalDurationStats(); + + /// + /// A method that returns the total rental time of each type of bike + /// public Dictionary GetTotalRentalTimeByType(); + + /// + /// A method that returns information about the customers who have rented bicycles the most times. + /// public List GetTopThreeRenters(); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeModelService.cs b/Bikes/Bikes.Application/Services/IBikeModelService.cs index e63b79f56..c16eddb6b 100644 --- a/Bikes/Bikes.Application/Services/IBikeModelService.cs +++ b/Bikes/Bikes.Application/Services/IBikeModelService.cs @@ -3,11 +3,43 @@ namespace Bikes.Application.Services; +/// +/// Interface for the BikeModel service class +/// public interface IBikeModelService { + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateBikeModel(BikeModelDto bikeModelDto); + + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllBikeModels(); + + /// + /// Returns object by id + /// + /// + /// public BikeModel? GetBikeModelById(int id); + + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto); + + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteBikeModel(int id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeService.cs b/Bikes/Bikes.Application/Services/IBikeService.cs index cbe49c568..d7000be92 100644 --- a/Bikes/Bikes.Application/Services/IBikeService.cs +++ b/Bikes/Bikes.Application/Services/IBikeService.cs @@ -3,11 +3,43 @@ namespace Bikes.Application.Services; +/// +/// Interface for the Bike service class +/// public interface IBikeService { + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateBike(BikeDto bikeDto); + + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllBikes(); + + /// + /// Returns object by id + /// + /// + /// public Bike? GetBikeById(int id); + + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public Bike? UpdateBike(int id, BikeDto bikeDto); + + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteBike(int id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IRentService.cs b/Bikes/Bikes.Application/Services/IRentService.cs index 59775741b..764df56db 100644 --- a/Bikes/Bikes.Application/Services/IRentService.cs +++ b/Bikes/Bikes.Application/Services/IRentService.cs @@ -3,11 +3,43 @@ namespace Bikes.Application.Services; +/// +/// Interface for the Rent service class +/// public interface IRentService { + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateRent(RentDto rentDto); + + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllRents(); + + /// + /// Returns object by id + /// + /// + /// public Rent? GetRentById(int id); + + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public Rent? UpdateRent(int id, RentDto rentDto); + + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteRent(int id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IRenterService.cs b/Bikes/Bikes.Application/Services/IRenterService.cs index fe5283ca0..283888105 100644 --- a/Bikes/Bikes.Application/Services/IRenterService.cs +++ b/Bikes/Bikes.Application/Services/IRenterService.cs @@ -3,11 +3,43 @@ namespace Bikes.Application.Services; +/// +/// Interface for the Renter service class +/// public interface IRenterService { + /// + /// Creates a new object + /// + /// DTO object + /// ID of the created object public int CreateRenter(RenterDto renterDto); + + /// + /// Returns all existing objects + /// + /// List of existing objects public List GetAllRenters(); + + /// + /// Returns object by id + /// + /// + /// public Renter? GetRenterById(int id); + + /// + /// Updates an existing object + /// + /// Id + /// DTO object + /// Object if exist public Renter? UpdateRenter(int id, RenterDto renterDto); + + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool DeleteRenter(int id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs index 065d505c4..745d1c15b 100644 --- a/Bikes/Bikes.Application/Services/RentService.cs +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -4,12 +4,21 @@ namespace Bikes.Application.Services; +/// +/// A class that implements the interface of the RentService class +/// public class RentService : IRentService { private readonly IRepository _rentRepository; private readonly IRepository _bikeRepository; private readonly IRepository _renterRepository; + /// + /// The constructor that initializes repositories + /// + /// + /// + /// public RentService( IRepository rentRepository, IRepository bikeRepository, @@ -20,7 +29,14 @@ public RentService( _renterRepository = renterRepository; } - + /// + /// A method that maps a DTO object to a domain object + /// + /// + /// + /// + /// + /// private static Rent MapToDomain(RentDto dto, Bike bike, Renter renter, int id = 0) { return new Rent @@ -33,6 +49,12 @@ private static Rent MapToDomain(RentDto dto, Bike bike, Renter renter, int id = }; } + /// + /// Creates a new object + /// + /// + /// + /// public int CreateRent(RentDto rentDto) { var bike = _bikeRepository.Read(rentDto.BikeId); @@ -47,10 +69,25 @@ public int CreateRent(RentDto rentDto) return _rentRepository.Create(rent); } + /// + /// Returns all existing objects + /// + /// public List GetAllRents() => _rentRepository.ReadAll(); + /// + /// Returns object by id + /// + /// + /// public Rent? GetRentById(int id) => _rentRepository.Read(id); + /// + /// Updates an existing object + /// + /// + /// + /// public Rent? UpdateRent(int id, RentDto rentDto) { var existingRent = _rentRepository.Read(id); @@ -66,5 +103,10 @@ public int CreateRent(RentDto rentDto) return _rentRepository.Update(id, updatedRent); } + /// + /// Deletes an existing object by id + /// + /// + /// public bool DeleteRent(int id) => _rentRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/RenterService.cs b/Bikes/Bikes.Application/Services/RenterService.cs index 20cbd5f9a..db0d4e64d 100644 --- a/Bikes/Bikes.Application/Services/RenterService.cs +++ b/Bikes/Bikes.Application/Services/RenterService.cs @@ -4,15 +4,28 @@ namespace Bikes.Application.Services; +/// +/// A class that implements the interface of the RenterService class +/// public class RenterService : IRenterService { private readonly IRepository _renterRepository; + /// + /// The constructor that initializes repositories + /// + /// public RenterService(IRepository renterRepository) { _renterRepository = renterRepository; } + /// + /// A method that maps a DTO object to a domain object + /// + /// + /// + /// private static Renter MapToDomain(RenterDto dto, int id = 0) { return new Renter @@ -23,16 +36,36 @@ private static Renter MapToDomain(RenterDto dto, int id = 0) }; } + /// + /// Creates a new object + /// + /// + /// public int CreateRenter(RenterDto renterDto) { var renter = MapToDomain(renterDto); return _renterRepository.Create(renter); } + /// + /// Returns all existing objects + /// + /// public List GetAllRenters() => _renterRepository.ReadAll(); + /// + /// Returns object by id + /// + /// + /// public Renter? GetRenterById(int id) => _renterRepository.Read(id); + /// + /// Updates an existing object + /// + /// + /// + /// public Renter? UpdateRenter(int id, RenterDto renterDto) { var existingRenter = _renterRepository.Read(id); @@ -42,5 +75,10 @@ public int CreateRenter(RenterDto renterDto) return _renterRepository.Update(id, updatedRenter); } + /// + /// Deletes an existing object by id + /// + /// + /// public bool DeleteRenter(int id) => _renterRepository.Delete(id); } diff --git a/Bikes/Bikes.Domain/Repositories/IRepository.cs b/Bikes/Bikes.Domain/Repositories/IRepository.cs index 0d72a0927..4f831d8d9 100644 --- a/Bikes/Bikes.Domain/Repositories/IRepository.cs +++ b/Bikes/Bikes.Domain/Repositories/IRepository.cs @@ -1,14 +1,44 @@ namespace Bikes.Domain.Repositories; +/// +/// Repository interface for CRUD operations with domain objects. +/// +/// Type of entity +/// Type of identifier public interface IRepository { + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public TKey Create(TEntity entity); + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll(); + /// + /// Returns object by id + /// + /// Id + /// Object if exist public TEntity? Read(TKey id); + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public TEntity? Update(TKey id, TEntity entity); + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(TKey id); } diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs index 21c49f4c6..1168ea7db 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs @@ -4,18 +4,29 @@ namespace Bikes.Infrastructure.InMemory.Repositories; +/// +/// Implementing a repository for the BikeModel class +/// public class InMemoryBikeModelRepository : IRepository { private readonly List _items = []; private int _currentId; + /// + /// A constructor that uses data from InMemorySeeder + /// public InMemoryBikeModelRepository() { _items = InMemorySeeder.GetBikeModels(); _currentId = _items.Count > 0 ? _items.Count : 0; } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(BikeModel entity) { entity.Id = ++_currentId; @@ -23,16 +34,31 @@ public int Create(BikeModel entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _items; } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public BikeModel? Read(int id) { return _items.FirstOrDefault(b => b.Id == id); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public BikeModel? Update(int id, BikeModel entity) { var exsitingEntity = Read(id); @@ -49,6 +75,11 @@ public List ReadAll() return exsitingEntity; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var exsitingEntity = Read(id); diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs index df4b796c8..7a5569750 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs @@ -4,18 +4,29 @@ namespace Bikes.Infrastructure.InMemory.Repositories; +/// +/// Implementing a repository for the Bike class +/// public class InMemoryBikeRepository : IRepository { private readonly List _items = []; private int _currentId; + /// + /// A constructor that uses data from InMemorySeeder + /// public InMemoryBikeRepository() { _items = InMemorySeeder.GetBikes(); _currentId = _items.Count > 0 ? _items.Count : 0; } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Bike entity) { entity.Id = ++_currentId; @@ -23,16 +34,31 @@ public int Create(Bike entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _items; } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Bike? Read(int id) { return _items.FirstOrDefault(b => b.Id == id); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Bike? Update(int id, Bike entity) { var exsitingEntity = Read(id); @@ -45,6 +71,11 @@ public List ReadAll() return exsitingEntity; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var exsitingEntity = Read(id); diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs index ea1e521b9..c6c4636a6 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs @@ -4,18 +4,29 @@ namespace Bikes.Infrastructure.InMemory.Repositories; +/// +/// Implementing a repository for the Rent class +/// public class InMemoryRentRepository : IRepository { private readonly List _items = []; private int _currentId; + /// + /// A constructor that uses data from InMemorySeeder + /// public InMemoryRentRepository() { _items = InMemorySeeder.GetRents(); _currentId = _items.Count > 0 ? _items.Count : 0; } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Rent entity) { entity.Id = ++_currentId; @@ -23,16 +34,31 @@ public int Create(Rent entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _items; } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Rent? Read(int id) { return _items.FirstOrDefault(b => b.Id == id); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Rent? Update(int id, Rent entity) { var exsitingEntity = Read(id); @@ -46,6 +72,11 @@ public List ReadAll() return exsitingEntity; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var exsitingEntity = Read(id); diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs index 6dbfd7a5e..ac3643cd7 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs @@ -4,18 +4,29 @@ namespace Bikes.Infrastructure.InMemory.Repositories; +/// +/// Implementing a repository for the Renter class +/// public class InMemoryRenterRepository : IRepository { private readonly List _items = []; private int _currentId; + /// + /// A constructor that uses data from InMemorySeeder + /// public InMemoryRenterRepository() { _items = InMemorySeeder.GetRenters(); _currentId = _items.Count > 0 ? _items.Count : 0; } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Renter entity) { entity.Id = ++_currentId; @@ -23,16 +34,31 @@ public int Create(Renter entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _items; } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Renter? Read(int id) { return _items.FirstOrDefault(b => b.Id == id); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Renter? Update(int id, Renter entity) { var exsitingEntity = Read(id); @@ -44,6 +70,11 @@ public List ReadAll() return exsitingEntity; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var exsitingEntity = Read(id); diff --git a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs index ddc6637c3..0c59b7917 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs @@ -2,8 +2,14 @@ namespace Bikes.Infrastructure.InMemory.Seeders; +/// +/// InMemorySeeder for creating the data +/// public static class InMemorySeeder { + /// + /// A static method that creates a list of bike models + /// public static List GetBikeModels() { return @@ -21,6 +27,9 @@ public static List GetBikeModels() ]; } + /// + /// A static method that creates a list of bikes + /// public static List GetBikes() { var models = GetBikeModels(); @@ -40,6 +49,9 @@ public static List GetBikes() ]; } + /// + /// A static method that creates a list of renters + /// public static List GetRenters() { return @@ -58,7 +70,7 @@ public static List GetRenters() } /// - /// A method that initializes list of rents + /// A static method that creates a list of rents /// public static List GetRents() { diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index fbf9445e6..a4d49e58d 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -1,5 +1,4 @@ using Bikes.Domain.Models; -using System.Collections.Generic; namespace Bikes.Tests; diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index f25e50730..6e890ba46 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes. {B6E3E827-ADA9-4B0E-B704-484513A051A8} = {B6E3E827-ADA9-4B0E-B704-484513A051A8} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api.Host\Bikes.Api.Host.csproj", "{B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +38,10 @@ Global {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {619487B4-C5C4-4396-B2DF-18B83CD522CE}.Release|Any CPU.Build.0 = Release|Any CPU + {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From bfc2c0a9b6c2e2aa54049566af8cdd06ab25b8c9 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 11 Nov 2025 00:57:46 +0400 Subject: [PATCH 20/41] Update tests and fix comments --- .../Controllers/AnalyticsController.cs | 23 ++++ .../Controllers/BikeModelsController.cs | 30 +++++ .../Controllers/BikesController.cs | 30 +++++ .../Controllers/RentersController.cs | 30 +++++ .../Controllers/RentsController.cs | 30 +++++ .../Extensions/ServiceCollectionExtensions.cs | 8 ++ .../Bikes.Infrastructure.InMemory.csproj | 1 - Bikes/Bikes.Tests/Bikes.Tests.csproj | 3 +- Bikes/Bikes.Tests/BikesFixture.cs | 126 ++---------------- Bikes/Bikes.Tests/BikesTests.cs | 72 +++------- 10 files changed, 184 insertions(+), 169 deletions(-) diff --git a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs index 9223b9ed8..b7de1ab5e 100644 --- a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -3,10 +3,18 @@ namespace Bikes.Api.Host.Controllers; +/// +/// A class that implements a controller for processing HTTP requests for the AnalyticsService class +/// +/// +/// [ApiController] [Route("api/[controller]")] public class AnalyticsController(IAnalyticsService service, ILogger logger) : ControllerBase { + /// + /// A method that returns information about all sports bikes + /// [HttpGet("sport-bikes")] public IActionResult GetSportBikes() { @@ -24,6 +32,9 @@ public IActionResult GetSportBikes() } } + /// + /// A method that returns the top 5 bike models by rental duration + /// [HttpGet("top-models/duration")] public IActionResult GetTopModelsByDuration() { @@ -41,6 +52,9 @@ public IActionResult GetTopModelsByDuration() } } + /// + /// A method that returns the top 5 bike models in terms of rental income + /// [HttpGet("top-models/profit")] public IActionResult GetTopModelsByProfit() { @@ -58,6 +72,9 @@ public IActionResult GetTopModelsByProfit() } } + /// + /// A method that returns information about the minimum, maximum, and average bike rental time. + /// [HttpGet("stats/duration")] public IActionResult GetRentalDurationStats() { @@ -81,6 +98,9 @@ public IActionResult GetRentalDurationStats() } } + /// + /// A method that returns the total rental time of each type of bike + /// [HttpGet("stats/rental-time-by-type")] public IActionResult GetTotalRentalTimeByType() { @@ -98,6 +118,9 @@ public IActionResult GetTotalRentalTimeByType() } } + /// + /// A method that returns information about the customers who have rented bicycles the most times. + /// [HttpGet("top-renters")] public IActionResult GetTopRenters() { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs index c5deba11e..591248dba 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -4,10 +4,19 @@ namespace Bikes.Api.Host.Controllers; +/// +/// A class that implements a controller for processing HTTP requests for the BikeModels class +/// +/// +/// [ApiController] [Route("api/[controller]")] public class BikeModelsController(IBikeModelService service, ILogger logger) : ControllerBase { + /// + /// Returns all existing objects + /// + /// [HttpGet] public IActionResult GetAllBikeModels() { @@ -25,6 +34,11 @@ public IActionResult GetAllBikeModels() } } + /// + /// Returns object by id + /// + /// + /// [HttpGet("{id:int}")] public IActionResult GetBikeModel(int id) { @@ -48,6 +62,11 @@ public IActionResult GetBikeModel(int id) } } + /// + /// Creates a new object + /// + /// + /// [HttpPost] public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) { @@ -73,6 +92,12 @@ public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) } } + /// + /// Updates an existing object + /// + /// + /// + /// [HttpPut("{id:int}")] public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDto) { @@ -102,6 +127,11 @@ public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDt } } + /// + /// Deletes an existing object by id + /// + /// + /// [HttpDelete("{id:int}")] public IActionResult DeleteBikeModel(int id) { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs index 3cd5a4133..4943b1680 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -4,10 +4,19 @@ namespace Bikes.Api.Host.Controllers; +/// +/// A class that implements a controller for processing HTTP requests for the BikeService class +/// +/// +/// [ApiController] [Route("api/[controller]")] public class BikesController(IBikeService service, ILogger logger) : ControllerBase { + /// + /// Returns all existing objects + /// + /// [HttpGet] public IActionResult GetAllBikes() { @@ -25,6 +34,11 @@ public IActionResult GetAllBikes() } } + /// + /// Returns object by id + /// + /// + /// [HttpGet("{id:int}")] public IActionResult GetBike(int id) { @@ -49,6 +63,11 @@ public IActionResult GetBike(int id) } } + /// + /// Creates a new object + /// + /// + /// [HttpPost] public IActionResult CreateBike([FromBody] BikeDto bikeDto) { @@ -79,6 +98,12 @@ public IActionResult CreateBike([FromBody] BikeDto bikeDto) } } + /// + /// Updates an existing object + /// + /// + /// + /// [HttpPut("{id:int}")] public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) { @@ -114,6 +139,11 @@ public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) } } + /// + /// Deletes an existing object by id + /// + /// + /// [HttpDelete("{id:int}")] public IActionResult DeleteBike(int id) { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs index b2eff5e10..bf8590763 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -4,10 +4,19 @@ namespace Bikes.Api.Host.Controllers; +/// +/// A class that implements a controller for processing HTTP requests for the RenterService class +/// +/// +/// [ApiController] [Route("api/[controller]")] public class RentersController(IRenterService service, ILogger logger) : ControllerBase { + /// + /// Returns all existing objects + /// + /// [HttpGet] public IActionResult GetAllRenters() { @@ -25,6 +34,11 @@ public IActionResult GetAllRenters() } } + /// + /// Returns object by id + /// + /// + /// [HttpGet("{id:int}")] public IActionResult GetRenter(int id) { @@ -48,6 +62,11 @@ public IActionResult GetRenter(int id) } } + /// + /// Creates a new objec + /// + /// + /// [HttpPost] public IActionResult CreateRenter([FromBody] RenterDto renterDto) { @@ -73,6 +92,12 @@ public IActionResult CreateRenter([FromBody] RenterDto renterDto) } } + /// + /// Updates an existing object + /// + /// + /// + /// [HttpPut("{id:int}")] public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) { @@ -102,6 +127,11 @@ public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) } } + /// + /// Deletes an existing object by id + /// + /// + /// [HttpDelete("{id:int}")] public IActionResult DeleteRenter(int id) { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs index d7512a789..d5d75e781 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -4,10 +4,19 @@ namespace Bikes.Api.Host.Controllers; +/// +/// A class that implements a controller for processing HTTP requests for the RentService class +/// +/// +/// [ApiController] [Route("api/[controller]")] public class RentsController(IRentService service, ILogger logger) : ControllerBase { + /// + /// Returns all existing objects + /// + /// [HttpGet] public IActionResult GetAllRents() { @@ -25,6 +34,11 @@ public IActionResult GetAllRents() } } + /// + /// Returns object by id + /// + /// + /// [HttpGet("{id:int}")] public IActionResult GetRent(int id) { @@ -48,6 +62,11 @@ public IActionResult GetRent(int id) } } + /// + /// Creates a new object + /// + /// + /// [HttpPost] public IActionResult CreateRent([FromBody] RentDto rentDto) { @@ -79,6 +98,12 @@ public IActionResult CreateRent([FromBody] RentDto rentDto) } } + /// + /// Updates an existing object + /// + /// + /// + /// [HttpPut("{id:int}")] public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) { @@ -113,6 +138,11 @@ public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) } } + /// + /// Deletes an existing object by id + /// + /// + /// [HttpDelete("{id:int}")] public IActionResult DeleteRent(int id) { diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs index 2dfd3b661..bb5dabc1b 100644 --- a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs @@ -4,8 +4,16 @@ namespace Bikes.Api.Host.Extensions; +/// +/// A class for hidden registration of services +/// public static class ServiceCollectionExtensions { + /// + /// The method that registers services + /// + /// + /// public static IServiceCollection AddBikeRentalServices(this IServiceCollection services) { services.AddSingleton, InMemoryBikeRepository>(); diff --git a/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj b/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj index 6cacc3bdd..30da6d385 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj +++ b/Bikes/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj @@ -8,7 +8,6 @@ - diff --git a/Bikes/Bikes.Tests/Bikes.Tests.csproj b/Bikes/Bikes.Tests/Bikes.Tests.csproj index 3a1bf7982..73a40b6ae 100644 --- a/Bikes/Bikes.Tests/Bikes.Tests.csproj +++ b/Bikes/Bikes.Tests/Bikes.Tests.csproj @@ -17,7 +17,8 @@ - + + diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index a4d49e58d..24229534b 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -1,130 +1,26 @@ -using Bikes.Domain.Models; +using Bikes.Application.Services; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Repositories; namespace Bikes.Tests; /// -/// A class for creating the data needed for testing +/// A class for tests /// public class BikesFixture { - /// - /// List of bike models - /// - public readonly List BikeModels; - - /// - /// List of bikes - /// - public readonly List Bikes; - - /// - /// List of renters - /// - public readonly List Renters; + public readonly IAnalyticsService AnalyticsService; /// - /// List of rents - /// - public readonly List Rents; - - /// - /// Initializes a new instance of the BikesFixture class. + /// A constructor that creates repositories and service classes /// public BikesFixture() { - BikeModels = InitializeBikeModels(); - Renters = InitializeRenters(); - Bikes = InitializeBikes(); - Rents = InitializeRents(); - } - - /// - /// A method that initializes list of bike models - /// - private static List InitializeBikeModels() - { - return - [ - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, - new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, - new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 }, - new() { Id = 4, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 125, Weight = 15, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 750 }, - new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, - new() { Id = 6, Type = BikeType.City, WheelSize = 27, MaxPassengerWeight = 135, Weight = 17, BrakeType = "Дисковые механические", Year = "2023", RentPrice = 550 }, - new() { Id = 7, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 13, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 800 }, - new() { Id = 8, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 10, BrakeType = "Ободные v-brake", Year = "2023", RentPrice = 950 }, - new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 600 }, - new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } - ]; - } - - /// - /// A method that initializes list of bikes - /// - private List InitializeBikes() - { - return - [ - new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = BikeModels[0] }, - new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = BikeModels[1] }, - new() { Id = 3, SerialNumber = "CTY202203001", Color = "Синий", Model = BikeModels[2] }, - new() { Id = 4, SerialNumber = "MTB202302001", Color = "Зеленый", Model = BikeModels[3] }, - new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = BikeModels[4] }, - new() { Id = 6, SerialNumber = "CTY202304001", Color = "Белый", Model = BikeModels[5] }, - new() { Id = 7, SerialNumber = "MTB202404001", Color = "Оранжевый", Model = BikeModels[6] }, - new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = BikeModels[7] }, - new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", Model = BikeModels[8] }, - new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = BikeModels[9] } - ]; - } + IRepository bikeRepo = new InMemoryBikeRepository(); + IRepository modelRepo = new InMemoryBikeModelRepository(); + IRepository rentRepo = new InMemoryRentRepository(); + IRepository renterRepo = new InMemoryRenterRepository(); - /// - /// A method that initializes list of renters - /// - private static List InitializeRenters() - { - return - [ - new() { Id = 1, FullName = "Иванов Иван Иванович", Number = "+7 (912) 345-67-89" }, - new() { Id = 2, FullName = "Петров Петр Сергеевич", Number = "+7 (923) 456-78-90" }, - new() { Id = 3, FullName = "Сидорова Анна Владимировна", Number = "+7 (934) 567-89-01" }, - new() { Id = 4, FullName = "Кузнецов Алексей Дмитриевич", Number = "+7 (945) 678-90-12" }, - new() { Id = 5, FullName = "Смирнова Екатерина Олеговна", Number = "+7 (956) 789-01-23" }, - new() { Id = 6, FullName = "Попов Денис Андреевич", Number = "+7 (967) 890-12-34" }, - new() { Id = 7, FullName = "Васильева Мария Игоревна", Number = "+7 (978) 901-23-45" }, - new() { Id = 8, FullName = "Николаев Сергей Викторович", Number = "+7 (989) 012-34-56" }, - new() { Id = 9, FullName = "Орлова Ольга Павловна", Number = "+7 (990) 123-45-67" }, - new() { Id = 10, FullName = "Федоров Артем Константинович", Number = "+7 (901) 234-56-78" } - ]; - } - - /// - /// A method that initializes list of rents - /// - private List InitializeRents() - { - return - [ - new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[0] }, - new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[0] }, - new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[0] }, - new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = Renters[3], Bike = Bikes[1] }, - new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = Renters[4], Bike = Bikes[1] }, - new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = Renters[5], Bike = Bikes[1] }, - new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = Renters[6], Bike = Bikes[2] }, - new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = Renters[7], Bike = Bikes[2] }, - new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = Renters[8], Bike = Bikes[3] }, - new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = Renters[9], Bike = Bikes[3] }, - new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = Renters[0], Bike = Bikes[4] }, - new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = Renters[0], Bike = Bikes[4] }, - new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = Renters[0], Bike = Bikes[5] }, - new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = Renters[0], Bike = Bikes[6] }, - new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = Renters[1], Bike = Bikes[7] }, - new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = Renters[1], Bike = Bikes[8] }, - new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = Renters[1], Bike = Bikes[9] }, - new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = Renters[5], Bike = Bikes[9] }, - new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = Renters[5], Bike = Bikes[9] }, - new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = Renters[2], Bike = Bikes[9] } - ]; + AnalyticsService = new AnalyticsService(bikeRepo, modelRepo, rentRepo, renterRepo); } } \ No newline at end of file diff --git a/Bikes/Bikes.Tests/BikesTests.cs b/Bikes/Bikes.Tests/BikesTests.cs index 7a0462e4a..798c309aa 100644 --- a/Bikes/Bikes.Tests/BikesTests.cs +++ b/Bikes/Bikes.Tests/BikesTests.cs @@ -13,14 +13,12 @@ public class BikesTests(BikesFixture fixture) : IClassFixture [Fact] public void InformationAboutSportBikes() { - var expectedModelIds = new List {2, 5, 8}; + var expectedBikeIds = new List { 2, 5, 8 }; - var actualIds = fixture.Bikes - .Where(bike => bike.Model.Type == BikeType.Sport) - .Select(bike => bike.Id) - .ToList(); + var sportBikes = fixture.AnalyticsService.GetSportBikes(); + var actualIds = sportBikes.Select(bike => bike.Id).ToList(); - Assert.Equal(expectedModelIds, actualIds); + Assert.Equal(expectedBikeIds, actualIds); } /// @@ -29,19 +27,10 @@ public void InformationAboutSportBikes() [Fact] public void TopFiveModelsRentDurationIds() { - var expectedModelIds = new List {10, 1, 2, 5, 3}; - - var actualIds = fixture.Rents - .GroupBy(rent => rent.Bike.Model.Id) - .Select(group => new - { - ModelId = group.Key, - TotalDuration = group.Sum(rent => rent.RentalDuration) - }) - .OrderByDescending(x => x.TotalDuration) - .Select(x => x.ModelId) - .Take(5) - .ToList(); + var expectedModelIds = new List { 10, 1, 2, 5, 3 }; + + var topModels = fixture.AnalyticsService.GetTopFiveModelsByRentDuration(); + var actualIds = topModels.Select(model => model.Id).ToList(); Assert.Equal(expectedModelIds, actualIds); } @@ -52,19 +41,11 @@ public void TopFiveModelsRentDurationIds() [Fact] public void TopFiveModelsProfit() { - var expectedModelIds = new List {10, 5, 2, 1, 3}; - - var actualIds = fixture.Rents - .GroupBy(rent => rent.Bike.Model.Id) - .Select(group => new - { - ModelId = group.Key, - TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) - }) - .OrderByDescending(x => x.TotalProfit) - .Select(x => x.ModelId) - .Take(5) - .ToList(); + var expectedModelIds = new List { 10, 5, 2, 1, 3 }; + + var topModels = fixture.AnalyticsService.GetTopFiveModelsByProfit(); + var actualIds = topModels.Select(model => model.Id).ToList(); + Assert.Equal(expectedModelIds, actualIds); } @@ -78,10 +59,7 @@ public void MinMaxAvgRentalDuration() const int expectedMax = 5; const double expectedAvg = 2.95; - var durations = fixture.Rents.Select(rent => rent.RentalDuration).ToList(); - var actualMin = durations.Min(); - var actualMax = durations.Max(); - var actualAvg = durations.Average(); + var (actualMin, actualMax, actualAvg) = fixture.AnalyticsService.GetRentalDurationStats(); Assert.Equal(expectedMin, actualMin); Assert.Equal(expectedMax, actualMax); @@ -97,9 +75,8 @@ public void MinMaxAvgRentalDuration() [InlineData(BikeType.City, 12)] public void TotalRentalTimeByType(BikeType bikeType, int expectedRentalTime) { - var actualRentalTime = fixture.Rents - .Where(rent => rent.Bike.Model.Type == bikeType) - .Sum(rent => rent.RentalDuration); + var rentalTimeByType = fixture.AnalyticsService.GetTotalRentalTimeByType(); + var actualRentalTime = rentalTimeByType[bikeType]; Assert.Equal(expectedRentalTime, actualRentalTime); } @@ -110,19 +87,10 @@ public void TotalRentalTimeByType(BikeType bikeType, int expectedRentalTime) [Fact] public void TopThreeRenters() { - var expectedTopRentersIds = new List {1, 2, 6}; - - var actualTopRentersIds = fixture.Rents - .GroupBy(rent => rent.Renter.Id) - .Select(group => new - { - RenterId = group.Key, - TotalRentals = group.Count() - }) - .OrderByDescending(r => r.TotalRentals) - .Select(x => x.RenterId) - .Take(3) - .ToList(); + var expectedTopRentersIds = new List { 1, 2, 6 }; + + var topRenters = fixture.AnalyticsService.GetTopThreeRenters(); + var actualTopRentersIds = topRenters.Select(renter => renter.Id).ToList(); Assert.Equal(expectedTopRentersIds, actualTopRentersIds); } From 8835b0db2d29c89369d8d87e3a84c06ac4908a81 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 11 Nov 2025 19:36:17 +0400 Subject: [PATCH 21/41] Update readme --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d36ce50ef..e29af7491 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,19 @@ 2. TopFiveModelsProfit, TopFiveModelsRentDurationIds - Вывести топ 5 моделей велосипедов (по прибыли от аренды и по длительности аренды отдельно). 3. MinMaxAvgRentalDuration - Вывести информацию о минимальном, максимальном и среднем времени аренды велосипедов. 4. TotalRentalTimeByType - Вывести суммарное время аренды велосипедов каждого типа. -5. TopThreeRenters - Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз. \ No newline at end of file +5. TopThreeRenters - Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз. + +## Лабораторная работа 2 - "Сервер" + +В рамках второй лабораторной работы было разработано серверное приложение с REST API для управления пунктом велопроката. Приложение предоставляет полный набор операций для работы с сущностями системы и аналитическими запросами. + +### Bikes.Domain - доменный слой, содержащий бизнес-сущности и интерфейсы репозиториев + +### Bikes.Application - слой приложения, содержащий DTO, сервисы и бизнес-логику + +### Bikes.Infrastructure.InMemory - инфраструктурный слой с реализацией in-memory репозиториев + +### Bikes.Api.Host - веб-слой с REST API контроллерами + +### Bikes.Tests - модульные тесты для проверки функциональности + From 213953a9542fafdcba076850c73dfc7bb3c25f9f Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 8 Dec 2025 03:55:15 +0400 Subject: [PATCH 22/41] add primary constructors, collection expressoins use ProducesResponseTypeautomapper, automapper all services now returning DTO and small fixes --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 3 + .../Controllers/AnalyticsController.cs | 81 ++++++++---- .../Controllers/BikeModelsController.cs | 94 ++++++++++---- .../Controllers/BikesController.cs | 108 ++++++++++++---- .../Controllers/RentersController.cs | 115 +++++++++++++---- .../Controllers/RentsController.cs | 107 ++++++++++++---- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Bikes.Application.csproj | 6 + .../Services/AnalyticsService.cs | 116 ++++++++++-------- .../Services/BikeModelService.cs | 79 ++++++------ .../Bikes.Application/Services/BikeService.cs | 81 +++++------- .../Services/IAnalyticsService.cs | 13 +- .../Services/IBikeModelService.cs | 8 +- .../Services/IBikeService.cs | 8 +- .../Services/IRentService.cs | 8 +- .../Services/IRenterService.cs | 8 +- .../Bikes.Application/Services/RentService.cs | 97 ++++++--------- .../Services/RenterService.cs | 64 ++++------ Bikes/Bikes.Contracts/Bikes.Contracts.csproj | 13 ++ .../Dto/BikeDto.cs | 2 +- .../Dto/BikeModelDto.cs | 2 +- .../Dto/RentDto.cs | 2 +- .../Dto/RentalDurationStatsDto.cs | 22 ++++ .../Dto/RenterDto.cs | 2 +- .../InMemoryBikeModelRepository.cs | 2 +- .../Repositories/InMemoryBikeRepository.cs | 2 +- .../Repositories/InMemoryRentRepository.cs | 2 +- .../Repositories/InMemoryRenterRepository.cs | 2 +- Bikes/Bikes.Tests/BikesFixture.cs | 2 +- Bikes/Bikes.Tests/BikesTests.cs | 64 +++++++--- Bikes/Bikes.sln | 6 + 31 files changed, 705 insertions(+), 416 deletions(-) create mode 100644 Bikes/Bikes.Contracts/Bikes.Contracts.csproj rename Bikes/{Bikes.Application => Bikes.Contracts}/Dto/BikeDto.cs (92%) rename Bikes/{Bikes.Application => Bikes.Contracts}/Dto/BikeModelDto.cs (96%) rename Bikes/{Bikes.Application => Bikes.Contracts}/Dto/RentDto.cs (93%) create mode 100644 Bikes/Bikes.Contracts/Dto/RentalDurationStatsDto.cs rename Bikes/{Bikes.Application => Bikes.Contracts}/Dto/RenterDto.cs (89%) diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index 5016e3ad7..afe9119e7 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -7,11 +7,14 @@ + + + diff --git a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs index b7de1ab5e..bf0d76b47 100644 --- a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -1,4 +1,6 @@ using Bikes.Application.Services; +using Bikes.Contracts.Dto; +using Bikes.Domain.Models; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -10,25 +12,33 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] -public class AnalyticsController(IAnalyticsService service, ILogger logger) : ControllerBase +[Produces("application/json")] +public class AnalyticsController( + IAnalyticsService service, + ILogger logger) : ControllerBase { /// /// A method that returns information about all sports bikes /// [HttpGet("sport-bikes")] - public IActionResult GetSportBikes() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetSportBikes() { try { logger.LogInformation("Getting sport bikes"); - var bikes = service.GetSportBikes(); + var bikes = service.GetSportBikes(); logger.LogInformation("Retrieved {Count} sport bikes", bikes.Count); - return Ok(bikes); + return Ok(bikes); } catch (Exception ex) { logger.LogError(ex, "Error getting sport bikes"); - return StatusCode(500, new { error = "An error occurred while retrieving sport bikes." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving sport bikes.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -36,7 +46,9 @@ public IActionResult GetSportBikes() /// A method that returns the top 5 bike models by rental duration /// [HttpGet("top-models/duration")] - public IActionResult GetTopModelsByDuration() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetTopModelsByDuration() { try { @@ -48,7 +60,10 @@ public IActionResult GetTopModelsByDuration() catch (Exception ex) { logger.LogError(ex, "Error getting top models by duration"); - return StatusCode(500, new { error = "An error occurred while retrieving top models by duration." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving top models by duration.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -56,19 +71,24 @@ public IActionResult GetTopModelsByDuration() /// A method that returns the top 5 bike models in terms of rental income /// [HttpGet("top-models/profit")] - public IActionResult GetTopModelsByProfit() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetTopModelsByProfit() { try { logger.LogInformation("Getting top models by profit"); - var models = service.GetTopFiveModelsByProfit(); + var models = service.GetTopFiveModelsByProfit(); logger.LogInformation("Retrieved top {Count} models by profit", models.Count); return Ok(models); } catch (Exception ex) { logger.LogError(ex, "Error getting top models by profit"); - return StatusCode(500, new { error = "An error occurred while retrieving top models by profit." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving top models by profit.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -76,25 +96,26 @@ public IActionResult GetTopModelsByProfit() /// A method that returns information about the minimum, maximum, and average bike rental time. /// [HttpGet("stats/duration")] - public IActionResult GetRentalDurationStats() + [ProducesResponseType(typeof(RentalDurationStatsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult GetRentalDurationStats() { try { logger.LogInformation("Getting rental duration statistics"); var stats = service.GetRentalDurationStats(); logger.LogInformation("Retrieved rental duration stats: Min={Min}, Max={Max}, Avg={Avg}", - stats.min, stats.max, stats.avg); - return Ok(new - { - min = stats.min, - max = stats.max, - average = stats.avg - }); + stats.Min, stats.Max, stats.Average); + + return Ok(stats); } catch (Exception ex) { logger.LogError(ex, "Error getting rental duration statistics"); - return StatusCode(500, new { error = "An error occurred while retrieving rental duration statistics." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving rental duration statistics.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -102,7 +123,9 @@ public IActionResult GetRentalDurationStats() /// A method that returns the total rental time of each type of bike /// [HttpGet("stats/rental-time-by-type")] - public IActionResult GetTotalRentalTimeByType() + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetTotalRentalTimeByType() { try { @@ -114,7 +137,10 @@ public IActionResult GetTotalRentalTimeByType() catch (Exception ex) { logger.LogError(ex, "Error getting total rental time by type"); - return StatusCode(500, new { error = "An error occurred while retrieving total rental time by type." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving total rental time by type.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -122,19 +148,24 @@ public IActionResult GetTotalRentalTimeByType() /// A method that returns information about the customers who have rented bicycles the most times. /// [HttpGet("top-renters")] - public IActionResult GetTopRenters() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetTopRenters() { try { logger.LogInformation("Getting top renters"); - var renters = service.GetTopThreeRenters(); + var renters = service.GetTopThreeRenters(); logger.LogInformation("Retrieved top {Count} renters", renters.Count); - return Ok(renters); + return Ok(renters); } catch (Exception ex) { logger.LogError(ex, "Error getting top renters"); - return StatusCode(500, new { error = "An error occurred while retrieving top renters." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving top renters.", + statusCode: StatusCodes.Status500InternalServerError); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs index 591248dba..e61ff7d30 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; @@ -11,14 +11,16 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] +[Produces("application/json")] public class BikeModelsController(IBikeModelService service, ILogger logger) : ControllerBase { /// /// Returns all existing objects /// - /// [HttpGet] - public IActionResult GetAllBikeModels() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetAllBikeModels() { try { @@ -30,7 +32,10 @@ public IActionResult GetAllBikeModels() catch (Exception ex) { logger.LogError(ex, "Error getting all bike models"); - return StatusCode(500, new { error = "An error occurred while retrieving bike models." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving bike models.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -38,9 +43,11 @@ public IActionResult GetAllBikeModels() /// Returns object by id /// /// - /// [HttpGet("{id:int}")] - public IActionResult GetBikeModel(int id) + [ProducesResponseType(typeof(BikeModelDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult GetBikeModel(int id) { try { @@ -50,7 +57,10 @@ public IActionResult GetBikeModel(int id) if (model == null) { logger.LogWarning("Bike model with ID {ModelId} not found", id); - return NotFound(new { error = $"Bike model with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike model with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } return Ok(model); @@ -58,7 +68,10 @@ public IActionResult GetBikeModel(int id) catch (Exception ex) { logger.LogError(ex, "Error getting bike model with ID {ModelId}", id); - return StatusCode(500, new { error = "An error occurred while retrieving the bike model." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving the bike model.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -66,9 +79,11 @@ public IActionResult GetBikeModel(int id) /// Creates a new object /// /// - /// [HttpPost] - public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) + [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) { try { @@ -76,19 +91,30 @@ public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) if (!ModelState.IsValid) { - logger.LogWarning("Invalid bike model data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid bike model data: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var id = service.CreateBikeModel(bikeModelDto); logger.LogInformation("Created bike model with ID {ModelId}", id); - return CreatedAtAction(nameof(GetBikeModel), new { id }, new { id, message = "Bike model created successfully." }); + return CreatedAtAction( + nameof(GetBikeModel), + new { id }, + new { id, message = "Bike model created successfully." }); } catch (Exception ex) { logger.LogError(ex, "Error creating bike model"); - return StatusCode(500, new { error = "An error occurred while creating the bike model." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while creating the bike model.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -97,9 +123,12 @@ public IActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) /// /// /// - /// [HttpPut("{id:int}")] - public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDto) + [ProducesResponseType(typeof(BikeModelDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDto) { try { @@ -107,23 +136,32 @@ public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDt if (!ModelState.IsValid) { - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var model = service.UpdateBikeModel(id, bikeModelDto); if (model == null) { logger.LogWarning("Bike model with ID {ModelId} not found for update", id); - return NotFound(new { error = $"Bike model with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike model with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Updated bike model with ID {ModelId}", id); - return Ok(new { message = "Bike model updated successfully.", model }); + return Ok(model); } catch (Exception ex) { logger.LogError(ex, "Error updating bike model with ID {ModelId}", id); - return StatusCode(500, new { error = "An error occurred while updating the bike model." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while updating the bike model.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -131,9 +169,11 @@ public IActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDt /// Deletes an existing object by id /// /// - /// [HttpDelete("{id:int}")] - public IActionResult DeleteBikeModel(int id) + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult DeleteBikeModel(int id) { try { @@ -143,7 +183,10 @@ public IActionResult DeleteBikeModel(int id) if (!result) { logger.LogWarning("Bike model with ID {ModelId} not found for deletion", id); - return NotFound(new { error = $"Bike model with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike model with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Deleted bike model with ID {ModelId}", id); @@ -152,7 +195,10 @@ public IActionResult DeleteBikeModel(int id) catch (Exception ex) { logger.LogError(ex, "Error deleting bike model with ID {ModelId}", id); - return StatusCode(500, new { error = "An error occurred while deleting the bike model." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while deleting the bike model.", + statusCode: StatusCodes.Status500InternalServerError); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs index 4943b1680..a39bc15c9 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; @@ -11,14 +11,16 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] +[Produces("application/json")] public class BikesController(IBikeService service, ILogger logger) : ControllerBase { /// /// Returns all existing objects /// - /// [HttpGet] - public IActionResult GetAllBikes() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetAllBikes() { try { @@ -30,7 +32,10 @@ public IActionResult GetAllBikes() catch (Exception ex) { logger.LogError(ex, "Error getting all bikes"); - return StatusCode(500, new { error = "An error occurred while retrieving bikes." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving bikes.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -38,9 +43,11 @@ public IActionResult GetAllBikes() /// Returns object by id /// /// - /// [HttpGet("{id:int}")] - public IActionResult GetBike(int id) + [ProducesResponseType(typeof(BikeDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult GetBike(int id) { try { @@ -50,7 +57,10 @@ public IActionResult GetBike(int id) if (bike == null) { logger.LogWarning("Bike with ID {BikeId} not found", id); - return NotFound(new { error = $"Bike with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Retrieved bike with ID {BikeId}", id); @@ -59,7 +69,10 @@ public IActionResult GetBike(int id) catch (Exception ex) { logger.LogError(ex, "Error getting bike with ID {BikeId}", id); - return StatusCode(500, new { error = "An error occurred while retrieving the bike." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving the bike.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -67,9 +80,11 @@ public IActionResult GetBike(int id) /// Creates a new object /// /// - /// [HttpPost] - public IActionResult CreateBike([FromBody] BikeDto bikeDto) + [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult CreateBike([FromBody] BikeDto bikeDto) { try { @@ -77,24 +92,38 @@ public IActionResult CreateBike([FromBody] BikeDto bikeDto) if (!ModelState.IsValid) { - logger.LogWarning("Invalid bike data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid bike data: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var id = service.CreateBike(bikeDto); logger.LogInformation("Created bike with ID {BikeId}", id); - return CreatedAtAction(nameof(GetBike), new { id }, new { id, message = "Bike created successfully." }); + return CreatedAtAction( + nameof(GetBike), + new { id }, + new { id, message = "Bike created successfully." }); } catch (ArgumentException ex) { logger.LogWarning(ex, "Error creating bike: {ErrorMessage}", ex.Message); - return BadRequest(new { error = ex.Message }); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error creating bike"); - return StatusCode(500, new { error = "An error occurred while creating the bike." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while creating the bike.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -103,9 +132,12 @@ public IActionResult CreateBike([FromBody] BikeDto bikeDto) /// /// /// - /// [HttpPut("{id:int}")] - public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) + [ProducesResponseType(typeof(BikeDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) { try { @@ -113,29 +145,43 @@ public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) if (!ModelState.IsValid) { - logger.LogWarning("Invalid bike data for update: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid bike data for update: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var bike = service.UpdateBike(id, bikeDto); if (bike == null) { logger.LogWarning("Bike with ID {BikeId} not found for update", id); - return NotFound(new { error = $"Bike with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Updated bike with ID {BikeId}", id); - return Ok(new { message = "Bike updated successfully.", bike }); + return Ok(bike); } catch (ArgumentException ex) { logger.LogWarning(ex, "Error updating bike: {ErrorMessage}", ex.Message); - return BadRequest(new { error = ex.Message }); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error updating bike with ID {BikeId}", id); - return StatusCode(500, new { error = "An error occurred while updating the bike." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while updating the bike.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -143,9 +189,11 @@ public IActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) /// Deletes an existing object by id /// /// - /// [HttpDelete("{id:int}")] - public IActionResult DeleteBike(int id) + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult DeleteBike(int id) { try { @@ -155,7 +203,10 @@ public IActionResult DeleteBike(int id) if (!result) { logger.LogWarning("Bike with ID {BikeId} not found for deletion", id); - return NotFound(new { error = $"Bike with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Bike with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Deleted bike with ID {BikeId}", id); @@ -164,7 +215,10 @@ public IActionResult DeleteBike(int id) catch (Exception ex) { logger.LogError(ex, "Error deleting bike with ID {BikeId}", id); - return StatusCode(500, new { error = "An error occurred while deleting the bike." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while deleting the bike.", + statusCode: StatusCodes.Status500InternalServerError); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs index bf8590763..21d76d6be 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; @@ -11,14 +11,16 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] +[Produces("application/json")] public class RentersController(IRenterService service, ILogger logger) : ControllerBase { /// /// Returns all existing objects /// - /// [HttpGet] - public IActionResult GetAllRenters() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetAllRenters() { try { @@ -30,7 +32,10 @@ public IActionResult GetAllRenters() catch (Exception ex) { logger.LogError(ex, "Error getting all renters"); - return StatusCode(500, new { error = "An error occurred while retrieving renters." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving renters.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -38,9 +43,11 @@ public IActionResult GetAllRenters() /// Returns object by id /// /// - /// [HttpGet("{id:int}")] - public IActionResult GetRenter(int id) + [ProducesResponseType(typeof(RenterDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult GetRenter(int id) { try { @@ -50,7 +57,10 @@ public IActionResult GetRenter(int id) if (renter == null) { logger.LogWarning("Renter with ID {RenterId} not found", id); - return NotFound(new { error = $"Renter with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Renter with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } return Ok(renter); @@ -58,17 +68,22 @@ public IActionResult GetRenter(int id) catch (Exception ex) { logger.LogError(ex, "Error getting renter with ID {RenterId}", id); - return StatusCode(500, new { error = "An error occurred while retrieving the renter." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving the renter.", + statusCode: StatusCodes.Status500InternalServerError); } } /// - /// Creates a new objec + /// Creates a new object /// /// - /// [HttpPost] - public IActionResult CreateRenter([FromBody] RenterDto renterDto) + [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult CreateRenter([FromBody] RenterDto renterDto) { try { @@ -76,19 +91,38 @@ public IActionResult CreateRenter([FromBody] RenterDto renterDto) if (!ModelState.IsValid) { - logger.LogWarning("Invalid renter data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid renter data: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var id = service.CreateRenter(renterDto); logger.LogInformation("Created renter with ID {RenterId}", id); - return CreatedAtAction(nameof(GetRenter), new { id }, new { id, message = "Renter created successfully." }); + return CreatedAtAction( + nameof(GetRenter), + new { id }, + new { id, message = "Renter created successfully." }); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error creating renter: {ErrorMessage}", ex.Message); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error creating renter"); - return StatusCode(500, new { error = "An error occurred while creating the renter." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while creating the renter.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -97,9 +131,12 @@ public IActionResult CreateRenter([FromBody] RenterDto renterDto) /// /// /// - /// [HttpPut("{id:int}")] - public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) + [ProducesResponseType(typeof(RenterDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) { try { @@ -107,23 +144,43 @@ public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) if (!ModelState.IsValid) { - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid renter data for update: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var renter = service.UpdateRenter(id, renterDto); if (renter == null) { logger.LogWarning("Renter with ID {RenterId} not found for update", id); - return NotFound(new { error = $"Renter with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Renter with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Updated renter with ID {RenterId}", id); - return Ok(new { message = "Renter updated successfully.", renter }); + return Ok(renter); + } + catch (ArgumentException ex) + { + logger.LogWarning(ex, "Error updating renter: {ErrorMessage}", ex.Message); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error updating renter with ID {RenterId}", id); - return StatusCode(500, new { error = "An error occurred while updating the renter." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while updating the renter.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -131,9 +188,11 @@ public IActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) /// Deletes an existing object by id /// /// - /// [HttpDelete("{id:int}")] - public IActionResult DeleteRenter(int id) + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult DeleteRenter(int id) { try { @@ -143,7 +202,10 @@ public IActionResult DeleteRenter(int id) if (!result) { logger.LogWarning("Renter with ID {RenterId} not found for deletion", id); - return NotFound(new { error = $"Renter with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Renter with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Deleted renter with ID {RenterId}", id); @@ -152,7 +214,10 @@ public IActionResult DeleteRenter(int id) catch (Exception ex) { logger.LogError(ex, "Error deleting renter with ID {RenterId}", id); - return StatusCode(500, new { error = "An error occurred while deleting the renter." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while deleting the renter.", + statusCode: StatusCodes.Status500InternalServerError); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs index d5d75e781..0f350e80c 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; @@ -11,14 +11,16 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] +[Produces("application/json")] public class RentsController(IRentService service, ILogger logger) : ControllerBase { /// /// Returns all existing objects /// - /// [HttpGet] - public IActionResult GetAllRents() + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult> GetAllRents() { try { @@ -30,7 +32,10 @@ public IActionResult GetAllRents() catch (Exception ex) { logger.LogError(ex, "Error getting all rents"); - return StatusCode(500, new { error = "An error occurred while retrieving rents." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving rents.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -38,9 +43,11 @@ public IActionResult GetAllRents() /// Returns object by id /// /// - /// [HttpGet("{id:int}")] - public IActionResult GetRent(int id) + [ProducesResponseType(typeof(RentDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult GetRent(int id) { try { @@ -50,7 +57,10 @@ public IActionResult GetRent(int id) if (rent == null) { logger.LogWarning("Rent with ID {RentId} not found", id); - return NotFound(new { error = $"Rent with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Rent with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } return Ok(rent); @@ -58,7 +68,10 @@ public IActionResult GetRent(int id) catch (Exception ex) { logger.LogError(ex, "Error getting rent with ID {RentId}", id); - return StatusCode(500, new { error = "An error occurred while retrieving the rent." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while retrieving the rent.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -66,9 +79,11 @@ public IActionResult GetRent(int id) /// Creates a new object /// /// - /// [HttpPost] - public IActionResult CreateRent([FromBody] RentDto rentDto) + [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult CreateRent([FromBody] RentDto rentDto) { try { @@ -77,24 +92,38 @@ public IActionResult CreateRent([FromBody] RentDto rentDto) if (!ModelState.IsValid) { - logger.LogWarning("Invalid rent data: {ModelErrors}", ModelState.Values.SelectMany(v => v.Errors)); - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid rent data: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var id = service.CreateRent(rentDto); logger.LogInformation("Created rent with ID {RentId}", id); - return CreatedAtAction(nameof(GetRent), new { id }, new { id, message = "Rent created successfully." }); + return CreatedAtAction( + nameof(GetRent), + new { id }, + new { id, message = "Rent created successfully." }); } catch (ArgumentException ex) { logger.LogWarning(ex, "Error creating rent: {ErrorMessage}", ex.Message); - return BadRequest(new { error = ex.Message }); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error creating rent"); - return StatusCode(500, new { error = "An error occurred while creating the rent." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while creating the rent.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -103,9 +132,12 @@ public IActionResult CreateRent([FromBody] RentDto rentDto) /// /// /// - /// [HttpPut("{id:int}")] - public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) + [ProducesResponseType(typeof(RentDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult UpdateRent(int id, [FromBody] RentDto rentDto) { try { @@ -113,28 +145,43 @@ public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) if (!ModelState.IsValid) { - return BadRequest(new { errors = ModelState.Values.SelectMany(v => v.Errors) }); + logger.LogWarning("Invalid rent data for update: {ModelErrors}", + ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); + + return ValidationProblem( + title: "Validation Error", + detail: "One or more validation errors occurred.", + modelStateDictionary: ModelState); } var rent = service.UpdateRent(id, rentDto); if (rent == null) { logger.LogWarning("Rent with ID {RentId} not found for update", id); - return NotFound(new { error = $"Rent with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Rent with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Updated rent with ID {RentId}", id); - return Ok(new { message = "Rent updated successfully.", rent }); + return Ok(rent); } catch (ArgumentException ex) { logger.LogWarning(ex, "Error updating rent: {ErrorMessage}", ex.Message); - return BadRequest(new { error = ex.Message }); + return Problem( + title: "Bad Request", + detail: ex.Message, + statusCode: StatusCodes.Status400BadRequest); } catch (Exception ex) { logger.LogError(ex, "Error updating rent with ID {RentId}", id); - return StatusCode(500, new { error = "An error occurred while updating the rent." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while updating the rent.", + statusCode: StatusCodes.Status500InternalServerError); } } @@ -142,9 +189,11 @@ public IActionResult UpdateRent(int id, [FromBody] RentDto rentDto) /// Deletes an existing object by id /// /// - /// [HttpDelete("{id:int}")] - public IActionResult DeleteRent(int id) + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public ActionResult DeleteRent(int id) { try { @@ -154,7 +203,10 @@ public IActionResult DeleteRent(int id) if (!result) { logger.LogWarning("Rent with ID {RentId} not found for deletion", id); - return NotFound(new { error = $"Rent with ID {id} not found." }); + return Problem( + title: "Not Found", + detail: $"Rent with ID {id} not found.", + statusCode: StatusCodes.Status404NotFound); } logger.LogInformation("Deleted rent with ID {RentId}", id); @@ -163,7 +215,10 @@ public IActionResult DeleteRent(int id) catch (Exception ex) { logger.LogError(ex, "Error deleting rent with ID {RentId}", id); - return StatusCode(500, new { error = "An error occurred while deleting the rent." }); + return Problem( + title: "Internal Server Error", + detail: "An error occurred while deleting the rent.", + statusCode: StatusCodes.Status500InternalServerError); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs index bb5dabc1b..b727863ca 100644 --- a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs @@ -16,6 +16,8 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddBikeRentalServices(this IServiceCollection services) { + services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + services.AddSingleton, InMemoryBikeRepository>(); services.AddSingleton, InMemoryBikeModelRepository>(); services.AddSingleton, InMemoryRenterRepository>(); diff --git a/Bikes/Bikes.Application/Bikes.Application.csproj b/Bikes/Bikes.Application/Bikes.Application.csproj index 30da6d385..3d607fb33 100644 --- a/Bikes/Bikes.Application/Bikes.Application.csproj +++ b/Bikes/Bikes.Application/Bikes.Application.csproj @@ -7,7 +7,13 @@ + + + + + + diff --git a/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs index 7b4fd88f5..c46d236a3 100644 --- a/Bikes/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -1,4 +1,5 @@ -using Bikes.Domain.Models; +using Bikes.Contracts.Dto; +using Bikes.Domain.Models; using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -6,51 +7,36 @@ namespace Bikes.Application.Services; /// /// A class that implements the interface of the AnalyticsService class /// -public class AnalyticsService : IAnalyticsService +public class AnalyticsService( + IRepository bikeRepository, + IRepository bikeModelRepository, + IRepository rentRepository, + IRepository renterRepository) : IAnalyticsService { - private readonly IRepository _bikeRepository; - private readonly IRepository _bikeModelRepository; - private readonly IRepository _rentRepository; - private readonly IRepository _renterRepository; - - /// - /// The constructor that initializes repositories - /// - /// - /// - /// - /// - public AnalyticsService( - IRepository bikeRepository, - IRepository bikeModelRepository, - IRepository rentRepository, - IRepository renterRepository) - { - _bikeRepository = bikeRepository; - _bikeModelRepository = bikeModelRepository; - _rentRepository = rentRepository; - _renterRepository = renterRepository; - } - /// /// A method that returns information about all sports bikes /// - public List GetSportBikes() + public List GetSportBikes() { - return _bikeRepository.ReadAll() + return [.. bikeRepository.ReadAll() .Where(bike => bike.Model.Type == BikeType.Sport) - .ToList(); + .Select(bike => new BikeDto + { + SerialNumber = bike.SerialNumber, + Color = bike.Color, + ModelId = bike.Model.Id + })]; } /// /// A method that returns the top 5 bike models by rental duration /// - public List GetTopFiveModelsByRentDuration() + public List GetTopFiveModelsByRentDuration() { - var rents = _rentRepository.ReadAll(); - var models = _bikeModelRepository.ReadAll(); + var rents = rentRepository.ReadAll(); + var models = bikeModelRepository.ReadAll(); - return rents + return [.. rents .GroupBy(rent => rent.Bike.Model.Id) .Select(group => new { @@ -62,19 +48,27 @@ public List GetTopFiveModelsByRentDuration() .Join(models, x => x.ModelId, model => model.Id, - (x, model) => model) - .ToList(); + (x, model) => new BikeModelDto + { + Type = model.Type, + WheelSize = model.WheelSize, + MaxPassengerWeight = model.MaxPassengerWeight, + Weight = model.Weight, + BrakeType = model.BrakeType, + Year = model.Year, + RentPrice = model.RentPrice + })]; } /// /// A method that returns the top 5 bike models in terms of rental income /// - public List GetTopFiveModelsByProfit() + public List GetTopFiveModelsByProfit() { - var rents = _rentRepository.ReadAll(); - var models = _bikeModelRepository.ReadAll(); + var rents = rentRepository.ReadAll(); + var models = bikeModelRepository.ReadAll(); - return rents + return [.. rents .GroupBy(rent => rent.Bike.Model.Id) .Select(group => new { @@ -83,20 +77,35 @@ public List GetTopFiveModelsByProfit() }) .OrderByDescending(x => x.TotalProfit) .Take(5) - .Join(models, x => x.ModelId, model => model.Id, (x, model) => model) - .ToList(); + .Join(models, + x => x.ModelId, + model => model.Id, + (x, model) => new BikeModelDto + { + Type = model.Type, + WheelSize = model.WheelSize, + MaxPassengerWeight = model.MaxPassengerWeight, + Weight = model.Weight, + BrakeType = model.BrakeType, + Year = model.Year, + RentPrice = model.RentPrice + })]; } /// /// A method that returns information about the minimum, maximum, and average bike rental time. /// - public (int min, int max, double avg) GetRentalDurationStats() + public RentalDurationStatsDto GetRentalDurationStats() { - var durations = _rentRepository.ReadAll() - .Select(rent => rent.RentalDuration) - .ToList(); + List durations = [.. rentRepository.ReadAll() + .Select(rent => rent.RentalDuration)]; - return (durations.Min(), durations.Max(), durations.Average()); + return new RentalDurationStatsDto + { + Min = durations.Min(), + Max = durations.Max(), + Average = durations.Average() + }; } /// @@ -104,7 +113,7 @@ public List GetTopFiveModelsByProfit() /// public Dictionary GetTotalRentalTimeByType() { - return _rentRepository.ReadAll() + return rentRepository.ReadAll() .GroupBy(rent => rent.Bike.Model.Type) .ToDictionary( group => group.Key, @@ -115,11 +124,11 @@ public Dictionary GetTotalRentalTimeByType() /// /// A method that returns information about the customers who have rented bicycles the most times. /// - public List GetTopThreeRenters() + public List GetTopThreeRenters() { - var renters = _renterRepository.ReadAll(); + var renters = renterRepository.ReadAll(); - return _rentRepository.ReadAll() + return [.. rentRepository.ReadAll() .GroupBy(rent => rent.Renter.Id) .Select(group => new { @@ -131,7 +140,10 @@ public List GetTopThreeRenters() .Join(renters, x => x.RenterId, renter => renter.Id, - (x, renter) => renter) - .ToList(); + (x, renter) => new RenterDto + { + FullName = renter.FullName, + Number = renter.Number + })]; } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeModelService.cs b/Bikes/Bikes.Application/Services/BikeModelService.cs index eca3d9065..95267b35f 100644 --- a/Bikes/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes/Bikes.Application/Services/BikeModelService.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Dto; +using AutoMapper; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -7,40 +8,10 @@ namespace Bikes.Application.Services; /// /// A class that implements the interface of the BikeModelService class /// -public class BikeModelService : IBikeModelService +public class BikeModelService( + IRepository bikeModelRepository, + IMapper mapper) : IBikeModelService { - private readonly IRepository _bikeModelRepository; - - /// - /// The constructor that initializes repositories - /// - /// - public BikeModelService(IRepository bikeModelRepository) - { - _bikeModelRepository = bikeModelRepository; - } - - /// - /// A method that maps a DTO object to a domain object - /// - /// - /// - /// - private static BikeModel MapToDomain(BikeModelDto dto, int id = 0) - { - return new BikeModel - { - Id = id, - Type = dto.Type, - WheelSize = dto.WheelSize, - MaxPassengerWeight = dto.MaxPassengerWeight, - Weight = dto.Weight, - BrakeType = dto.BrakeType, - Year = dto.Year, - RentPrice = dto.RentPrice - }; - } - /// /// Creates a new object /// @@ -48,22 +19,38 @@ private static BikeModel MapToDomain(BikeModelDto dto, int id = 0) /// ID of the created object public int CreateBikeModel(BikeModelDto bikeModelDto) { - var bikeModel = MapToDomain(bikeModelDto); - return _bikeModelRepository.Create(bikeModel); + var bikeModel = mapper.Map(bikeModelDto); + + return bikeModelRepository.Create(bikeModel); } /// /// Returns all existing objects /// /// List of existing objects - public List GetAllBikeModels() => _bikeModelRepository.ReadAll(); + public List GetAllBikeModels() + { + var models = bikeModelRepository.ReadAll(); + + return models.Select(model => + { + var dto = mapper.Map(model); + return dto; + }).ToList(); + } /// /// Returns object by id /// /// /// - public BikeModel? GetBikeModelById(int id) => _bikeModelRepository.Read(id); + public BikeModelDto? GetBikeModelById(int id) + { + var model = bikeModelRepository.Read(id); + if (model == null) return null; + + return mapper.Map(model); + } /// /// Updates an existing object @@ -71,13 +58,19 @@ public int CreateBikeModel(BikeModelDto bikeModelDto) /// Id /// DTO object /// Object if exist - public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto) + public BikeModelDto? UpdateBikeModel(int id, BikeModelDto bikeModelDto) { - var existingModel = _bikeModelRepository.Read(id); + var existingModel = bikeModelRepository.Read(id); if (existingModel == null) return null; - var updatedModel = MapToDomain(bikeModelDto, id); - return _bikeModelRepository.Update(id, updatedModel); + mapper.Map(bikeModelDto, existingModel); + + existingModel.Id = id; + + var updatedModel = bikeModelRepository.Update(id, existingModel); + if (updatedModel == null) return null; + + return mapper.Map(updatedModel); } /// @@ -85,5 +78,5 @@ public int CreateBikeModel(BikeModelDto bikeModelDto) /// /// /// True or false? result of deleting - public bool DeleteBikeModel(int id) => _bikeModelRepository.Delete(id); + public bool DeleteBikeModel(int id) => bikeModelRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs index 83b97bb91..dc2c1c954 100644 --- a/Bikes/Bikes.Application/Services/BikeService.cs +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Dto; +using AutoMapper; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -7,42 +8,11 @@ namespace Bikes.Application.Services; /// /// A class that implements the interface of the BikeService class /// -public class BikeService : IBikeService +public class BikeService( + IRepository bikeRepository, + IRepository bikeModelRepository, + IMapper mapper) : IBikeService { - private readonly IRepository _bikeRepository; - private readonly IRepository _bikeModelRepository; - - /// - /// The constructor that initializes repositories - /// - /// - /// - public BikeService( - IRepository bikeRepository, - IRepository bikeModelRepository) - { - _bikeRepository = bikeRepository; - _bikeModelRepository = bikeModelRepository; - } - - /// - /// A method that maps a DTO object to a domain object - /// - /// - /// - /// - /// - private static Bike MapToDomain(BikeDto dto, BikeModel model, int id = 0) - { - return new Bike - { - Id = id, - SerialNumber = dto.SerialNumber, - Color = dto.Color, - Model = model - }; - } - /// /// Creates a new object /// @@ -50,26 +20,35 @@ private static Bike MapToDomain(BikeDto dto, BikeModel model, int id = 0) /// ID of the created object public int CreateBike(BikeDto bikeDto) { - var model = _bikeModelRepository.Read(bikeDto.ModelId); - if (model == null) - throw new ArgumentException($"BikeModel with id {bikeDto.ModelId} not found"); + var model = bikeModelRepository.Read(bikeDto.ModelId) + ?? throw new ArgumentException($"BikeModel with id {bikeDto.ModelId} not found"); - var bike = MapToDomain(bikeDto, model); - return _bikeRepository.Create(bike); + var bike = mapper.Map(bikeDto); + bike.Model = model; + + return bikeRepository.Create(bike); } /// /// Returns all existing objects /// /// List of existing objects - public List GetAllBikes() => _bikeRepository.ReadAll(); + public List GetAllBikes() + { + var bikes = bikeRepository.ReadAll(); + return mapper.Map>(bikes); + } /// /// Returns object by id /// /// /// - public Bike? GetBikeById(int id) => _bikeRepository.Read(id); + public BikeDto? GetBikeById(int id) + { + var bike = bikeRepository.Read(id); + return bike != null ? mapper.Map(bike) : null; + } /// /// Updates an existing object @@ -77,16 +56,20 @@ public int CreateBike(BikeDto bikeDto) /// Id /// DTO object /// Object if exist - public Bike? UpdateBike(int id, BikeDto bikeDto) + public BikeDto? UpdateBike(int id, BikeDto bikeDto) { - var existingBike = _bikeRepository.Read(id); + var existingBike = bikeRepository.Read(id); if (existingBike == null) return null; - var model = _bikeModelRepository.Read(bikeDto.ModelId); + var model = bikeModelRepository.Read(bikeDto.ModelId); if (model == null) return null; - var updatedBike = MapToDomain(bikeDto, model, id); - return _bikeRepository.Update(id, updatedBike); + mapper.Map(bikeDto, existingBike); + + existingBike.Model = model; + + var updatedBike = bikeRepository.Update(id, existingBike); + return updatedBike != null ? mapper.Map(updatedBike) : null; } /// @@ -94,5 +77,5 @@ public int CreateBike(BikeDto bikeDto) /// /// /// True or false? result of deleting - public bool DeleteBike(int id) => _bikeRepository.Delete(id); + public bool DeleteBike(int id) => bikeRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IAnalyticsService.cs b/Bikes/Bikes.Application/Services/IAnalyticsService.cs index 72c9f8fbc..e76267a41 100644 --- a/Bikes/Bikes.Application/Services/IAnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/IAnalyticsService.cs @@ -1,4 +1,5 @@ -using Bikes.Domain.Models; +using Bikes.Contracts.Dto; +using Bikes.Domain.Models; namespace Bikes.Application.Services; @@ -10,22 +11,22 @@ public interface IAnalyticsService /// /// A method that returns information about all sports bikes /// - public List GetSportBikes(); + public List GetSportBikes(); /// /// A method that returns the top 5 bike models by rental duration /// - public List GetTopFiveModelsByRentDuration(); + public List GetTopFiveModelsByRentDuration(); /// /// A method that returns the top 5 bike models in terms of rental income /// - public List GetTopFiveModelsByProfit(); + public List GetTopFiveModelsByProfit(); /// /// A method that returns information about the minimum, maximum, and average bike rental time. /// - public (int min, int max, double avg) GetRentalDurationStats(); + public RentalDurationStatsDto GetRentalDurationStats(); /// /// A method that returns the total rental time of each type of bike @@ -35,5 +36,5 @@ public interface IAnalyticsService /// /// A method that returns information about the customers who have rented bicycles the most times. /// - public List GetTopThreeRenters(); + public List GetTopThreeRenters(); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeModelService.cs b/Bikes/Bikes.Application/Services/IBikeModelService.cs index c16eddb6b..c94ffb8fa 100644 --- a/Bikes/Bikes.Application/Services/IBikeModelService.cs +++ b/Bikes/Bikes.Application/Services/IBikeModelService.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; namespace Bikes.Application.Services; @@ -19,14 +19,14 @@ public interface IBikeModelService /// Returns all existing objects /// /// List of existing objects - public List GetAllBikeModels(); + public List GetAllBikeModels(); /// /// Returns object by id /// /// /// - public BikeModel? GetBikeModelById(int id); + public BikeModelDto? GetBikeModelById(int id); /// /// Updates an existing object @@ -34,7 +34,7 @@ public interface IBikeModelService /// Id /// DTO object /// Object if exist - public BikeModel? UpdateBikeModel(int id, BikeModelDto bikeModelDto); + public BikeModelDto? UpdateBikeModel(int id, BikeModelDto bikeModelDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IBikeService.cs b/Bikes/Bikes.Application/Services/IBikeService.cs index d7000be92..395eed070 100644 --- a/Bikes/Bikes.Application/Services/IBikeService.cs +++ b/Bikes/Bikes.Application/Services/IBikeService.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; namespace Bikes.Application.Services; @@ -19,14 +19,14 @@ public interface IBikeService /// Returns all existing objects /// /// List of existing objects - public List GetAllBikes(); + public List GetAllBikes(); /// /// Returns object by id /// /// /// - public Bike? GetBikeById(int id); + public BikeDto? GetBikeById(int id); /// /// Updates an existing object @@ -34,7 +34,7 @@ public interface IBikeService /// Id /// DTO object /// Object if exist - public Bike? UpdateBike(int id, BikeDto bikeDto); + public BikeDto? UpdateBike(int id, BikeDto bikeDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IRentService.cs b/Bikes/Bikes.Application/Services/IRentService.cs index 764df56db..402482838 100644 --- a/Bikes/Bikes.Application/Services/IRentService.cs +++ b/Bikes/Bikes.Application/Services/IRentService.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; namespace Bikes.Application.Services; @@ -19,14 +19,14 @@ public interface IRentService /// Returns all existing objects /// /// List of existing objects - public List GetAllRents(); + public List GetAllRents(); /// /// Returns object by id /// /// /// - public Rent? GetRentById(int id); + public RentDto? GetRentById(int id); /// /// Updates an existing object @@ -34,7 +34,7 @@ public interface IRentService /// Id /// DTO object /// Object if exist - public Rent? UpdateRent(int id, RentDto rentDto); + public RentDto? UpdateRent(int id, RentDto rentDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IRenterService.cs b/Bikes/Bikes.Application/Services/IRenterService.cs index 283888105..b20e3809f 100644 --- a/Bikes/Bikes.Application/Services/IRenterService.cs +++ b/Bikes/Bikes.Application/Services/IRenterService.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Dto; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; namespace Bikes.Application.Services; @@ -19,14 +19,14 @@ public interface IRenterService /// Returns all existing objects /// /// List of existing objects - public List GetAllRenters(); + public List GetAllRenters(); /// /// Returns object by id /// /// /// - public Renter? GetRenterById(int id); + public RenterDto? GetRenterById(int id); /// /// Updates an existing object @@ -34,7 +34,7 @@ public interface IRenterService /// Id /// DTO object /// Object if exist - public Renter? UpdateRenter(int id, RenterDto renterDto); + public RenterDto? UpdateRenter(int id, RenterDto renterDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs index 745d1c15b..5867371f0 100644 --- a/Bikes/Bikes.Application/Services/RentService.cs +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Dto; +using AutoMapper; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -7,48 +8,12 @@ namespace Bikes.Application.Services; /// /// A class that implements the interface of the RentService class /// -public class RentService : IRentService +public class RentService( + IRepository rentRepository, + IRepository bikeRepository, + IRepository renterRepository, + IMapper mapper) : IRentService { - private readonly IRepository _rentRepository; - private readonly IRepository _bikeRepository; - private readonly IRepository _renterRepository; - - /// - /// The constructor that initializes repositories - /// - /// - /// - /// - public RentService( - IRepository rentRepository, - IRepository bikeRepository, - IRepository renterRepository) - { - _rentRepository = rentRepository; - _bikeRepository = bikeRepository; - _renterRepository = renterRepository; - } - - /// - /// A method that maps a DTO object to a domain object - /// - /// - /// - /// - /// - /// - private static Rent MapToDomain(RentDto dto, Bike bike, Renter renter, int id = 0) - { - return new Rent - { - Id = id, - RentalStartTime = dto.RentalStartTime, - RentalDuration = dto.RentalDuration, - Bike = bike, - Renter = renter - }; - } - /// /// Creates a new object /// @@ -57,30 +22,39 @@ private static Rent MapToDomain(RentDto dto, Bike bike, Renter renter, int id = /// public int CreateRent(RentDto rentDto) { - var bike = _bikeRepository.Read(rentDto.BikeId); - if (bike == null) - throw new ArgumentException($"Bike with id {rentDto.BikeId} not found"); + var bike = bikeRepository.Read(rentDto.BikeId) + ?? throw new ArgumentException($"Bike with id {rentDto.BikeId} not found"); - var renter = _renterRepository.Read(rentDto.RenterId); - if (renter == null) - throw new ArgumentException($"Renter with id {rentDto.RenterId} not found"); + var renter = renterRepository.Read(rentDto.RenterId) + ?? throw new ArgumentException($"Renter with id {rentDto.RenterId} not found"); - var rent = MapToDomain(rentDto, bike, renter); - return _rentRepository.Create(rent); + var rent = mapper.Map(rentDto); + rent.Bike = bike; + rent.Renter = renter; + + return rentRepository.Create(rent); } /// /// Returns all existing objects /// /// - public List GetAllRents() => _rentRepository.ReadAll(); + public List GetAllRents() + { + var rents = rentRepository.ReadAll(); + return mapper.Map>(rents); + } /// /// Returns object by id /// /// /// - public Rent? GetRentById(int id) => _rentRepository.Read(id); + public RentDto? GetRentById(int id) + { + var rent = rentRepository.Read(id); + return rent != null ? mapper.Map(rent) : null; + } /// /// Updates an existing object @@ -88,19 +62,24 @@ public int CreateRent(RentDto rentDto) /// /// /// - public Rent? UpdateRent(int id, RentDto rentDto) + public RentDto? UpdateRent(int id, RentDto rentDto) { - var existingRent = _rentRepository.Read(id); + var existingRent = rentRepository.Read(id); if (existingRent == null) return null; - var bike = _bikeRepository.Read(rentDto.BikeId); + var bike = bikeRepository.Read(rentDto.BikeId); if (bike == null) return null; - var renter = _renterRepository.Read(rentDto.RenterId); + var renter = renterRepository.Read(rentDto.RenterId); if (renter == null) return null; - var updatedRent = MapToDomain(rentDto, bike, renter, id); - return _rentRepository.Update(id, updatedRent); + mapper.Map(rentDto, existingRent); + + existingRent.Bike = bike; + existingRent.Renter = renter; + + var updatedRent = rentRepository.Update(id, existingRent); + return updatedRent != null ? mapper.Map(updatedRent) : null; } /// @@ -108,5 +87,5 @@ public int CreateRent(RentDto rentDto) /// /// /// - public bool DeleteRent(int id) => _rentRepository.Delete(id); + public bool DeleteRent(int id) => rentRepository.Delete(id); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/RenterService.cs b/Bikes/Bikes.Application/Services/RenterService.cs index db0d4e64d..e936e5322 100644 --- a/Bikes/Bikes.Application/Services/RenterService.cs +++ b/Bikes/Bikes.Application/Services/RenterService.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Dto; +using AutoMapper; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -7,35 +8,10 @@ namespace Bikes.Application.Services; /// /// A class that implements the interface of the RenterService class /// -public class RenterService : IRenterService +public class RenterService( + IRepository renterRepository, + IMapper mapper) : IRenterService { - private readonly IRepository _renterRepository; - - /// - /// The constructor that initializes repositories - /// - /// - public RenterService(IRepository renterRepository) - { - _renterRepository = renterRepository; - } - - /// - /// A method that maps a DTO object to a domain object - /// - /// - /// - /// - private static Renter MapToDomain(RenterDto dto, int id = 0) - { - return new Renter - { - Id = id, - FullName = dto.FullName, - Number = dto.Number - }; - } - /// /// Creates a new object /// @@ -43,22 +19,30 @@ private static Renter MapToDomain(RenterDto dto, int id = 0) /// public int CreateRenter(RenterDto renterDto) { - var renter = MapToDomain(renterDto); - return _renterRepository.Create(renter); + var renter = mapper.Map(renterDto); + return renterRepository.Create(renter); } /// /// Returns all existing objects /// /// - public List GetAllRenters() => _renterRepository.ReadAll(); + public List GetAllRenters() + { + var renters = renterRepository.ReadAll(); + return mapper.Map>(renters); + } /// /// Returns object by id /// /// /// - public Renter? GetRenterById(int id) => _renterRepository.Read(id); + public RenterDto? GetRenterById(int id) + { + var renter = renterRepository.Read(id); + return renter != null ? mapper.Map(renter) : null; + } /// /// Updates an existing object @@ -66,13 +50,15 @@ public int CreateRenter(RenterDto renterDto) /// /// /// - public Renter? UpdateRenter(int id, RenterDto renterDto) + public RenterDto? UpdateRenter(int id, RenterDto renterDto) { - var existingRenter = _renterRepository.Read(id); + var existingRenter = renterRepository.Read(id); if (existingRenter == null) return null; - var updatedRenter = MapToDomain(renterDto, id); - return _renterRepository.Update(id, updatedRenter); + mapper.Map(renterDto, existingRenter); + + var updatedRenter = renterRepository.Update(id, existingRenter); + return updatedRenter != null ? mapper.Map(updatedRenter) : null; } /// @@ -80,5 +66,5 @@ public int CreateRenter(RenterDto renterDto) /// /// /// - public bool DeleteRenter(int id) => _renterRepository.Delete(id); -} + public bool DeleteRenter(int id) => renterRepository.Delete(id); +} \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Bikes.Contracts.csproj b/Bikes/Bikes.Contracts/Bikes.Contracts.csproj new file mode 100644 index 000000000..30da6d385 --- /dev/null +++ b/Bikes/Bikes.Contracts/Bikes.Contracts.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Bikes/Bikes.Application/Dto/BikeDto.cs b/Bikes/Bikes.Contracts/Dto/BikeDto.cs similarity index 92% rename from Bikes/Bikes.Application/Dto/BikeDto.cs rename to Bikes/Bikes.Contracts/Dto/BikeDto.cs index 48f8c0ed9..b8bae5098 100644 --- a/Bikes/Bikes.Application/Dto/BikeDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeDto.cs @@ -1,4 +1,4 @@ -namespace Bikes.Application.Dto; +namespace Bikes.Contracts.Dto; /// /// DTO class for the Bike class diff --git a/Bikes/Bikes.Application/Dto/BikeModelDto.cs b/Bikes/Bikes.Contracts/Dto/BikeModelDto.cs similarity index 96% rename from Bikes/Bikes.Application/Dto/BikeModelDto.cs rename to Bikes/Bikes.Contracts/Dto/BikeModelDto.cs index 6231c76db..3d8b338ae 100644 --- a/Bikes/Bikes.Application/Dto/BikeModelDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeModelDto.cs @@ -1,6 +1,6 @@ using Bikes.Domain.Models; -namespace Bikes.Application.Dto; +namespace Bikes.Contracts.Dto; /// /// DTO class for the BikeModel class diff --git a/Bikes/Bikes.Application/Dto/RentDto.cs b/Bikes/Bikes.Contracts/Dto/RentDto.cs similarity index 93% rename from Bikes/Bikes.Application/Dto/RentDto.cs rename to Bikes/Bikes.Contracts/Dto/RentDto.cs index 856bc8946..a9b129f71 100644 --- a/Bikes/Bikes.Application/Dto/RentDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RentDto.cs @@ -1,4 +1,4 @@ -namespace Bikes.Application.Dto; +namespace Bikes.Contracts.Dto; /// /// DTO class for the Rent class diff --git a/Bikes/Bikes.Contracts/Dto/RentalDurationStatsDto.cs b/Bikes/Bikes.Contracts/Dto/RentalDurationStatsDto.cs new file mode 100644 index 000000000..68e9cf8b9 --- /dev/null +++ b/Bikes/Bikes.Contracts/Dto/RentalDurationStatsDto.cs @@ -0,0 +1,22 @@ +namespace Bikes.Contracts.Dto; + +/// +/// DTO class for rental duration statistics +/// +public class RentalDurationStatsDto +{ + /// + /// Minimum rental duration + /// + public required int Min { get; set; } + + /// + /// Maximum rental duration + /// + public required int Max { get; set; } + + /// + /// Average rental duration + /// + public required double Average { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Application/Dto/RenterDto.cs b/Bikes/Bikes.Contracts/Dto/RenterDto.cs similarity index 89% rename from Bikes/Bikes.Application/Dto/RenterDto.cs rename to Bikes/Bikes.Contracts/Dto/RenterDto.cs index 7228ca991..2f02cc235 100644 --- a/Bikes/Bikes.Application/Dto/RenterDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RenterDto.cs @@ -1,4 +1,4 @@ -namespace Bikes.Application.Dto; +namespace Bikes.Contracts.Dto; /// /// DTO class for the Renter class diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs index 1168ea7db..367615b06 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs @@ -9,7 +9,7 @@ namespace Bikes.Infrastructure.InMemory.Repositories; /// public class InMemoryBikeModelRepository : IRepository { - private readonly List _items = []; + private readonly List _items; private int _currentId; diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs index 7a5569750..a1e38a52c 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs @@ -9,7 +9,7 @@ namespace Bikes.Infrastructure.InMemory.Repositories; /// public class InMemoryBikeRepository : IRepository { - private readonly List _items = []; + private readonly List _items; private int _currentId; diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs index c6c4636a6..7d85a817c 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs @@ -9,7 +9,7 @@ namespace Bikes.Infrastructure.InMemory.Repositories; /// public class InMemoryRentRepository : IRepository { - private readonly List _items = []; + private readonly List _items; private int _currentId; diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs index ac3643cd7..6fd9ff419 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs @@ -9,7 +9,7 @@ namespace Bikes.Infrastructure.InMemory.Repositories; /// public class InMemoryRenterRepository : IRepository { - private readonly List _items = []; + private readonly List _items; private int _currentId; diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 24229534b..7c1df05e6 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -23,4 +23,4 @@ public BikesFixture() AnalyticsService = new AnalyticsService(bikeRepo, modelRepo, rentRepo, renterRepo); } -} \ No newline at end of file +} diff --git a/Bikes/Bikes.Tests/BikesTests.cs b/Bikes/Bikes.Tests/BikesTests.cs index 798c309aa..81ebb655f 100644 --- a/Bikes/Bikes.Tests/BikesTests.cs +++ b/Bikes/Bikes.Tests/BikesTests.cs @@ -13,12 +13,19 @@ public class BikesTests(BikesFixture fixture) : IClassFixture [Fact] public void InformationAboutSportBikes() { - var expectedBikeIds = new List { 2, 5, 8 }; + var expectedSerialNumbers = new List + { + "SPT202402001", // Bike Id 2 + "SPT202403001", // Bike Id 5 + "SPT202305001" // Bike Id 8 + }; var sportBikes = fixture.AnalyticsService.GetSportBikes(); - var actualIds = sportBikes.Select(bike => bike.Id).ToList(); - Assert.Equal(expectedBikeIds, actualIds); + Assert.Equal(3, sportBikes.Count); + + var actualSerialNumbers = sportBikes.Select(bike => bike.SerialNumber).ToList(); + Assert.Equal(expectedSerialNumbers, actualSerialNumbers); } /// @@ -27,12 +34,21 @@ public void InformationAboutSportBikes() [Fact] public void TopFiveModelsRentDurationIds() { - var expectedModelIds = new List { 10, 1, 2, 5, 3 }; + var expectedModelTypes = new List + { + BikeType.Mountain, // Model Id 10 + BikeType.Mountain, // Model Id 1 + BikeType.Sport, // Model Id 2 + BikeType.Sport, // Model Id 5 + BikeType.City // Model Id 3 + }; var topModels = fixture.AnalyticsService.GetTopFiveModelsByRentDuration(); - var actualIds = topModels.Select(model => model.Id).ToList(); - Assert.Equal(expectedModelIds, actualIds); + Assert.Equal(5, topModels.Count); + + var actualModelTypes = topModels.Select(model => model.Type).ToList(); + Assert.Equal(expectedModelTypes, actualModelTypes); } /// @@ -41,12 +57,21 @@ public void TopFiveModelsRentDurationIds() [Fact] public void TopFiveModelsProfit() { - var expectedModelIds = new List { 10, 5, 2, 1, 3 }; + var expectedModelTypes = new List + { + BikeType.Mountain, // Model Id 10 + BikeType.Sport, // Model Id 5 + BikeType.Sport, // Model Id 2 + BikeType.Mountain, // Model Id 1 + BikeType.City // Model Id 3 + }; var topModels = fixture.AnalyticsService.GetTopFiveModelsByProfit(); - var actualIds = topModels.Select(model => model.Id).ToList(); - Assert.Equal(expectedModelIds, actualIds); + Assert.Equal(5, topModels.Count); + + var actualModelTypes = topModels.Select(model => model.Type).ToList(); + Assert.Equal(expectedModelTypes, actualModelTypes); } /// @@ -59,11 +84,11 @@ public void MinMaxAvgRentalDuration() const int expectedMax = 5; const double expectedAvg = 2.95; - var (actualMin, actualMax, actualAvg) = fixture.AnalyticsService.GetRentalDurationStats(); + var stats = fixture.AnalyticsService.GetRentalDurationStats(); - Assert.Equal(expectedMin, actualMin); - Assert.Equal(expectedMax, actualMax); - Assert.Equal(expectedAvg, actualAvg); + Assert.Equal(expectedMin, stats.Min); + Assert.Equal(expectedMax, stats.Max); + Assert.Equal(expectedAvg, stats.Average); } /// @@ -87,11 +112,18 @@ public void TotalRentalTimeByType(BikeType bikeType, int expectedRentalTime) [Fact] public void TopThreeRenters() { - var expectedTopRentersIds = new List { 1, 2, 6 }; + var expectedFullNames = new List + { + "Иванов Иван Иванович", // Renter Id 1 + "Петров Петр Сергеевич", // Renter Id 2 + "Попов Денис Андреевич" // Renter Id 6 + }; var topRenters = fixture.AnalyticsService.GetTopThreeRenters(); - var actualTopRentersIds = topRenters.Select(renter => renter.Id).ToList(); - Assert.Equal(expectedTopRentersIds, actualTopRentersIds); + Assert.Equal(3, topRenters.Count); + + var actualFullNames = topRenters.Select(renter => renter.FullName).ToList(); + Assert.Equal(expectedFullNames, actualFullNames); } } \ No newline at end of file diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index 6e890ba46..dd70544cd 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api.Host\Bikes.Api.Host.csproj", "{B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Contracts", "Bikes.Contracts\Bikes.Contracts.csproj", "{2BE08B58-C908-406E-8B12-5EA2B44C39DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +44,10 @@ Global {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5E2CA3E-DD81-4B28-8783-1D1EE5352EF3}.Release|Any CPU.Build.0 = Release|Any CPU + {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c89309d92403aa294c7d72214bfa8033b4df2b51 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 10 Dec 2025 05:13:56 +0400 Subject: [PATCH 23/41] Fix api --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 2 -- .../Extensions/ServiceCollectionExtensions.cs | 2 +- Bikes/Bikes.Application/Bikes.Application.csproj | 2 +- .../Bikes.Application/Mapping/MappingProfile.cs | 16 ++++++++++++++++ 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 Bikes/Bikes.Application/Mapping/MappingProfile.cs diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index afe9119e7..577010821 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -7,8 +7,6 @@ - - diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs index b727863ca..00fcf2b8a 100644 --- a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddBikeRentalServices(this IServiceCollection services) { - services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + services.AddAutoMapper(typeof(BikeService).Assembly); services.AddSingleton, InMemoryBikeRepository>(); services.AddSingleton, InMemoryBikeModelRepository>(); diff --git a/Bikes/Bikes.Application/Bikes.Application.csproj b/Bikes/Bikes.Application/Bikes.Application.csproj index 3d607fb33..f8fdd9a05 100644 --- a/Bikes/Bikes.Application/Bikes.Application.csproj +++ b/Bikes/Bikes.Application/Bikes.Application.csproj @@ -12,7 +12,7 @@ - + diff --git a/Bikes/Bikes.Application/Mapping/MappingProfile.cs b/Bikes/Bikes.Application/Mapping/MappingProfile.cs new file mode 100644 index 000000000..32284ba0a --- /dev/null +++ b/Bikes/Bikes.Application/Mapping/MappingProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using Bikes.Contracts.Dto; +using Bikes.Domain.Models; + +namespace Bikes.Application.Mapping; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file From 8a078e8a3a70ed87fb4a820207eeaee97c74588e Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 10 Dec 2025 07:05:08 +0400 Subject: [PATCH 24/41] Fix dto's and small fixes --- .../Controllers/AnalyticsController.cs | 18 ++-- .../Controllers/BikeModelsController.cs | 16 ++-- .../Controllers/BikesController.cs | 16 ++-- .../Controllers/RentersController.cs | 16 ++-- .../Controllers/RentsController.cs | 16 ++-- .../Extensions/ServiceCollectionExtensions.cs | 11 ++- .../IAnalyticsService.cs | 10 +- .../IBikeModelService.cs | 11 +-- .../{Services => Interfaces}/IBikeService.cs | 11 +-- .../{Services => Interfaces}/IRentService.cs | 11 +-- .../IRenterService.cs | 11 +-- .../Mapping/MappingProfile.cs | 15 ++- .../Services/AnalyticsService.cs | 94 +++++++------------ .../Services/BikeModelService.cs | 20 ++-- .../Bikes.Application/Services/BikeService.cs | 15 +-- .../Bikes.Application/Services/RentService.cs | 15 +-- .../Services/RenterService.cs | 15 +-- .../{BikeDto.cs => BikeCreateUpdateDto.cs} | 4 +- Bikes/Bikes.Contracts/Dto/BikeGetDto.cs | 27 ++++++ ...odelDto.cs => BikeModelCreateUpdateDto.cs} | 4 +- Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs | 49 ++++++++++ .../{RentDto.cs => RentCreateUpdateDto.cs} | 4 +- Bikes/Bikes.Contracts/Dto/RentGetDto.cs | 34 +++++++ ...{RenterDto.cs => RenterCreateUpdateDto.cs} | 4 +- Bikes/Bikes.Contracts/Dto/RenterGetDto.cs | 22 +++++ .../InMemoryBikeModelRepository.cs | 2 +- .../Repositories/InMemoryBikeRepository.cs | 2 +- .../Repositories/InMemoryRentRepository.cs | 2 +- .../Repositories/InMemoryRenterRepository.cs | 2 +- Bikes/Bikes.Tests/BikesFixture.cs | 3 +- 30 files changed, 296 insertions(+), 184 deletions(-) rename Bikes/Bikes.Application/{Services => Interfaces}/IAnalyticsService.cs (79%) rename Bikes/Bikes.Application/{Services => Interfaces}/IBikeModelService.cs (76%) rename Bikes/Bikes.Application/{Services => Interfaces}/IBikeService.cs (79%) rename Bikes/Bikes.Application/{Services => Interfaces}/IRentService.cs (79%) rename Bikes/Bikes.Application/{Services => Interfaces}/IRenterService.cs (77%) rename Bikes/Bikes.Contracts/Dto/{BikeDto.cs => BikeCreateUpdateDto.cs} (83%) create mode 100644 Bikes/Bikes.Contracts/Dto/BikeGetDto.cs rename Bikes/Bikes.Contracts/Dto/{BikeModelDto.cs => BikeModelCreateUpdateDto.cs} (91%) create mode 100644 Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs rename Bikes/Bikes.Contracts/Dto/{RentDto.cs => RentCreateUpdateDto.cs} (86%) create mode 100644 Bikes/Bikes.Contracts/Dto/RentGetDto.cs rename Bikes/Bikes.Contracts/Dto/{RenterDto.cs => RenterCreateUpdateDto.cs} (77%) create mode 100644 Bikes/Bikes.Contracts/Dto/RenterGetDto.cs diff --git a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs index bf0d76b47..6c276b62a 100644 --- a/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -1,4 +1,4 @@ -using Bikes.Application.Services; +using Bikes.Application.Interfaces; using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Microsoft.AspNetCore.Mvc; @@ -21,9 +21,9 @@ public class AnalyticsController( /// A method that returns information about all sports bikes /// [HttpGet("sport-bikes")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetSportBikes() + public ActionResult> GetSportBikes() { try { @@ -46,9 +46,9 @@ public ActionResult> GetSportBikes() /// A method that returns the top 5 bike models by rental duration /// [HttpGet("top-models/duration")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetTopModelsByDuration() + public ActionResult> GetTopModelsByDuration() { try { @@ -71,9 +71,9 @@ public ActionResult> GetTopModelsByDuration() /// A method that returns the top 5 bike models in terms of rental income /// [HttpGet("top-models/profit")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetTopModelsByProfit() + public ActionResult> GetTopModelsByProfit() { try { @@ -148,9 +148,9 @@ public ActionResult> GetTotalRentalTimeByType() /// A method that returns information about the customers who have rented bicycles the most times. /// [HttpGet("top-renters")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetTopRenters() + public ActionResult> GetTopRenters() { try { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs index e61ff7d30..167954d72 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -1,6 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; +using Bikes.Application.Interfaces; namespace Bikes.Api.Host.Controllers; @@ -18,9 +18,9 @@ public class BikeModelsController(IBikeModelService service, ILogger [HttpGet] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetAllBikeModels() + public ActionResult> GetAllBikeModels() { try { @@ -44,10 +44,10 @@ public ActionResult> GetAllBikeModels() /// /// [HttpGet("{id:int}")] - [ProducesResponseType(typeof(BikeModelDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(BikeModelGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult GetBikeModel(int id) + public ActionResult GetBikeModel(int id) { try { @@ -83,7 +83,7 @@ public ActionResult GetBikeModel(int id) [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateBikeModel([FromBody] BikeModelDto bikeModelDto) + public ActionResult CreateBikeModel([FromBody] BikeModelCreateUpdateDto bikeModelDto) { try { @@ -124,11 +124,11 @@ public ActionResult CreateBikeModel([FromBody] BikeModelD /// /// [HttpPut("{id:int}")] - [ProducesResponseType(typeof(BikeModelDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(BikeModelGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult UpdateBikeModel(int id, [FromBody] BikeModelDto bikeModelDto) + public ActionResult UpdateBikeModel(int id, [FromBody] BikeModelCreateUpdateDto bikeModelDto) { try { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs index a39bc15c9..5df4bbbf2 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -1,6 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; +using Bikes.Application.Interfaces; namespace Bikes.Api.Host.Controllers; @@ -18,9 +18,9 @@ public class BikesController(IBikeService service, ILogger logg /// Returns all existing objects /// [HttpGet] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetAllBikes() + public ActionResult> GetAllBikes() { try { @@ -44,10 +44,10 @@ public ActionResult> GetAllBikes() /// /// [HttpGet("{id:int}")] - [ProducesResponseType(typeof(BikeDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(BikeGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult GetBike(int id) + public ActionResult GetBike(int id) { try { @@ -84,7 +84,7 @@ public ActionResult GetBike(int id) [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateBike([FromBody] BikeDto bikeDto) + public ActionResult CreateBike([FromBody] BikeCreateUpdateDto bikeDto) { try { @@ -133,11 +133,11 @@ public ActionResult CreateBike([FromBody] BikeDto bikeDto /// /// [HttpPut("{id:int}")] - [ProducesResponseType(typeof(BikeDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(BikeGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult UpdateBike(int id, [FromBody] BikeDto bikeDto) + public ActionResult UpdateBike(int id, [FromBody] BikeCreateUpdateDto bikeDto) { try { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs index 21d76d6be..a5b737b90 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -1,6 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; +using Bikes.Application.Interfaces; namespace Bikes.Api.Host.Controllers; @@ -18,9 +18,9 @@ public class RentersController(IRenterService service, ILogger [HttpGet] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetAllRenters() + public ActionResult> GetAllRenters() { try { @@ -44,10 +44,10 @@ public ActionResult> GetAllRenters() /// /// [HttpGet("{id:int}")] - [ProducesResponseType(typeof(RenterDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RenterGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult GetRenter(int id) + public ActionResult GetRenter(int id) { try { @@ -83,7 +83,7 @@ public ActionResult GetRenter(int id) [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateRenter([FromBody] RenterDto renterDto) + public ActionResult CreateRenter([FromBody] RenterCreateUpdateDto renterDto) { try { @@ -132,11 +132,11 @@ public ActionResult CreateRenter([FromBody] RenterDto ren /// /// [HttpPut("{id:int}")] - [ProducesResponseType(typeof(RenterDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RenterGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult UpdateRenter(int id, [FromBody] RenterDto renterDto) + public ActionResult UpdateRenter(int id, [FromBody] RenterCreateUpdateDto renterDto) { try { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs index 0f350e80c..2c0e8da3d 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -1,6 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; +using Bikes.Application.Interfaces; namespace Bikes.Api.Host.Controllers; @@ -18,9 +18,9 @@ public class RentsController(IRentService service, ILogger logg /// Returns all existing objects /// [HttpGet] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult> GetAllRents() + public ActionResult> GetAllRents() { try { @@ -44,10 +44,10 @@ public ActionResult> GetAllRents() /// /// [HttpGet("{id:int}")] - [ProducesResponseType(typeof(RentDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RentGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult GetRent(int id) + public ActionResult GetRent(int id) { try { @@ -83,7 +83,7 @@ public ActionResult GetRent(int id) [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateRent([FromBody] RentDto rentDto) + public ActionResult CreateRent([FromBody] RentCreateUpdateDto rentDto) { try { @@ -133,11 +133,11 @@ public ActionResult CreateRent([FromBody] RentDto rentDto /// /// [HttpPut("{id:int}")] - [ProducesResponseType(typeof(RentDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(RentGetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult UpdateRent(int id, [FromBody] RentDto rentDto) + public ActionResult UpdateRent(int id, [FromBody] RentCreateUpdateDto rentDto) { try { diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs index 00fcf2b8a..083be456b 100644 --- a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Services; +using Bikes.Application.Interfaces; +using Bikes.Application.Services; using Bikes.Domain.Repositories; using Bikes.Infrastructure.InMemory.Repositories; @@ -18,10 +19,10 @@ public static IServiceCollection AddBikeRentalServices(this IServiceCollection s { services.AddAutoMapper(typeof(BikeService).Assembly); - services.AddSingleton, InMemoryBikeRepository>(); - services.AddSingleton, InMemoryBikeModelRepository>(); - services.AddSingleton, InMemoryRenterRepository>(); - services.AddSingleton, InMemoryRentRepository>(); + services.AddSingleton, InMemoryBikeRepository>(); + services.AddSingleton, InMemoryBikeModelRepository>(); + services.AddSingleton, InMemoryRenterRepository>(); + services.AddSingleton, InMemoryRentRepository>(); services.AddScoped(); services.AddScoped(); diff --git a/Bikes/Bikes.Application/Services/IAnalyticsService.cs b/Bikes/Bikes.Application/Interfaces/IAnalyticsService.cs similarity index 79% rename from Bikes/Bikes.Application/Services/IAnalyticsService.cs rename to Bikes/Bikes.Application/Interfaces/IAnalyticsService.cs index e76267a41..9c6425406 100644 --- a/Bikes/Bikes.Application/Services/IAnalyticsService.cs +++ b/Bikes/Bikes.Application/Interfaces/IAnalyticsService.cs @@ -1,7 +1,7 @@ using Bikes.Contracts.Dto; using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Application.Interfaces; /// /// Interface for the Analytics service class @@ -11,17 +11,17 @@ public interface IAnalyticsService /// /// A method that returns information about all sports bikes /// - public List GetSportBikes(); + public List GetSportBikes(); /// /// A method that returns the top 5 bike models by rental duration /// - public List GetTopFiveModelsByRentDuration(); + public List GetTopFiveModelsByRentDuration(); /// /// A method that returns the top 5 bike models in terms of rental income /// - public List GetTopFiveModelsByProfit(); + public List GetTopFiveModelsByProfit(); /// /// A method that returns information about the minimum, maximum, and average bike rental time. @@ -36,5 +36,5 @@ public interface IAnalyticsService /// /// A method that returns information about the customers who have rented bicycles the most times. /// - public List GetTopThreeRenters(); + public List GetTopThreeRenters(); } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/IBikeModelService.cs b/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs similarity index 76% rename from Bikes/Bikes.Application/Services/IBikeModelService.cs rename to Bikes/Bikes.Application/Interfaces/IBikeModelService.cs index c94ffb8fa..f85455885 100644 --- a/Bikes/Bikes.Application/Services/IBikeModelService.cs +++ b/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs @@ -1,7 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Application.Interfaces; /// /// Interface for the BikeModel service class @@ -13,20 +12,20 @@ public interface IBikeModelService /// /// DTO object /// ID of the created object - public int CreateBikeModel(BikeModelDto bikeModelDto); + public int CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto); /// /// Returns all existing objects /// /// List of existing objects - public List GetAllBikeModels(); + public List GetAllBikeModels(); /// /// Returns object by id /// /// /// - public BikeModelDto? GetBikeModelById(int id); + public BikeModelGetDto? GetBikeModelById(int id); /// /// Updates an existing object @@ -34,7 +33,7 @@ public interface IBikeModelService /// Id /// DTO object /// Object if exist - public BikeModelDto? UpdateBikeModel(int id, BikeModelDto bikeModelDto); + public BikeModelGetDto? UpdateBikeModel(int id, BikeModelCreateUpdateDto bikeModelDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IBikeService.cs b/Bikes/Bikes.Application/Interfaces/IBikeService.cs similarity index 79% rename from Bikes/Bikes.Application/Services/IBikeService.cs rename to Bikes/Bikes.Application/Interfaces/IBikeService.cs index 395eed070..b5d429765 100644 --- a/Bikes/Bikes.Application/Services/IBikeService.cs +++ b/Bikes/Bikes.Application/Interfaces/IBikeService.cs @@ -1,7 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Application.Interfaces; /// /// Interface for the Bike service class @@ -13,20 +12,20 @@ public interface IBikeService /// /// DTO object /// ID of the created object - public int CreateBike(BikeDto bikeDto); + public int CreateBike(BikeCreateUpdateDto bikeDto); /// /// Returns all existing objects /// /// List of existing objects - public List GetAllBikes(); + public List GetAllBikes(); /// /// Returns object by id /// /// /// - public BikeDto? GetBikeById(int id); + public BikeGetDto? GetBikeById(int id); /// /// Updates an existing object @@ -34,7 +33,7 @@ public interface IBikeService /// Id /// DTO object /// Object if exist - public BikeDto? UpdateBike(int id, BikeDto bikeDto); + public BikeGetDto? UpdateBike(int id, BikeCreateUpdateDto bikeDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IRentService.cs b/Bikes/Bikes.Application/Interfaces/IRentService.cs similarity index 79% rename from Bikes/Bikes.Application/Services/IRentService.cs rename to Bikes/Bikes.Application/Interfaces/IRentService.cs index 402482838..2697bcd1c 100644 --- a/Bikes/Bikes.Application/Services/IRentService.cs +++ b/Bikes/Bikes.Application/Interfaces/IRentService.cs @@ -1,7 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Application.Interfaces; /// /// Interface for the Rent service class @@ -13,20 +12,20 @@ public interface IRentService /// /// DTO object /// ID of the created object - public int CreateRent(RentDto rentDto); + public int CreateRent(RentCreateUpdateDto rentDto); /// /// Returns all existing objects /// /// List of existing objects - public List GetAllRents(); + public List GetAllRents(); /// /// Returns object by id /// /// /// - public RentDto? GetRentById(int id); + public RentGetDto? GetRentById(int id); /// /// Updates an existing object @@ -34,7 +33,7 @@ public interface IRentService /// Id /// DTO object /// Object if exist - public RentDto? UpdateRent(int id, RentDto rentDto); + public RentGetDto? UpdateRent(int id, RentCreateUpdateDto rentDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Services/IRenterService.cs b/Bikes/Bikes.Application/Interfaces/IRenterService.cs similarity index 77% rename from Bikes/Bikes.Application/Services/IRenterService.cs rename to Bikes/Bikes.Application/Interfaces/IRenterService.cs index b20e3809f..4024b7453 100644 --- a/Bikes/Bikes.Application/Services/IRenterService.cs +++ b/Bikes/Bikes.Application/Interfaces/IRenterService.cs @@ -1,7 +1,6 @@ using Bikes.Contracts.Dto; -using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Application.Interfaces; /// /// Interface for the Renter service class @@ -13,20 +12,20 @@ public interface IRenterService /// /// DTO object /// ID of the created object - public int CreateRenter(RenterDto renterDto); + public int CreateRenter(RenterCreateUpdateDto renterDto); /// /// Returns all existing objects /// /// List of existing objects - public List GetAllRenters(); + public List GetAllRenters(); /// /// Returns object by id /// /// /// - public RenterDto? GetRenterById(int id); + public RenterGetDto? GetRenterById(int id); /// /// Updates an existing object @@ -34,7 +33,7 @@ public interface IRenterService /// Id /// DTO object /// Object if exist - public RenterDto? UpdateRenter(int id, RenterDto renterDto); + public RenterGetDto? UpdateRenter(int id, RenterCreateUpdateDto renterDto); /// /// Deletes an existing object by id diff --git a/Bikes/Bikes.Application/Mapping/MappingProfile.cs b/Bikes/Bikes.Application/Mapping/MappingProfile.cs index 32284ba0a..64dd50de5 100644 --- a/Bikes/Bikes.Application/Mapping/MappingProfile.cs +++ b/Bikes/Bikes.Application/Mapping/MappingProfile.cs @@ -8,9 +8,16 @@ public class MappingProfile : Profile { public MappingProfile() { - CreateMap().ReverseMap(); - CreateMap().ReverseMap(); - CreateMap().ReverseMap(); - CreateMap().ReverseMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs index c46d236a3..bedb9992a 100644 --- a/Bikes/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -1,4 +1,6 @@ -using Bikes.Contracts.Dto; +using AutoMapper; +using Bikes.Application.Interfaces; +using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -11,85 +13,59 @@ public class AnalyticsService( IRepository bikeRepository, IRepository bikeModelRepository, IRepository rentRepository, - IRepository renterRepository) : IAnalyticsService + IRepository renterRepository, + IMapper mapper) : IAnalyticsService { /// /// A method that returns information about all sports bikes /// - public List GetSportBikes() + public List GetSportBikes() { - return [.. bikeRepository.ReadAll() + var sportBikes = bikeRepository.ReadAll() .Where(bike => bike.Model.Type == BikeType.Sport) - .Select(bike => new BikeDto - { - SerialNumber = bike.SerialNumber, - Color = bike.Color, - ModelId = bike.Model.Id - })]; + .ToList(); + + return mapper.Map>(sportBikes); } /// /// A method that returns the top 5 bike models by rental duration /// - public List GetTopFiveModelsByRentDuration() + public List GetTopFiveModelsByRentDuration() { - var rents = rentRepository.ReadAll(); - var models = bikeModelRepository.ReadAll(); - - return [.. rents - .GroupBy(rent => rent.Bike.Model.Id) + var topModels = rentRepository.ReadAll() + .GroupBy(rent => rent.Bike.Model) .Select(group => new { - ModelId = group.Key, + Model = group.Key, TotalDuration = group.Sum(rent => rent.RentalDuration) }) .OrderByDescending(x => x.TotalDuration) .Take(5) - .Join(models, - x => x.ModelId, - model => model.Id, - (x, model) => new BikeModelDto - { - Type = model.Type, - WheelSize = model.WheelSize, - MaxPassengerWeight = model.MaxPassengerWeight, - Weight = model.Weight, - BrakeType = model.BrakeType, - Year = model.Year, - RentPrice = model.RentPrice - })]; + .Select(x => x.Model) + .ToList(); + + return mapper.Map>(topModels); } /// /// A method that returns the top 5 bike models in terms of rental income /// - public List GetTopFiveModelsByProfit() + public List GetTopFiveModelsByProfit() { - var rents = rentRepository.ReadAll(); - var models = bikeModelRepository.ReadAll(); - - return [.. rents - .GroupBy(rent => rent.Bike.Model.Id) + var topModels = rentRepository.ReadAll() + .GroupBy(rent => rent.Bike.Model) .Select(group => new { - ModelId = group.Key, + Model = group.Key, TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) }) .OrderByDescending(x => x.TotalProfit) .Take(5) - .Join(models, - x => x.ModelId, - model => model.Id, - (x, model) => new BikeModelDto - { - Type = model.Type, - WheelSize = model.WheelSize, - MaxPassengerWeight = model.MaxPassengerWeight, - Weight = model.Weight, - BrakeType = model.BrakeType, - Year = model.Year, - RentPrice = model.RentPrice - })]; + .Select(x => x.Model) + .ToList(); + + return mapper.Map>(topModels); } /// @@ -97,8 +73,9 @@ public List GetTopFiveModelsByProfit() /// public RentalDurationStatsDto GetRentalDurationStats() { - List durations = [.. rentRepository.ReadAll() - .Select(rent => rent.RentalDuration)]; + var durations = rentRepository.ReadAll() + .Select(rent => rent.RentalDuration) + .ToList(); return new RentalDurationStatsDto { @@ -124,11 +101,11 @@ public Dictionary GetTotalRentalTimeByType() /// /// A method that returns information about the customers who have rented bicycles the most times. /// - public List GetTopThreeRenters() + public List GetTopThreeRenters() { var renters = renterRepository.ReadAll(); - return [.. rentRepository.ReadAll() + var topRenters = rentRepository.ReadAll() .GroupBy(rent => rent.Renter.Id) .Select(group => new { @@ -140,10 +117,9 @@ public List GetTopThreeRenters() .Join(renters, x => x.RenterId, renter => renter.Id, - (x, renter) => new RenterDto - { - FullName = renter.FullName, - Number = renter.Number - })]; + (x, renter) => renter) + .ToList(); + + return mapper.Map>(topRenters); } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/BikeModelService.cs b/Bikes/Bikes.Application/Services/BikeModelService.cs index 95267b35f..61b22ca86 100644 --- a/Bikes/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes/Bikes.Application/Services/BikeModelService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bikes.Application.Interfaces; using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -17,7 +18,7 @@ public class BikeModelService( /// /// DTO object /// ID of the created object - public int CreateBikeModel(BikeModelDto bikeModelDto) + public int CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto) { var bikeModel = mapper.Map(bikeModelDto); @@ -28,15 +29,10 @@ public int CreateBikeModel(BikeModelDto bikeModelDto) /// Returns all existing objects /// /// List of existing objects - public List GetAllBikeModels() + public List GetAllBikeModels() { var models = bikeModelRepository.ReadAll(); - - return models.Select(model => - { - var dto = mapper.Map(model); - return dto; - }).ToList(); + return mapper.Map>(models); } /// @@ -44,12 +40,12 @@ public List GetAllBikeModels() /// /// /// - public BikeModelDto? GetBikeModelById(int id) + public BikeModelGetDto? GetBikeModelById(int id) { var model = bikeModelRepository.Read(id); if (model == null) return null; - return mapper.Map(model); + return mapper.Map(model); } /// @@ -58,7 +54,7 @@ public List GetAllBikeModels() /// Id /// DTO object /// Object if exist - public BikeModelDto? UpdateBikeModel(int id, BikeModelDto bikeModelDto) + public BikeModelGetDto? UpdateBikeModel(int id, BikeModelCreateUpdateDto bikeModelDto) { var existingModel = bikeModelRepository.Read(id); if (existingModel == null) return null; @@ -70,7 +66,7 @@ public List GetAllBikeModels() var updatedModel = bikeModelRepository.Update(id, existingModel); if (updatedModel == null) return null; - return mapper.Map(updatedModel); + return mapper.Map(updatedModel); } /// diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs index dc2c1c954..0be3b36c9 100644 --- a/Bikes/Bikes.Application/Services/BikeService.cs +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bikes.Application.Interfaces; using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -18,7 +19,7 @@ public class BikeService( /// /// DTO object /// ID of the created object - public int CreateBike(BikeDto bikeDto) + public int CreateBike(BikeCreateUpdateDto bikeDto) { var model = bikeModelRepository.Read(bikeDto.ModelId) ?? throw new ArgumentException($"BikeModel with id {bikeDto.ModelId} not found"); @@ -33,10 +34,10 @@ public int CreateBike(BikeDto bikeDto) /// Returns all existing objects /// /// List of existing objects - public List GetAllBikes() + public List GetAllBikes() { var bikes = bikeRepository.ReadAll(); - return mapper.Map>(bikes); + return mapper.Map>(bikes); } /// @@ -44,10 +45,10 @@ public List GetAllBikes() /// /// /// - public BikeDto? GetBikeById(int id) + public BikeGetDto? GetBikeById(int id) { var bike = bikeRepository.Read(id); - return bike != null ? mapper.Map(bike) : null; + return bike != null ? mapper.Map(bike) : null; } /// @@ -56,7 +57,7 @@ public List GetAllBikes() /// Id /// DTO object /// Object if exist - public BikeDto? UpdateBike(int id, BikeDto bikeDto) + public BikeGetDto? UpdateBike(int id, BikeCreateUpdateDto bikeDto) { var existingBike = bikeRepository.Read(id); if (existingBike == null) return null; @@ -69,7 +70,7 @@ public List GetAllBikes() existingBike.Model = model; var updatedBike = bikeRepository.Update(id, existingBike); - return updatedBike != null ? mapper.Map(updatedBike) : null; + return updatedBike != null ? mapper.Map(updatedBike) : null; } /// diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs index 5867371f0..ccc9d38c0 100644 --- a/Bikes/Bikes.Application/Services/RentService.cs +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bikes.Application.Interfaces; using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -20,7 +21,7 @@ public class RentService( /// /// /// - public int CreateRent(RentDto rentDto) + public int CreateRent(RentCreateUpdateDto rentDto) { var bike = bikeRepository.Read(rentDto.BikeId) ?? throw new ArgumentException($"Bike with id {rentDto.BikeId} not found"); @@ -39,10 +40,10 @@ public int CreateRent(RentDto rentDto) /// Returns all existing objects /// /// - public List GetAllRents() + public List GetAllRents() { var rents = rentRepository.ReadAll(); - return mapper.Map>(rents); + return mapper.Map>(rents); } /// @@ -50,10 +51,10 @@ public List GetAllRents() /// /// /// - public RentDto? GetRentById(int id) + public RentGetDto? GetRentById(int id) { var rent = rentRepository.Read(id); - return rent != null ? mapper.Map(rent) : null; + return rent != null ? mapper.Map(rent) : null; } /// @@ -62,7 +63,7 @@ public List GetAllRents() /// /// /// - public RentDto? UpdateRent(int id, RentDto rentDto) + public RentGetDto? UpdateRent(int id, RentCreateUpdateDto rentDto) { var existingRent = rentRepository.Read(id); if (existingRent == null) return null; @@ -79,7 +80,7 @@ public List GetAllRents() existingRent.Renter = renter; var updatedRent = rentRepository.Update(id, existingRent); - return updatedRent != null ? mapper.Map(updatedRent) : null; + return updatedRent != null ? mapper.Map(updatedRent) : null; } /// diff --git a/Bikes/Bikes.Application/Services/RenterService.cs b/Bikes/Bikes.Application/Services/RenterService.cs index e936e5322..36efb6728 100644 --- a/Bikes/Bikes.Application/Services/RenterService.cs +++ b/Bikes/Bikes.Application/Services/RenterService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bikes.Application.Interfaces; using Bikes.Contracts.Dto; using Bikes.Domain.Models; using Bikes.Domain.Repositories; @@ -17,7 +18,7 @@ public class RenterService( /// /// /// - public int CreateRenter(RenterDto renterDto) + public int CreateRenter(RenterCreateUpdateDto renterDto) { var renter = mapper.Map(renterDto); return renterRepository.Create(renter); @@ -27,10 +28,10 @@ public int CreateRenter(RenterDto renterDto) /// Returns all existing objects /// /// - public List GetAllRenters() + public List GetAllRenters() { var renters = renterRepository.ReadAll(); - return mapper.Map>(renters); + return mapper.Map>(renters); } /// @@ -38,10 +39,10 @@ public List GetAllRenters() /// /// /// - public RenterDto? GetRenterById(int id) + public RenterGetDto? GetRenterById(int id) { var renter = renterRepository.Read(id); - return renter != null ? mapper.Map(renter) : null; + return renter != null ? mapper.Map(renter) : null; } /// @@ -50,7 +51,7 @@ public List GetAllRenters() /// /// /// - public RenterDto? UpdateRenter(int id, RenterDto renterDto) + public RenterGetDto? UpdateRenter(int id, RenterCreateUpdateDto renterDto) { var existingRenter = renterRepository.Read(id); if (existingRenter == null) return null; @@ -58,7 +59,7 @@ public List GetAllRenters() mapper.Map(renterDto, existingRenter); var updatedRenter = renterRepository.Update(id, existingRenter); - return updatedRenter != null ? mapper.Map(updatedRenter) : null; + return updatedRenter != null ? mapper.Map(updatedRenter) : null; } /// diff --git a/Bikes/Bikes.Contracts/Dto/BikeDto.cs b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs similarity index 83% rename from Bikes/Bikes.Contracts/Dto/BikeDto.cs rename to Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs index b8bae5098..d6747d908 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs @@ -1,9 +1,9 @@ namespace Bikes.Contracts.Dto; /// -/// DTO class for the Bike class +/// DTO create/update class for the Bike class /// -public class BikeDto +public class BikeCreateUpdateDto { /// /// Bike's serial number diff --git a/Bikes/Bikes.Contracts/Dto/BikeGetDto.cs b/Bikes/Bikes.Contracts/Dto/BikeGetDto.cs new file mode 100644 index 000000000..eb8ed55dd --- /dev/null +++ b/Bikes/Bikes.Contracts/Dto/BikeGetDto.cs @@ -0,0 +1,27 @@ +namespace Bikes.Contracts.Dto; + +/// +/// DTO get class for the Bike class +/// +public class BikeGetDto +{ + /// + /// 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; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/BikeModelDto.cs b/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs similarity index 91% rename from Bikes/Bikes.Contracts/Dto/BikeModelDto.cs rename to Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs index 3d8b338ae..58c2c1a1c 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeModelDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs @@ -3,9 +3,9 @@ namespace Bikes.Contracts.Dto; /// -/// DTO class for the BikeModel class +/// DTO create/update class for the BikeModel class /// -public class BikeModelDto +public class BikeModelCreateUpdateDto { /// /// Model's type diff --git a/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs b/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs new file mode 100644 index 000000000..32445987e --- /dev/null +++ b/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs @@ -0,0 +1,49 @@ +using Bikes.Domain.Models; + +namespace Bikes.Contracts.Dto; + +/// +/// DTO get class for the BikeModel class +/// +public class BikeModelGetDto +{ + /// + /// Model's unique id + /// + public required int Id { get; set; } + + /// + /// Model's type + /// + public required BikeType Type { get; set; } + + /// + /// Model's size of wheel + /// + public required int WheelSize { get; set; } + + /// + /// Maximum allowable passenger weight + /// + public required int MaxPassengerWeight { get; set; } + + /// + /// Model's weight + /// + public required int Weight { get; set; } + + /// + /// Model's type of brake + /// + public required string BrakeType { get; set; } + + /// + /// Model's production year + /// + public required string Year { get; set; } + + /// + /// The price of an hour of rent + /// + public required int RentPrice { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/RentDto.cs b/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs similarity index 86% rename from Bikes/Bikes.Contracts/Dto/RentDto.cs rename to Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs index a9b129f71..be13adb6d 100644 --- a/Bikes/Bikes.Contracts/Dto/RentDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs @@ -1,9 +1,9 @@ namespace Bikes.Contracts.Dto; /// -/// DTO class for the Rent class +/// DTO create/update class for the Rent class /// -public class RentDto +public class RentCreateUpdateDto { /// /// Rental start time diff --git a/Bikes/Bikes.Contracts/Dto/RentGetDto.cs b/Bikes/Bikes.Contracts/Dto/RentGetDto.cs new file mode 100644 index 000000000..adb880a7a --- /dev/null +++ b/Bikes/Bikes.Contracts/Dto/RentGetDto.cs @@ -0,0 +1,34 @@ +using Bikes.Domain.Models; + +namespace Bikes.Contracts.Dto; + +/// +/// DTO get class for the Rent class +/// +public class RentGetDto +{ + /// + /// Rent's unique id + /// + public required int Id { get; set; } + + /// + /// Rental start time + /// + public required DateTime RentalStartTime { get; set; } + + /// + /// Rental duration + /// + public required int RentalDuration { get; set; } + + /// + /// Renter + /// + public required int RenterId { get; set; } + + /// + /// Bike + /// + public required int BikeId { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/RenterDto.cs b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs similarity index 77% rename from Bikes/Bikes.Contracts/Dto/RenterDto.cs rename to Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs index 2f02cc235..6d110cad9 100644 --- a/Bikes/Bikes.Contracts/Dto/RenterDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs @@ -1,9 +1,9 @@ namespace Bikes.Contracts.Dto; /// -/// DTO class for the Renter class +/// DTO create/update class for the Renter class /// -public class RenterDto +public class RenterCreateUpdateDto { /// /// Renter's full name diff --git a/Bikes/Bikes.Contracts/Dto/RenterGetDto.cs b/Bikes/Bikes.Contracts/Dto/RenterGetDto.cs new file mode 100644 index 000000000..62a63d15c --- /dev/null +++ b/Bikes/Bikes.Contracts/Dto/RenterGetDto.cs @@ -0,0 +1,22 @@ +namespace Bikes.Contracts.Dto; + +/// +/// DTO get class for the Renter class +/// +public class RenterGetDto +{ + /// + /// Renter's unique id + /// + public required int Id { get; set; } + + /// + /// 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/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs index 367615b06..7f00d4dd4 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeModelRepository.cs @@ -40,7 +40,7 @@ public int Create(BikeModel entity) /// List of existing objects public List ReadAll() { - return _items; + return [.. _items]; } /// diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs index a1e38a52c..92cda4790 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs @@ -40,7 +40,7 @@ public int Create(Bike entity) /// List of existing objects public List ReadAll() { - return _items; + return [.. _items]; } /// diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs index 7d85a817c..e4c101613 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRentRepository.cs @@ -40,7 +40,7 @@ public int Create(Rent entity) /// List of existing objects public List ReadAll() { - return _items; + return [.. _items]; } /// diff --git a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs index 6fd9ff419..dc9310a00 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Repositories/InMemoryRenterRepository.cs @@ -40,7 +40,7 @@ public int Create(Renter entity) /// List of existing objects public List ReadAll() { - return _items; + return [.. _items]; } /// diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 7c1df05e6..56dbf82ef 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Services; +using Bikes.Application.Interfaces; +using Bikes.Application.Services; using Bikes.Domain.Repositories; using Bikes.Infrastructure.InMemory.Repositories; From 2c0d95b394c159d600709785b8684801634084ac Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 10 Dec 2025 07:20:11 +0400 Subject: [PATCH 25/41] Fix tests --- Bikes/Bikes.Tests/Bikes.Tests.csproj | 1 + Bikes/Bikes.Tests/BikesFixture.cs | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Bikes/Bikes.Tests/Bikes.Tests.csproj b/Bikes/Bikes.Tests/Bikes.Tests.csproj index 73a40b6ae..27dc94319 100644 --- a/Bikes/Bikes.Tests/Bikes.Tests.csproj +++ b/Bikes/Bikes.Tests/Bikes.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 56dbf82ef..74b145223 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -1,5 +1,7 @@ -using Bikes.Application.Interfaces; +using AutoMapper; +using Bikes.Application.Interfaces; using Bikes.Application.Services; +using Bikes.Application.Mapping; using Bikes.Domain.Repositories; using Bikes.Infrastructure.InMemory.Repositories; @@ -11,17 +13,25 @@ namespace Bikes.Tests; public class BikesFixture { public readonly IAnalyticsService AnalyticsService; + private readonly IMapper _mapper; /// /// A constructor that creates repositories and service classes /// public BikesFixture() { - IRepository bikeRepo = new InMemoryBikeRepository(); - IRepository modelRepo = new InMemoryBikeModelRepository(); - IRepository rentRepo = new InMemoryRentRepository(); - IRepository renterRepo = new InMemoryRenterRepository(); + IRepository bikeRepo = new InMemoryBikeRepository(); + IRepository modelRepo = new InMemoryBikeModelRepository(); + IRepository rentRepo = new InMemoryRentRepository(); + IRepository renterRepo = new InMemoryRenterRepository(); - AnalyticsService = new AnalyticsService(bikeRepo, modelRepo, rentRepo, renterRepo); + var configuration = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + }); + + _mapper = configuration.CreateMapper(); + + AnalyticsService = new AnalyticsService(bikeRepo, modelRepo, rentRepo, renterRepo, _mapper); } } From 1fc2bec7e587c6ffef34e1a8bae6e87afadc330a Mon Sep 17 00:00:00 2001 From: comandir26 Date: Thu, 11 Dec 2025 08:23:26 +0400 Subject: [PATCH 26/41] Validation of DTOs and fix creating objects --- .../Controllers/BikeModelsController.cs | 17 +++++++++++++--- .../Controllers/BikesController.cs | 17 +++++++++++++--- .../Controllers/RentersController.cs | 17 +++++++++++++--- .../Controllers/RentsController.cs | 17 +++++++++++++--- .../Services/AnalyticsService.cs | 1 - .../Dto/BikeCreateUpdateDto.cs | 12 ++++++++--- .../Dto/BikeModelCreateUpdateDto.cs | 15 +++++++++++++- Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs | 2 +- .../Dto/RentCreateUpdateDto.cs | 12 +++++++++-- Bikes/Bikes.Contracts/Dto/RentGetDto.cs | 4 +--- .../Dto/RenterCreateUpdateDto.cs | 6 +++++- Bikes/Bikes.Domain/Models/BikeModel.cs | 2 +- .../Seeders/InMemorySeeder.cs | 20 +++++++++---------- Bikes/Bikes.Tests/BikesFixture.cs | 2 +- 14 files changed, 108 insertions(+), 36 deletions(-) diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs index 167954d72..89ba52cc3 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -80,10 +80,10 @@ public ActionResult GetBikeModel(int id) /// /// [HttpPost] - [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(BikeModelGetDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateBikeModel([FromBody] BikeModelCreateUpdateDto bikeModelDto) + public ActionResult CreateBikeModel([FromBody] BikeModelCreateUpdateDto bikeModelDto) { try { @@ -103,10 +103,21 @@ public ActionResult CreateBikeModel([FromBody] BikeModelC var id = service.CreateBikeModel(bikeModelDto); logger.LogInformation("Created bike model with ID {ModelId}", id); + var createdModel = service.GetBikeModelById(id); + + if (createdModel == null) + { + logger.LogError("Failed to retrieve created bike model with ID {ModelId}", id); + return Problem( + title: "Internal Server Error", + detail: "Bike model was created but cannot be retrieved.", + statusCode: StatusCodes.Status500InternalServerError); + } + return CreatedAtAction( nameof(GetBikeModel), new { id }, - new { id, message = "Bike model created successfully." }); + createdModel); } catch (Exception ex) { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs index 5df4bbbf2..7f82c5482 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -81,10 +81,10 @@ public ActionResult GetBike(int id) /// /// [HttpPost] - [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(BikeGetDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateBike([FromBody] BikeCreateUpdateDto bikeDto) + public ActionResult CreateBike([FromBody] BikeCreateUpdateDto bikeDto) { try { @@ -104,10 +104,21 @@ public ActionResult CreateBike([FromBody] BikeCreateUpdat var id = service.CreateBike(bikeDto); logger.LogInformation("Created bike with ID {BikeId}", id); + var createdBike = service.GetBikeById(id); + + if (createdBike == null) + { + logger.LogError("Failed to retrieve created bike with ID {BikeId}", id); + return Problem( + title: "Internal Server Error", + detail: "Bike was created but cannot be retrieved.", + statusCode: StatusCodes.Status500InternalServerError); + } + return CreatedAtAction( nameof(GetBike), new { id }, - new { id, message = "Bike created successfully." }); + createdBike); } catch (ArgumentException ex) { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs index a5b737b90..f8ff342dd 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -80,10 +80,10 @@ public ActionResult GetRenter(int id) /// /// [HttpPost] - [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(RenterGetDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateRenter([FromBody] RenterCreateUpdateDto renterDto) + public ActionResult CreateRenter([FromBody] RenterCreateUpdateDto renterDto) { try { @@ -103,10 +103,21 @@ public ActionResult CreateRenter([FromBody] RenterCreateU var id = service.CreateRenter(renterDto); logger.LogInformation("Created renter with ID {RenterId}", id); + var createdRenter = service.GetRenterById(id); + + if (createdRenter == null) + { + logger.LogError("Failed to retrieve created renter with ID {RenterId}", id); + return Problem( + title: "Internal Server Error", + detail: "Renter was created but cannot be retrieved.", + statusCode: StatusCodes.Status500InternalServerError); + } + return CreatedAtAction( nameof(GetRenter), new { id }, - new { id, message = "Renter created successfully." }); + createdRenter); } catch (ArgumentException ex) { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs index 2c0e8da3d..943d1f113 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -80,10 +80,10 @@ public ActionResult GetRent(int id) /// /// [HttpPost] - [ProducesResponseType(typeof(CreatedAtActionResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(RentGetDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] - public ActionResult CreateRent([FromBody] RentCreateUpdateDto rentDto) + public ActionResult CreateRent([FromBody] RentCreateUpdateDto rentDto) { try { @@ -104,10 +104,21 @@ public ActionResult CreateRent([FromBody] RentCreateUpdat var id = service.CreateRent(rentDto); logger.LogInformation("Created rent with ID {RentId}", id); + var createdRent = service.GetRentById(id); + + if (createdRent == null) + { + logger.LogError("Failed to retrieve created rent with ID {RentId}", id); + return Problem( + title: "Internal Server Error", + detail: "Rent was created but cannot be retrieved.", + statusCode: StatusCodes.Status500InternalServerError); + } + return CreatedAtAction( nameof(GetRent), new { id }, - new { id, message = "Rent created successfully." }); + createdRent); } catch (ArgumentException ex) { diff --git a/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs index bedb9992a..4c2900dc5 100644 --- a/Bikes/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -11,7 +11,6 @@ namespace Bikes.Application.Services; /// public class AnalyticsService( IRepository bikeRepository, - IRepository bikeModelRepository, IRepository rentRepository, IRepository renterRepository, IMapper mapper) : IAnalyticsService diff --git a/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs index d6747d908..944137d1d 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Contracts.Dto; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Contracts.Dto; /// /// DTO create/update class for the Bike class @@ -8,15 +10,19 @@ public class BikeCreateUpdateDto /// /// Bike's serial number /// + [Required(ErrorMessage = "Serial number is required")] + [StringLength(50, MinimumLength = 5, ErrorMessage = "Serial number must be between 5 and 50 characters")] public required string SerialNumber { get; set; } /// /// Bike's color /// + [Required(ErrorMessage = "Color is required")] public required string Color { get; set; } /// - /// Bike's model + /// Bike's model ID /// + [Required(ErrorMessage = "Model ID is required")] public required int ModelId { get; set; } -} +} \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs index 58c2c1a1c..3c9a300e7 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeModelCreateUpdateDto.cs @@ -1,4 +1,5 @@ using Bikes.Domain.Models; +using System.ComponentModel.DataAnnotations; namespace Bikes.Contracts.Dto; @@ -10,35 +11,47 @@ public class BikeModelCreateUpdateDto /// /// Model's type /// + [Required(ErrorMessage = "Type is required")] public required BikeType Type { get; set; } /// /// Model's size of wheel /// + [Required(ErrorMessage = "Wheel size is required")] + [Range(12, 36, ErrorMessage = "Wheel size must be between 12 and 36 inches")] public required int WheelSize { get; set; } /// /// Maximum allowable passenger weight /// + [Required(ErrorMessage = "Maximum passenger weight is required")] + [Range(25, 120, ErrorMessage = "Maximum passenger weight must be between 25 and 120 kg")] public required int MaxPassengerWeight { get; set; } /// /// Model's weight /// + [Required(ErrorMessage = "Weight is required")] + [Range(5, 30, ErrorMessage = "Weight must be between 5 and 30 kg")] public required int Weight { get; set; } /// /// Model's type of brake /// + [Required(ErrorMessage = "Brake type is required")] public required string BrakeType { get; set; } /// /// Model's production year /// - public required string Year { get; set; } + [Required(ErrorMessage = "Year is required")] + [Range(2010, 2025, ErrorMessage = "Year must be between 2010 and current year")] + public required int Year { get; set; } /// /// The price of an hour of rent /// + [Required(ErrorMessage = "Rent price is required")] + [Range(1, 1000, ErrorMessage = "Rent price must be between 1 and 1000")] public required int RentPrice { get; set; } } \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs b/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs index 32445987e..00c4c2544 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeModelGetDto.cs @@ -40,7 +40,7 @@ public class BikeModelGetDto /// /// Model's production year /// - public required string Year { get; set; } + public required int Year { get; set; } /// /// The price of an hour of rent diff --git a/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs index be13adb6d..32cc4cf13 100644 --- a/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RentCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Contracts.Dto; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Contracts.Dto; /// /// DTO create/update class for the Rent class @@ -8,20 +10,26 @@ public class RentCreateUpdateDto /// /// Rental start time /// + [Required(ErrorMessage = "Rental start time is required")] + [DataType(DataType.DateTime, ErrorMessage = "Invalid date time format")] public required DateTime RentalStartTime { get; set; } /// - /// Rental duration + /// Rental duration (in hours) /// + [Required(ErrorMessage = "Rental duration is required")] + [Range(1, 24, ErrorMessage = "Rental duration must be between 1 and 24 hours")] public required int RentalDuration { get; set; } /// /// Renter's id /// + [Required(ErrorMessage = "Renter ID is required")] public required int RenterId { get; set; } /// /// Bike's id /// + [Required(ErrorMessage = "Bike ID is required")] public required int BikeId { get; set; } } \ No newline at end of file diff --git a/Bikes/Bikes.Contracts/Dto/RentGetDto.cs b/Bikes/Bikes.Contracts/Dto/RentGetDto.cs index adb880a7a..31d2d25ee 100644 --- a/Bikes/Bikes.Contracts/Dto/RentGetDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RentGetDto.cs @@ -1,6 +1,4 @@ -using Bikes.Domain.Models; - -namespace Bikes.Contracts.Dto; +namespace Bikes.Contracts.Dto; /// /// DTO get class for the Rent class diff --git a/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs index 6d110cad9..487d2757e 100644 --- a/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Contracts.Dto; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Contracts.Dto; /// /// DTO create/update class for the Renter class @@ -8,10 +10,12 @@ public class RenterCreateUpdateDto /// /// Renter's full name /// + [Required(ErrorMessage = "Full name is required")] public required string FullName { get; set; } /// /// Renter's phone number /// + [Required(ErrorMessage = "Phone number is required")] public required string Number { get; set; } } \ No newline at end of file diff --git a/Bikes/Bikes.Domain/Models/BikeModel.cs b/Bikes/Bikes.Domain/Models/BikeModel.cs index fd56af753..9d991ccd2 100644 --- a/Bikes/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes/Bikes.Domain/Models/BikeModel.cs @@ -38,7 +38,7 @@ public class BikeModel /// /// Model's production year /// - public required string Year { get; set; } + public required int Year { get; set; } /// /// The price of an hour of rent diff --git a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs index 0c59b7917..dd3b9366f 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs @@ -14,16 +14,16 @@ public static List GetBikeModels() { return [ - new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 700 }, - new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = "2024", RentPrice = 850 }, - new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 500 }, - new() { Id = 4, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 125, Weight = 15, BrakeType = "Дисковые гидравлические", Year = "2023", RentPrice = 750 }, - new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = "2024", RentPrice = 900 }, - new() { Id = 6, Type = BikeType.City, WheelSize = 27, MaxPassengerWeight = 135, Weight = 17, BrakeType = "Дисковые механические", Year = "2023", RentPrice = 550 }, - new() { Id = 7, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 13, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 800 }, - new() { Id = 8, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 10, BrakeType = "Ободные v-brake", Year = "2023", RentPrice = 950 }, - new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = "2022", RentPrice = 600 }, - new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = "2024", RentPrice = 650 } + new() { Id = 1, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 14, BrakeType = "Дисковые гидравлические", Year = 2023, RentPrice = 700 }, + new() { Id = 2, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 11, BrakeType = "Ободные v-brake", Year = 2024, RentPrice = 850 }, + new() { Id = 3, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 130, Weight = 16, BrakeType = "Дисковые механические", Year = 2022, RentPrice = 500 }, + new() { Id = 4, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 125, Weight = 15, BrakeType = "Дисковые гидравлические", Year = 2023, RentPrice = 750 }, + new() { Id = 5, Type = BikeType.Sport, WheelSize = 26, MaxPassengerWeight = 115, Weight = 12, BrakeType = "Ободные карбоновые", Year = 2024, RentPrice = 900 }, + new() { Id = 6, Type = BikeType.City, WheelSize = 27, MaxPassengerWeight = 135, Weight = 17, BrakeType = "Дисковые механические", Year = 2023, RentPrice = 550 }, + new() { Id = 7, Type = BikeType.Mountain, WheelSize = 29, MaxPassengerWeight = 120, Weight = 13, BrakeType = "Дисковые гидравлические", Year = 2024, RentPrice = 800 }, + new() { Id = 8, Type = BikeType.Sport, WheelSize = 27, MaxPassengerWeight = 110, Weight = 10, BrakeType = "Ободные v-brake", Year = 2023, RentPrice = 950 }, + new() { Id = 9, Type = BikeType.City, WheelSize = 26, MaxPassengerWeight = 140, Weight = 18, BrakeType = "Дисковые механические", Year = 2022, RentPrice = 600 }, + new() { Id = 10, Type = BikeType.Mountain, WheelSize = 26, MaxPassengerWeight = 130, Weight = 14, BrakeType = "Дисковые гидравлические", Year = 2024, RentPrice = 650 } ]; } diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index 74b145223..bda3dba9f 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -32,6 +32,6 @@ public BikesFixture() _mapper = configuration.CreateMapper(); - AnalyticsService = new AnalyticsService(bikeRepo, modelRepo, rentRepo, renterRepo, _mapper); + AnalyticsService = new AnalyticsService(bikeRepo, rentRepo, renterRepo, _mapper); } } From ecb72457856a52fcb949f30ea9038e4d46258f24 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Thu, 11 Dec 2025 08:33:16 +0400 Subject: [PATCH 27/41] some validation checks --- Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs | 2 ++ Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs index 944137d1d..bb02c1cf1 100644 --- a/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/BikeCreateUpdateDto.cs @@ -12,12 +12,14 @@ public class BikeCreateUpdateDto /// [Required(ErrorMessage = "Serial number is required")] [StringLength(50, MinimumLength = 5, ErrorMessage = "Serial number must be between 5 and 50 characters")] + [RegularExpression(@"^[A-Z0-9]+$", ErrorMessage = "Serial number can only contain uppercase letters and numbers")] public required string SerialNumber { get; set; } /// /// Bike's color /// [Required(ErrorMessage = "Color is required")] + [RegularExpression(@"^[a-zA-Zа-яА-ЯёЁ]+$", ErrorMessage = "Color can only contain letters")] public required string Color { get; set; } /// diff --git a/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs index 487d2757e..e1f76ffff 100644 --- a/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs +++ b/Bikes/Bikes.Contracts/Dto/RenterCreateUpdateDto.cs @@ -11,11 +11,15 @@ public class RenterCreateUpdateDto /// Renter's full name /// [Required(ErrorMessage = "Full name is required")] + [StringLength(100, MinimumLength = 5, ErrorMessage = "Full name must be between 5 and 100 characters")] + [RegularExpression(@"^[a-zA-Zа-яА-ЯёЁ\s\-]+$", ErrorMessage = "Full name can only contain letters, spaces and hyphens")] public required string FullName { get; set; } /// /// Renter's phone number /// [Required(ErrorMessage = "Phone number is required")] + [RegularExpression(@"^\+7\s\(\d{3}\)\s\d{3}-\d{2}-\d{2}$", + ErrorMessage = "Phone number must be in format: +7 (XXX) XXX-XX-XX")] public required string Number { get; set; } } \ No newline at end of file From 52c0e276ae0d13afc83156e51ac71fa8b7022f6e Mon Sep 17 00:00:00 2001 From: comandir26 Date: Thu, 11 Dec 2025 09:12:59 +0400 Subject: [PATCH 28/41] Fix create methods --- .../Controllers/BikeModelsController.cs | 15 +++++++-------- .../Bikes.Api.Host/Controllers/BikesController.cs | 13 ++++++------- .../Controllers/RentersController.cs | 15 +++++++-------- .../Bikes.Api.Host/Controllers/RentsController.cs | 13 ++++++------- .../Interfaces/IBikeModelService.cs | 4 ++-- .../Bikes.Application/Interfaces/IBikeService.cs | 2 +- .../Bikes.Application/Interfaces/IRentService.cs | 2 +- .../Interfaces/IRenterService.cs | 2 +- .../Services/BikeModelService.cs | 7 +++++-- Bikes/Bikes.Application/Services/BikeService.cs | 8 ++++++-- Bikes/Bikes.Application/Services/RentService.cs | 11 +++++++---- Bikes/Bikes.Application/Services/RenterService.cs | 8 ++++++-- 12 files changed, 55 insertions(+), 45 deletions(-) diff --git a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs index 89ba52cc3..cc38b20dc 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -100,24 +100,23 @@ public ActionResult CreateBikeModel([FromBody] BikeModelCreateU modelStateDictionary: ModelState); } - var id = service.CreateBikeModel(bikeModelDto); - logger.LogInformation("Created bike model with ID {ModelId}", id); - - var createdModel = service.GetBikeModelById(id); + var createdModel = service.CreateBikeModel(bikeModelDto); if (createdModel == null) { - logger.LogError("Failed to retrieve created bike model with ID {ModelId}", id); + logger.LogError("Failed to create bike model"); return Problem( title: "Internal Server Error", - detail: "Bike model was created but cannot be retrieved.", + detail: "Failed to create bike model.", statusCode: StatusCodes.Status500InternalServerError); } + logger.LogInformation("Created bike model with ID {ModelId}", createdModel.Id); + return CreatedAtAction( nameof(GetBikeModel), - new { id }, - createdModel); + new { id = createdModel.Id }, + createdModel); } catch (Exception ex) { diff --git a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs index 7f82c5482..72ef6038a 100644 --- a/Bikes/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/BikesController.cs @@ -101,23 +101,22 @@ public ActionResult CreateBike([FromBody] BikeCreateUpdateDto bikeDt modelStateDictionary: ModelState); } - var id = service.CreateBike(bikeDto); - logger.LogInformation("Created bike with ID {BikeId}", id); - - var createdBike = service.GetBikeById(id); + var createdBike = service.CreateBike(bikeDto); if (createdBike == null) { - logger.LogError("Failed to retrieve created bike with ID {BikeId}", id); + logger.LogError("Failed to create bike"); return Problem( title: "Internal Server Error", - detail: "Bike was created but cannot be retrieved.", + detail: "Failed to create bike.", statusCode: StatusCodes.Status500InternalServerError); } + logger.LogInformation("Created bike with ID {BikeId}", createdBike.Id); + return CreatedAtAction( nameof(GetBike), - new { id }, + new { id = createdBike.Id }, createdBike); } catch (ArgumentException ex) diff --git a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs index f8ff342dd..224f9d6b5 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentersController.cs @@ -100,24 +100,23 @@ public ActionResult CreateRenter([FromBody] RenterCreateUpdateDto modelStateDictionary: ModelState); } - var id = service.CreateRenter(renterDto); - logger.LogInformation("Created renter with ID {RenterId}", id); - - var createdRenter = service.GetRenterById(id); + var createdRenter = service.CreateRenter(renterDto); if (createdRenter == null) { - logger.LogError("Failed to retrieve created renter with ID {RenterId}", id); + logger.LogError("Failed to create renter"); return Problem( title: "Internal Server Error", - detail: "Renter was created but cannot be retrieved.", + detail: "Failed to create renter.", statusCode: StatusCodes.Status500InternalServerError); } + logger.LogInformation("Created renter with ID {RenterId}", createdRenter.Id); + return CreatedAtAction( nameof(GetRenter), - new { id }, - createdRenter); + new { id = createdRenter.Id }, + createdRenter); } catch (ArgumentException ex) { diff --git a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs index 943d1f113..555e038b8 100644 --- a/Bikes/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes/Bikes.Api.Host/Controllers/RentsController.cs @@ -101,23 +101,22 @@ public ActionResult CreateRent([FromBody] RentCreateUpdateDto rentDt modelStateDictionary: ModelState); } - var id = service.CreateRent(rentDto); - logger.LogInformation("Created rent with ID {RentId}", id); - - var createdRent = service.GetRentById(id); + var createdRent = service.CreateRent(rentDto); if (createdRent == null) { - logger.LogError("Failed to retrieve created rent with ID {RentId}", id); + logger.LogError("Failed to create rent"); return Problem( title: "Internal Server Error", - detail: "Rent was created but cannot be retrieved.", + detail: "Failed to create rent.", statusCode: StatusCodes.Status500InternalServerError); } + logger.LogInformation("Created rent with ID {RentId}", createdRent.Id); + return CreatedAtAction( nameof(GetRent), - new { id }, + new { id = createdRent.Id }, createdRent); } catch (ArgumentException ex) diff --git a/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs b/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs index f85455885..f1ffb2423 100644 --- a/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs +++ b/Bikes/Bikes.Application/Interfaces/IBikeModelService.cs @@ -11,8 +11,8 @@ public interface IBikeModelService /// Creates a new object /// /// DTO object - /// ID of the created object - public int CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto); + /// Created object + public BikeModelGetDto CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto); /// /// Returns all existing objects diff --git a/Bikes/Bikes.Application/Interfaces/IBikeService.cs b/Bikes/Bikes.Application/Interfaces/IBikeService.cs index b5d429765..7f697e9e9 100644 --- a/Bikes/Bikes.Application/Interfaces/IBikeService.cs +++ b/Bikes/Bikes.Application/Interfaces/IBikeService.cs @@ -12,7 +12,7 @@ public interface IBikeService /// /// DTO object /// ID of the created object - public int CreateBike(BikeCreateUpdateDto bikeDto); + public BikeGetDto CreateBike(BikeCreateUpdateDto bikeDto); /// /// Returns all existing objects diff --git a/Bikes/Bikes.Application/Interfaces/IRentService.cs b/Bikes/Bikes.Application/Interfaces/IRentService.cs index 2697bcd1c..1785f458f 100644 --- a/Bikes/Bikes.Application/Interfaces/IRentService.cs +++ b/Bikes/Bikes.Application/Interfaces/IRentService.cs @@ -12,7 +12,7 @@ public interface IRentService /// /// DTO object /// ID of the created object - public int CreateRent(RentCreateUpdateDto rentDto); + public RentGetDto CreateRent(RentCreateUpdateDto rentDto); /// /// Returns all existing objects diff --git a/Bikes/Bikes.Application/Interfaces/IRenterService.cs b/Bikes/Bikes.Application/Interfaces/IRenterService.cs index 4024b7453..0b04b475b 100644 --- a/Bikes/Bikes.Application/Interfaces/IRenterService.cs +++ b/Bikes/Bikes.Application/Interfaces/IRenterService.cs @@ -12,7 +12,7 @@ public interface IRenterService /// /// DTO object /// ID of the created object - public int CreateRenter(RenterCreateUpdateDto renterDto); + public RenterGetDto CreateRenter(RenterCreateUpdateDto renterDto); /// /// Returns all existing objects diff --git a/Bikes/Bikes.Application/Services/BikeModelService.cs b/Bikes/Bikes.Application/Services/BikeModelService.cs index 61b22ca86..b005e4bf4 100644 --- a/Bikes/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes/Bikes.Application/Services/BikeModelService.cs @@ -18,11 +18,14 @@ public class BikeModelService( /// /// DTO object /// ID of the created object - public int CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto) + public BikeModelGetDto CreateBikeModel(BikeModelCreateUpdateDto bikeModelDto) { var bikeModel = mapper.Map(bikeModelDto); - return bikeModelRepository.Create(bikeModel); + var id = bikeModelRepository.Create(bikeModel); + var createdModel = bikeModelRepository.Read(id); + + return mapper.Map(createdModel); } /// diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs index 0be3b36c9..2cb0b666f 100644 --- a/Bikes/Bikes.Application/Services/BikeService.cs +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -19,7 +19,7 @@ public class BikeService( /// /// DTO object /// ID of the created object - public int CreateBike(BikeCreateUpdateDto bikeDto) + public BikeGetDto CreateBike(BikeCreateUpdateDto bikeDto) { var model = bikeModelRepository.Read(bikeDto.ModelId) ?? throw new ArgumentException($"BikeModel with id {bikeDto.ModelId} not found"); @@ -27,7 +27,11 @@ public int CreateBike(BikeCreateUpdateDto bikeDto) var bike = mapper.Map(bikeDto); bike.Model = model; - return bikeRepository.Create(bike); + var id = bikeRepository.Create(bike); + + var createdBike = bikeRepository.Read(id); + + return mapper.Map(createdBike); } /// diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs index ccc9d38c0..41f3571a3 100644 --- a/Bikes/Bikes.Application/Services/RentService.cs +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -18,10 +18,10 @@ public class RentService( /// /// Creates a new object /// - /// - /// + /// DTO object + /// Created object DTO /// - public int CreateRent(RentCreateUpdateDto rentDto) + public RentGetDto CreateRent(RentCreateUpdateDto rentDto) { var bike = bikeRepository.Read(rentDto.BikeId) ?? throw new ArgumentException($"Bike with id {rentDto.BikeId} not found"); @@ -33,7 +33,10 @@ public int CreateRent(RentCreateUpdateDto rentDto) rent.Bike = bike; rent.Renter = renter; - return rentRepository.Create(rent); + var id = rentRepository.Create(rent); + var createdRent = rentRepository.Read(id); + + return mapper.Map(createdRent); } /// diff --git a/Bikes/Bikes.Application/Services/RenterService.cs b/Bikes/Bikes.Application/Services/RenterService.cs index 36efb6728..557eaf076 100644 --- a/Bikes/Bikes.Application/Services/RenterService.cs +++ b/Bikes/Bikes.Application/Services/RenterService.cs @@ -18,10 +18,14 @@ public class RenterService( /// /// /// - public int CreateRenter(RenterCreateUpdateDto renterDto) + public RenterGetDto CreateRenter(RenterCreateUpdateDto renterDto) { var renter = mapper.Map(renterDto); - return renterRepository.Create(renter); + + var id = renterRepository.Create(renter); + var createdRenter = renterRepository.Read(id); + + return mapper.Map(createdRenter); } /// From 442349fb2a8f10542f198871c7c73702831fec8d Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 14 Dec 2025 09:18:04 +0400 Subject: [PATCH 29/41] Add new projects. Add DbContext and repositories. --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 3 +- Bikes/Bikes.AppHost/AppHost.cs | 3 + Bikes/Bikes.AppHost/Bikes.AppHost.csproj | 23 +++++ .../Properties/launchSettings.json | 29 ++++++ .../appsettings.Development.json | 8 ++ Bikes/Bikes.AppHost/appsettings.json | 9 ++ .../Bikes.Infrastructure.MongoDb.csproj | 20 ++++ .../Configuration/MongoDbSettings.cs | 19 ++++ .../MongoDbContext.cs | 25 +++++ .../Repositories/MongoBikeModelRepository.cs | 56 +++++++++++ .../Repositories/MongoBikeRepository.cs | 62 ++++++++++++ .../Repositories/MongoRentRepository.cs | 97 +++++++++++++++++++ .../Repositories/MongoRenterRepository.cs | 56 +++++++++++ .../Bikes.ServiceDefaults.csproj | 20 ++++ Bikes/Bikes.ServiceDefaults/Class1.cs | 6 ++ Bikes/Bikes.sln | 18 ++++ 16 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 Bikes/Bikes.AppHost/AppHost.cs create mode 100644 Bikes/Bikes.AppHost/Bikes.AppHost.csproj create mode 100644 Bikes/Bikes.AppHost/Properties/launchSettings.json create mode 100644 Bikes/Bikes.AppHost/appsettings.Development.json create mode 100644 Bikes/Bikes.AppHost/appsettings.json create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs create mode 100644 Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj create mode 100644 Bikes/Bikes.ServiceDefaults/Class1.cs diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index 577010821..c1106c38d 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -13,7 +13,8 @@ - + + diff --git a/Bikes/Bikes.AppHost/AppHost.cs b/Bikes/Bikes.AppHost/AppHost.cs new file mode 100644 index 000000000..c62c3a0f6 --- /dev/null +++ b/Bikes/Bikes.AppHost/AppHost.cs @@ -0,0 +1,3 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.Build().Run(); diff --git a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj new file mode 100644 index 000000000..ffd637b9a --- /dev/null +++ b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net8.0 + enable + enable + c5698091-b4a9-4bd7-a5f8-050a0249db80 + + + + + + + + + + + + + diff --git a/Bikes/Bikes.AppHost/Properties/launchSettings.json b/Bikes/Bikes.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..de5f8ef23 --- /dev/null +++ b/Bikes/Bikes.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17205;http://localhost:15251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21105", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15251", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19246", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20112" + } + } + } +} diff --git a/Bikes/Bikes.AppHost/appsettings.Development.json b/Bikes/Bikes.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Bikes/Bikes.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Bikes/Bikes.AppHost/appsettings.json b/Bikes/Bikes.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/Bikes/Bikes.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj new file mode 100644 index 000000000..ddf05d553 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs b/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs new file mode 100644 index 000000000..3fe2a395c --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs @@ -0,0 +1,19 @@ +namespace Bikes.Infrastructure.MongoDb.Configuration; + +/// +/// MongoDB Connection Settings +/// +public class MongoDbSettings +{ + public const string SectionName = "MongoDb"; + + /// + /// Connection string + /// + public required string ConnectionString { get; set; } + + /// + /// Database Name + /// + public required string DatabaseName { get; set; } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs new file mode 100644 index 000000000..c1def92f9 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Infrastructure.MongoDb.Configuration; + +namespace Bikes.Infrastructure.MongoDb; + +/// +/// Context of connecting to MongoDB +/// +public class MongoDbContext +{ + private readonly IMongoDatabase _database; + + public MongoDbContext(IOptions settings) + { + var mongoClient = new MongoClient(settings.Value.ConnectionString); + _database = mongoClient.GetDatabase(settings.Value.DatabaseName); + } + + public IMongoCollection Bikes => _database.GetCollection("bikes"); + public IMongoCollection BikeModels => _database.GetCollection("bike_models"); + public IMongoCollection Renters => _database.GetCollection("renters"); + public IMongoCollection Rents => _database.GetCollection("rents"); +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs new file mode 100644 index 000000000..b1535e730 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs @@ -0,0 +1,56 @@ +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.MongoDb.Repositories; + +/// +/// Repository for working with bike models in MongoDB +/// +public class MongoBikeModelRepository : IRepository +{ + private readonly IMongoCollection _collection; + + public MongoBikeModelRepository(MongoDbContext context) + { + _collection = context.BikeModels; + + var indexKeysDefinition = Builders.IndexKeys.Ascending(m => m.Id); + _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + } + + public int Create(BikeModel entity) + { + var maxId = _collection.Find(_ => true) + .SortByDescending(m => m.Id) + .Limit(1) + .FirstOrDefault()?.Id ?? 0; + + entity.Id = maxId + 1; + _collection.InsertOne(entity); + return entity.Id; + } + + public List ReadAll() + { + return _collection.Find(_ => true).ToList(); + } + + public BikeModel? Read(int id) + { + return _collection.Find(m => m.Id == id).FirstOrDefault(); + } + + public BikeModel? Update(int id, BikeModel entity) + { + entity.Id = id; + var result = _collection.ReplaceOne(m => m.Id == id, entity); + return result.ModifiedCount > 0 ? entity : null; + } + + public bool Delete(int id) + { + var result = _collection.DeleteOne(m => m.Id == id); + return result.DeletedCount > 0; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs new file mode 100644 index 000000000..defc9e7d2 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs @@ -0,0 +1,62 @@ +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.MongoDb.Repositories; + +/// +/// Repository for working with bikes in MongoDB +/// +public class MongoBikeRepository : IRepository +{ + private readonly IMongoCollection _collection; + + public MongoBikeRepository(MongoDbContext context) + { + _collection = context.Bikes; + + + var indexKeysDefinition = Builders.IndexKeys.Ascending(b => b.Id); + _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + + var serialNumberIndexKeys = Builders.IndexKeys.Ascending(b => b.SerialNumber); + var serialNumberIndexOptions = new CreateIndexOptions { Unique = true }; + _collection.Indexes.CreateOne( + new CreateIndexModel(serialNumberIndexKeys, serialNumberIndexOptions)); + } + + public int Create(Bike entity) + { + var maxId = _collection.Find(_ => true) + .SortByDescending(b => b.Id) + .Limit(1) + .FirstOrDefault()?.Id ?? 0; + + entity.Id = maxId + 1; + _collection.InsertOne(entity); + return entity.Id; + } + + public List ReadAll() + { + return _collection.Find(_ => true).ToList(); + } + + public Bike? Read(int id) + { + return _collection.Find(b => b.Id == id).FirstOrDefault(); + } + + public Bike? Update(int id, Bike entity) + { + entity.Id = id; + var result = _collection.ReplaceOne(b => b.Id == id, entity); + return result.ModifiedCount > 0 ? entity : null; + } + + public bool Delete(int id) + { + var result = _collection.DeleteOne(b => b.Id == id); + return result.DeletedCount > 0; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs new file mode 100644 index 000000000..d2b8cd867 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs @@ -0,0 +1,97 @@ +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.MongoDb.Repositories; + +/// +/// Repository for working with rents in MongoDB +/// +public class MongoRentRepository : IRepository +{ + private readonly IMongoCollection _collection; + private readonly MongoDbContext _context; + + public MongoRentRepository(MongoDbContext context) + { + _context = context; + _collection = context.Rents; + + var indexKeysDefinition = Builders.IndexKeys.Ascending(r => r.Id); + _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + } + + public int Create(Rent entity) + { + if (entity.Bike != null && entity.Bike.Id > 0) + { + entity.Bike = _context.Bikes.Find(b => b.Id == entity.Bike.Id).FirstOrDefault(); + } + + if (entity.Renter != null && entity.Renter.Id > 0) + { + entity.Renter = _context.Renters.Find(r => r.Id == entity.Renter.Id).FirstOrDefault(); + } + + var maxId = _collection.Find(_ => true) + .SortByDescending(r => r.Id) + .Limit(1) + .FirstOrDefault()?.Id ?? 0; + + entity.Id = maxId + 1; + _collection.InsertOne(entity); + return entity.Id; + } + + public List ReadAll() + { + + var rents = _collection.Find(_ => true).ToList(); + + foreach (var rent in rents) + { + LoadRelatedData(rent); + } + + return rents; + } + + public Rent? Read(int id) + { + var rent = _collection.Find(r => r.Id == id).FirstOrDefault(); + if (rent != null) + { + LoadRelatedData(rent); + } + return rent; + } + + public Rent? Update(int id, Rent entity) + { + entity.Id = id; + + LoadRelatedData(entity); + + var result = _collection.ReplaceOne(r => r.Id == id, entity); + return result.ModifiedCount > 0 ? entity : null; + } + + public bool Delete(int id) + { + var result = _collection.DeleteOne(r => r.Id == id); + return result.DeletedCount > 0; + } + + private void LoadRelatedData(Rent rent) + { + if (rent.Bike != null && rent.Bike.Id > 0) + { + rent.Bike = _context.Bikes.Find(b => b.Id == rent.Bike.Id).FirstOrDefault(); + } + + if (rent.Renter != null && rent.Renter.Id > 0) + { + rent.Renter = _context.Renters.Find(r => r.Id == rent.Renter.Id).FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs new file mode 100644 index 000000000..b5f08eb26 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs @@ -0,0 +1,56 @@ +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.MongoDb.Repositories; + +/// +/// Repository for working with renters in MongoDB +/// +public class MongoRenterRepository : IRepository +{ + private readonly IMongoCollection _collection; + + public MongoRenterRepository(MongoDbContext context) + { + _collection = context.Renters; + + var indexKeysDefinition = Builders.IndexKeys.Ascending(r => r.Id); + _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + } + + public int Create(Renter entity) + { + var maxId = _collection.Find(_ => true) + .SortByDescending(r => r.Id) + .Limit(1) + .FirstOrDefault()?.Id ?? 0; + + entity.Id = maxId + 1; + _collection.InsertOne(entity); + return entity.Id; + } + + public List ReadAll() + { + return _collection.Find(_ => true).ToList(); + } + + public Renter? Read(int id) + { + return _collection.Find(r => r.Id == id).FirstOrDefault(); + } + + public Renter? Update(int id, Renter entity) + { + entity.Id = id; + var result = _collection.ReplaceOne(r => r.Id == id, entity); + return result.ModifiedCount > 0 ? entity : null; + } + + public bool Delete(int id) + { + var result = _collection.DeleteOne(r => r.Id == id); + return result.DeletedCount > 0; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj b/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj new file mode 100644 index 000000000..c3a3a8811 --- /dev/null +++ b/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/Bikes/Bikes.ServiceDefaults/Class1.cs b/Bikes/Bikes.ServiceDefaults/Class1.cs new file mode 100644 index 000000000..dbab7fe89 --- /dev/null +++ b/Bikes/Bikes.ServiceDefaults/Class1.cs @@ -0,0 +1,6 @@ +namespace Bikes.ServiceDefaults; + +public class Class1 +{ + +} diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index dd70544cd..858657d23 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -18,6 +18,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Contracts", "Bikes.Contracts\Bikes.Contracts.csproj", "{2BE08B58-C908-406E-8B12-5EA2B44C39DB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.MongoDb", "Bikes.Infrastructure.MongoDb\Bikes.Infrastructure.MongoDb.csproj", "{4F00B2E3-DB82-4F90-9086-D9AA517058DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.ServiceDefaults", "Bikes.ServiceDefaults\Bikes.ServiceDefaults.csproj", "{51AF03C6-E1D9-4256-83CA-AE064A256692}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.AppHost", "Bikes.AppHost\Bikes.AppHost.csproj", "{C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,6 +54,18 @@ Global {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BE08B58-C908-406E-8B12-5EA2B44C39DB}.Release|Any CPU.Build.0 = Release|Any CPU + {4F00B2E3-DB82-4F90-9086-D9AA517058DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F00B2E3-DB82-4F90-9086-D9AA517058DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F00B2E3-DB82-4F90-9086-D9AA517058DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F00B2E3-DB82-4F90-9086-D9AA517058DF}.Release|Any CPU.Build.0 = Release|Any CPU + {51AF03C6-E1D9-4256-83CA-AE064A256692}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51AF03C6-E1D9-4256-83CA-AE064A256692}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51AF03C6-E1D9-4256-83CA-AE064A256692}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51AF03C6-E1D9-4256-83CA-AE064A256692}.Release|Any CPU.Build.0 = Release|Any CPU + {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e9d1a44760296dd91c3f07b56ada2f4e7eb1ca22 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 14 Dec 2025 14:40:38 +0400 Subject: [PATCH 30/41] Success build Infrastructure.MongoDb, update program.cs and extensions --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 2 +- Bikes/Bikes.Api.Host/Program.cs | 28 +++++++-- Bikes/Bikes.Api.Host/appsettings.json | 4 ++ .../Extensions/ServiceCollectionExtensions.cs | 10 +--- .../Bikes.Infrastructure.MongoDb.csproj | 1 + .../Extensions/ServiceCollectionExtensions.cs | 33 +++++++++++ .../MongoDbSeeder.cs | 57 +++++++++++++++++++ .../Repositories/MongoBikeRepository.cs | 6 -- 8 files changed, 122 insertions(+), 19 deletions(-) rename Bikes/{Bikes.Api.Host => Bikes.Application}/Extensions/ServiceCollectionExtensions.cs (62%) create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index c1106c38d..c7c261c7b 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/Bikes/Bikes.Api.Host/Program.cs b/Bikes/Bikes.Api.Host/Program.cs index 4c3d44bbd..9adba048f 100644 --- a/Bikes/Bikes.Api.Host/Program.cs +++ b/Bikes/Bikes.Api.Host/Program.cs @@ -1,16 +1,36 @@ -using Bikes.Api.Host.Extensions; +using Bikes.Application.Extensions; +using Bikes.Infrastructure.MongoDb; +using Bikes.Infrastructure.MongoDb.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); - -builder.Services.AddBikeRentalServices(); - builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddMongoDbInfrastructure(builder.Configuration); + +builder.Services.AddBikeRentalServices(); + var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + try + { + var seeder = scope.ServiceProvider.GetRequiredService(); + await seeder.SeedAsync(); + + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogInformation("Database seeded successfully!"); + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "An error occurred while seeding the database"); + } +} + if (app.Environment.IsDevelopment()) { app.UseSwagger(); diff --git a/Bikes/Bikes.Api.Host/appsettings.json b/Bikes/Bikes.Api.Host/appsettings.json index 10f68b8c8..d49646f29 100644 --- a/Bikes/Bikes.Api.Host/appsettings.json +++ b/Bikes/Bikes.Api.Host/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "MongoDb": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "BikeRentalDb" + }, "AllowedHosts": "*" } diff --git a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Application/Extensions/ServiceCollectionExtensions.cs similarity index 62% rename from Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs rename to Bikes/Bikes.Application/Extensions/ServiceCollectionExtensions.cs index 083be456b..b3e75a36c 100644 --- a/Bikes/Bikes.Api.Host/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Application/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,8 @@ using Bikes.Application.Interfaces; using Bikes.Application.Services; -using Bikes.Domain.Repositories; -using Bikes.Infrastructure.InMemory.Repositories; +using Microsoft.Extensions.DependencyInjection; -namespace Bikes.Api.Host.Extensions; +namespace Bikes.Application.Extensions; /// /// A class for hidden registration of services @@ -19,11 +18,6 @@ public static IServiceCollection AddBikeRentalServices(this IServiceCollection s { services.AddAutoMapper(typeof(BikeService).Assembly); - services.AddSingleton, InMemoryBikeRepository>(); - services.AddSingleton, InMemoryBikeModelRepository>(); - services.AddSingleton, InMemoryRenterRepository>(); - services.AddSingleton, InMemoryRentRepository>(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj index ddf05d553..a673a9ff7 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj +++ b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj @@ -15,6 +15,7 @@ + diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..5ae7afc53 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.MongoDb.Configuration; +using Bikes.Infrastructure.MongoDb.Repositories; + +namespace Bikes.Infrastructure.MongoDb.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMongoDbInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + var mongoDbSection = configuration.GetSection(MongoDbSettings.SectionName); + var mongoDbSettings = new MongoDbSettings + { + ConnectionString = mongoDbSection["ConnectionString"]!, + DatabaseName = mongoDbSection["DatabaseName"]! + }; + + services.AddSingleton(mongoDbSettings); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton, MongoBikeRepository>(); + services.AddSingleton, MongoBikeModelRepository>(); + services.AddSingleton, MongoRenterRepository>(); + services.AddSingleton, MongoRentRepository>(); + + return services; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs new file mode 100644 index 000000000..5d0fba780 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs @@ -0,0 +1,57 @@ +using MongoDB.Driver; +using Bikes.Domain.Models; +using Bikes.Infrastructure.InMemory.Seeders; + +namespace Bikes.Infrastructure.MongoDb; + +/// +/// MongoDb seeder +/// +public class MongoDbSeeder +{ + private readonly MongoDbContext _context; + + public MongoDbSeeder(MongoDbContext context) + { + _context = context; + } + + public async Task SeedAsync() + { + var hasModels = await _context.BikeModels.Find(_ => true).AnyAsync(); + if (hasModels) return; + + Console.WriteLine("Seeding MongoDB with initial data..."); + + var models = InMemorySeeder.GetBikeModels(); + var bikes = InMemorySeeder.GetBikes(); + var renters = InMemorySeeder.GetRenters(); + var rents = InMemorySeeder.GetRents(); + + if (models.Any()) + { + await _context.BikeModels.InsertManyAsync(models); + Console.WriteLine($"Inserted {models.Count} bike models"); + } + + if (bikes.Any()) + { + await _context.Bikes.InsertManyAsync(bikes); + Console.WriteLine($"Inserted {bikes.Count} bikes"); + } + + if (renters.Any()) + { + await _context.Renters.InsertManyAsync(renters); + Console.WriteLine($"Inserted {renters.Count} renters"); + } + + if (rents.Any()) + { + await _context.Rents.InsertManyAsync(rents); + Console.WriteLine($"Inserted {rents.Count} rents"); + } + + Console.WriteLine("Seeding completed!"); + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs index defc9e7d2..b2f27e46a 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs @@ -15,14 +15,8 @@ public MongoBikeRepository(MongoDbContext context) { _collection = context.Bikes; - var indexKeysDefinition = Builders.IndexKeys.Ascending(b => b.Id); _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); - - var serialNumberIndexKeys = Builders.IndexKeys.Ascending(b => b.SerialNumber); - var serialNumberIndexOptions = new CreateIndexOptions { Unique = true }; - _collection.Indexes.CreateOne( - new CreateIndexModel(serialNumberIndexKeys, serialNumberIndexOptions)); } public int Create(Bike entity) From 7ba2bc15e8a778a29a84b70d36c36ae396c467be Mon Sep 17 00:00:00 2001 From: comandir26 Date: Mon, 15 Dec 2025 12:51:45 +0400 Subject: [PATCH 31/41] everything is working, but need to use Entity Framework Core( --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 1 + Bikes/Bikes.Api.Host/Program.cs | 6 +- .../appsettings.Development.json | 8 +- Bikes/Bikes.AppHost/AppHost.cs | 3 - Bikes/Bikes.AppHost/Bikes.AppHost.csproj | 17 ++-- Bikes/Bikes.AppHost/Program.cs | 9 ++ .../Properties/launchSettings.json | 10 +-- .../Bikes.Infrastructure.MongoDb.csproj | 2 +- .../Configuration/MongoDbSettings.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 25 ++++-- .../MongoDbContext.cs | 41 ++++++++- .../MongoDbSeeder.cs | 38 +++++--- .../Repositories/MongoBikeModelRepository.cs | 25 ++++++ .../Repositories/MongoBikeRepository.cs | 25 ++++++ .../Repositories/MongoRentRepository.cs | 29 ++++++ .../Repositories/MongoRenterRepository.cs | 25 ++++++ .../Bikes.ServiceDefaults.csproj | 36 ++++---- Bikes/Bikes.ServiceDefaults/Class1.cs | 6 -- Bikes/Bikes.ServiceDefaults/Extensions.cs | 88 +++++++++++++++++++ 19 files changed, 335 insertions(+), 63 deletions(-) delete mode 100644 Bikes/Bikes.AppHost/AppHost.cs create mode 100644 Bikes/Bikes.AppHost/Program.cs delete mode 100644 Bikes/Bikes.ServiceDefaults/Class1.cs create mode 100644 Bikes/Bikes.ServiceDefaults/Extensions.cs diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index c7c261c7b..9abe8e8ad 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -7,6 +7,7 @@ + diff --git a/Bikes/Bikes.Api.Host/Program.cs b/Bikes/Bikes.Api.Host/Program.cs index 9adba048f..8ade9cc11 100644 --- a/Bikes/Bikes.Api.Host/Program.cs +++ b/Bikes/Bikes.Api.Host/Program.cs @@ -1,19 +1,23 @@ using Bikes.Application.Extensions; using Bikes.Infrastructure.MongoDb; using Bikes.Infrastructure.MongoDb.Extensions; +using Bikes.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddMongoDbInfrastructure(builder.Configuration); - builder.Services.AddBikeRentalServices(); var app = builder.Build(); +app.MapDefaultEndpoints(); + using (var scope = app.Services.CreateScope()) { try diff --git a/Bikes/Bikes.Api.Host/appsettings.Development.json b/Bikes/Bikes.Api.Host/appsettings.Development.json index 0c208ae91..6b4dbc6ac 100644 --- a/Bikes/Bikes.Api.Host/appsettings.Development.json +++ b/Bikes/Bikes.Api.Host/appsettings.Development.json @@ -4,5 +4,11 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "MongoDb": { + "DatabaseName": "BikesDB" + }, + "ConnectionStrings": { + "MongoDB": "" } -} +} \ No newline at end of file diff --git a/Bikes/Bikes.AppHost/AppHost.cs b/Bikes/Bikes.AppHost/AppHost.cs deleted file mode 100644 index c62c3a0f6..000000000 --- a/Bikes/Bikes.AppHost/AppHost.cs +++ /dev/null @@ -1,3 +0,0 @@ -var builder = DistributedApplication.CreateBuilder(args); - -builder.Build().Run(); diff --git a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj index ffd637b9a..ece33e39a 100644 --- a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj +++ b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj @@ -1,6 +1,6 @@ - + - + Exe @@ -11,13 +11,14 @@ - - + + - - - - + + + diff --git a/Bikes/Bikes.AppHost/Program.cs b/Bikes/Bikes.AppHost/Program.cs new file mode 100644 index 000000000..50be2abeb --- /dev/null +++ b/Bikes/Bikes.AppHost/Program.cs @@ -0,0 +1,9 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var mongodb = builder.AddMongoDB("mongodb") + .WithDataVolume(); + +var api = builder.AddProject("bikes-api") + .WithReference(mongodb); + +builder.Build().Run(); \ No newline at end of file diff --git a/Bikes/Bikes.AppHost/Properties/launchSettings.json b/Bikes/Bikes.AppHost/Properties/launchSettings.json index de5f8ef23..9244fd45f 100644 --- a/Bikes/Bikes.AppHost/Properties/launchSettings.json +++ b/Bikes/Bikes.AppHost/Properties/launchSettings.json @@ -9,8 +9,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21105", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21105", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" } }, "http": { @@ -21,9 +21,9 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19246", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20112" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19246", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20112" } } } -} +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj index a673a9ff7..1334912f0 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj +++ b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj @@ -10,7 +10,7 @@ - + diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs b/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs index 3fe2a395c..8b18e89f7 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Configuration/MongoDbSettings.cs @@ -10,10 +10,10 @@ public class MongoDbSettings /// /// Connection string /// - public required string ConnectionString { get; set; } + public string ConnectionString { get; set; } = string.Empty; /// /// Database Name /// - public required string DatabaseName { get; set; } + public string DatabaseName { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs index 5ae7afc53..1d028b0b8 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs @@ -6,21 +6,32 @@ namespace Bikes.Infrastructure.MongoDb.Extensions; +/// +/// A class for hidden registration of services +/// public static class ServiceCollectionExtensions { + /// + /// The method that registers services + /// + /// + /// + /// public static IServiceCollection AddMongoDbInfrastructure( this IServiceCollection services, IConfiguration configuration) { - var mongoDbSection = configuration.GetSection(MongoDbSettings.SectionName); - var mongoDbSettings = new MongoDbSettings + var connectionString = configuration.GetConnectionString("MongoDB"); + var databaseName = configuration["MongoDb:DatabaseName"] ?? "BikesDB"; + + services.Configure(options => { - ConnectionString = mongoDbSection["ConnectionString"]!, - DatabaseName = mongoDbSection["DatabaseName"]! - }; + options.ConnectionString = connectionString + ?? "mongodb://localhost:27017"; + options.DatabaseName = databaseName; + }); - services.AddSingleton(mongoDbSettings); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton, MongoBikeRepository>(); diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs index c1def92f9..15a13e05e 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs @@ -6,20 +6,55 @@ namespace Bikes.Infrastructure.MongoDb; /// -/// Context of connecting to MongoDB +/// The context of connecting to MongoDB. /// public class MongoDbContext { + /// + /// Link to the MongoDB database + /// private readonly IMongoDatabase _database; + /// + /// A constructor that accepts settings via the IOptions pattern. + /// + /// MongoDB connection settings transmitted from the AppHost + /// + /// public MongoDbContext(IOptions settings) { - var mongoClient = new MongoClient(settings.Value.ConnectionString); - _database = mongoClient.GetDatabase(settings.Value.DatabaseName); + if (settings?.Value == null) + throw new ArgumentNullException(nameof(settings), "MongoDbSettings is not configured"); + + var mongoDbSettings = settings.Value; + + if (string.IsNullOrEmpty(mongoDbSettings.ConnectionString)) + throw new ArgumentException("MongoDB connection string is not configured"); + + if (string.IsNullOrEmpty(mongoDbSettings.DatabaseName)) + mongoDbSettings.DatabaseName = "BikesDB"; + + var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); + _database = mongoClient.GetDatabase(mongoDbSettings.DatabaseName); } + /// + /// Bike collection. Corresponds to the "bikes" collection in MongoDB. + /// public IMongoCollection Bikes => _database.GetCollection("bikes"); + + /// + /// A collection of bike models. Corresponds to the collection "bike_models" in MongoDB. + /// public IMongoCollection BikeModels => _database.GetCollection("bike_models"); + + /// + /// Collection of renters. Corresponds to the "renters" collection in MongoDB + /// public IMongoCollection Renters => _database.GetCollection("renters"); + + /// + /// A collection of rental records. Corresponds to the "rents" collection in MongoDB. + /// public IMongoCollection Rents => _database.GetCollection("rents"); } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs index 5d0fba780..8efae53ff 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs @@ -1,27 +1,45 @@ using MongoDB.Driver; -using Bikes.Domain.Models; using Bikes.Infrastructure.InMemory.Seeders; +using Microsoft.Extensions.Logging; namespace Bikes.Infrastructure.MongoDb; /// -/// MongoDb seeder +/// A class for initializing initial data in MongoDB /// public class MongoDbSeeder { + private readonly MongoDbContext _context; - public MongoDbSeeder(MongoDbContext context) + private readonly ILogger _logger; + + /// + /// Constructor of the MongoDbSeeder class + /// + /// MongoDB context for working with the database + /// Logger for recording diagnostic information + public MongoDbSeeder(MongoDbContext context, ILogger logger) { _context = context; + _logger = logger; } + /// + /// The main method for filling the database with initial data + /// + /// Asynchronous task public async Task SeedAsync() { var hasModels = await _context.BikeModels.Find(_ => true).AnyAsync(); - if (hasModels) return; - Console.WriteLine("Seeding MongoDB with initial data..."); + if (hasModels) + { + _logger.LogInformation("Database already contains data. Skipping seeding."); + return; + } + + _logger.LogInformation("Starting MongoDB database seeding..."); var models = InMemorySeeder.GetBikeModels(); var bikes = InMemorySeeder.GetBikes(); @@ -31,27 +49,27 @@ public async Task SeedAsync() if (models.Any()) { await _context.BikeModels.InsertManyAsync(models); - Console.WriteLine($"Inserted {models.Count} bike models"); + _logger.LogInformation("Inserted {Count} bike models into database", models.Count); } if (bikes.Any()) { await _context.Bikes.InsertManyAsync(bikes); - Console.WriteLine($"Inserted {bikes.Count} bikes"); + _logger.LogInformation("Inserted {Count} bikes into database", bikes.Count); } if (renters.Any()) { await _context.Renters.InsertManyAsync(renters); - Console.WriteLine($"Inserted {renters.Count} renters"); + _logger.LogInformation("Inserted {Count} renters into database", renters.Count); } if (rents.Any()) { await _context.Rents.InsertManyAsync(rents); - Console.WriteLine($"Inserted {rents.Count} rents"); + _logger.LogInformation("Inserted {Count} rents into database", rents.Count); } - Console.WriteLine("Seeding completed!"); + _logger.LogInformation("MongoDB database seeding completed successfully"); } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs index b1535e730..691fb03f8 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs @@ -19,6 +19,11 @@ public MongoBikeModelRepository(MongoDbContext context) _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(BikeModel entity) { var maxId = _collection.Find(_ => true) @@ -31,16 +36,31 @@ public int Create(BikeModel entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _collection.Find(_ => true).ToList(); } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public BikeModel? Read(int id) { return _collection.Find(m => m.Id == id).FirstOrDefault(); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public BikeModel? Update(int id, BikeModel entity) { entity.Id = id; @@ -48,6 +68,11 @@ public List ReadAll() return result.ModifiedCount > 0 ? entity : null; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var result = _collection.DeleteOne(m => m.Id == id); diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs index b2f27e46a..e343f346f 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs @@ -19,6 +19,11 @@ public MongoBikeRepository(MongoDbContext context) _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Bike entity) { var maxId = _collection.Find(_ => true) @@ -31,16 +36,31 @@ public int Create(Bike entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _collection.Find(_ => true).ToList(); } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Bike? Read(int id) { return _collection.Find(b => b.Id == id).FirstOrDefault(); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Bike? Update(int id, Bike entity) { entity.Id = id; @@ -48,6 +68,11 @@ public List ReadAll() return result.ModifiedCount > 0 ? entity : null; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var result = _collection.DeleteOne(b => b.Id == id); diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs index d2b8cd867..0257bd120 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs @@ -21,6 +21,11 @@ public MongoRentRepository(MongoDbContext context) _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Rent entity) { if (entity.Bike != null && entity.Bike.Id > 0) @@ -43,6 +48,10 @@ public int Create(Rent entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { @@ -56,6 +65,11 @@ public List ReadAll() return rents; } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Rent? Read(int id) { var rent = _collection.Find(r => r.Id == id).FirstOrDefault(); @@ -66,6 +80,12 @@ public List ReadAll() return rent; } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Rent? Update(int id, Rent entity) { entity.Id = id; @@ -76,12 +96,21 @@ public List ReadAll() return result.ModifiedCount > 0 ? entity : null; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var result = _collection.DeleteOne(r => r.Id == id); return result.DeletedCount > 0; } + /// + /// Load related data + /// + /// private void LoadRelatedData(Rent rent) { if (rent.Bike != null && rent.Bike.Id > 0) diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs index b5f08eb26..5edc0f617 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs @@ -19,6 +19,11 @@ public MongoRenterRepository(MongoDbContext context) _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } + /// + /// Creates a new object + /// + /// Object + /// ID of the created object public int Create(Renter entity) { var maxId = _collection.Find(_ => true) @@ -31,16 +36,31 @@ public int Create(Renter entity) return entity.Id; } + /// + /// Returns all existing objects + /// + /// List of existing objects public List ReadAll() { return _collection.Find(_ => true).ToList(); } + /// + /// Returns object by id + /// + /// Id + /// Object if exist public Renter? Read(int id) { return _collection.Find(r => r.Id == id).FirstOrDefault(); } + /// + /// Updates an existing object + /// + /// Id + /// Object + /// Object if exist public Renter? Update(int id, Renter entity) { entity.Id = id; @@ -48,6 +68,11 @@ public List ReadAll() return result.ModifiedCount > 0 ? entity : null; } + /// + /// Deletes an existing object by id + /// + /// + /// True or false? result of deleting public bool Delete(int id) { var result = _collection.DeleteOne(r => r.Id == id); diff --git a/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj b/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj index c3a3a8811..760aa0e6c 100644 --- a/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj +++ b/Bikes/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj @@ -1,20 +1,24 @@  - - net8.0 - enable - enable - true - + + net8.0 + enable + enable + true + false + - - - - - - - - - + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Bikes/Bikes.ServiceDefaults/Class1.cs b/Bikes/Bikes.ServiceDefaults/Class1.cs deleted file mode 100644 index dbab7fe89..000000000 --- a/Bikes/Bikes.ServiceDefaults/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bikes.ServiceDefaults; - -public class Class1 -{ - -} diff --git a/Bikes/Bikes.ServiceDefaults/Extensions.cs b/Bikes/Bikes.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..697fac22f --- /dev/null +++ b/Bikes/Bikes.ServiceDefaults/Extensions.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Bikes.ServiceDefaults; + +/// +/// Extensions for configuring standard services and middleware applications +/// +public static class Extensions +{ + /// + /// Adds standard services and default settings for the application + /// + /// Application Builder for service configuration + /// The same builder for the call chain + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.Services.Configure(logging => logging.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + + return builder; + } + + /// + /// Adds standard health checks for the app + /// + /// Health checks registration application builder + /// The same builder for the call chain + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Sets up standard endpoints for the application + /// + /// An instance of WebApplication for configuring routes + /// The same application instance for the call chain + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + return app; + } +} \ No newline at end of file From 3ee4acf02da42794a0844d1b9e7e9aee9aaa41ee Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 16 Dec 2025 17:02:48 +0400 Subject: [PATCH 32/41] Using EfCore --- .../appsettings.Development.json | 2 +- Bikes/Bikes.Api.Host/appsettings.json | 2 +- .../Mapping/MappingProfile.cs | 16 ++- .../Services/AnalyticsService.cs | 105 +++++++++++++++--- .../Bikes.Application/Services/BikeService.cs | 1 + .../Bikes.Application/Services/RentService.cs | 2 + Bikes/Bikes.Domain/Models/Bike.cs | 5 + Bikes/Bikes.Domain/Models/Rent.cs | 10 ++ .../Seeders/InMemorySeeder.cs | 60 +++++----- .../Bikes.Infrastructure.MongoDb.csproj | 4 +- .../BikesDbContext.cs | 41 +++++++ .../Extensions/ServiceCollectionExtensions.cs | 27 +++-- .../MongoDbContext.cs | 60 ---------- .../MongoDbSeeder.cs | 50 ++------- .../Repositories/MongoBikeModelRepository.cs | 55 +++++---- .../Repositories/MongoBikeRepository.cs | 55 +++++---- .../Repositories/MongoRentRepository.cs | 87 +++++---------- .../Repositories/MongoRenterRepository.cs | 54 +++++---- Bikes/Bikes.Tests/BikesFixture.cs | 2 +- 19 files changed, 350 insertions(+), 288 deletions(-) create mode 100644 Bikes/Bikes.Infrastructure.MongoDb/BikesDbContext.cs delete mode 100644 Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs diff --git a/Bikes/Bikes.Api.Host/appsettings.Development.json b/Bikes/Bikes.Api.Host/appsettings.Development.json index 6b4dbc6ac..40085cdca 100644 --- a/Bikes/Bikes.Api.Host/appsettings.Development.json +++ b/Bikes/Bikes.Api.Host/appsettings.Development.json @@ -6,7 +6,7 @@ } }, "MongoDb": { - "DatabaseName": "BikesDB" + "DatabaseName": "BikesDb_v2" }, "ConnectionStrings": { "MongoDB": "" diff --git a/Bikes/Bikes.Api.Host/appsettings.json b/Bikes/Bikes.Api.Host/appsettings.json index d49646f29..de382b4ed 100644 --- a/Bikes/Bikes.Api.Host/appsettings.json +++ b/Bikes/Bikes.Api.Host/appsettings.json @@ -7,7 +7,7 @@ }, "MongoDb": { "ConnectionString": "mongodb://localhost:27017", - "DatabaseName": "BikeRentalDb" + "DatabaseName": "BikesDb_v2" }, "AllowedHosts": "*" } diff --git a/Bikes/Bikes.Application/Mapping/MappingProfile.cs b/Bikes/Bikes.Application/Mapping/MappingProfile.cs index 64dd50de5..d70684d83 100644 --- a/Bikes/Bikes.Application/Mapping/MappingProfile.cs +++ b/Bikes/Bikes.Application/Mapping/MappingProfile.cs @@ -8,8 +8,11 @@ public class MappingProfile : Profile { public MappingProfile() { - CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.ModelId, opt => opt.MapFrom(src => src.ModelId)); + + CreateMap() + .ForMember(dest => dest.Model, opt => opt.Ignore()); CreateMap(); CreateMap(); @@ -17,7 +20,12 @@ public MappingProfile() CreateMap(); CreateMap(); - CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.RenterId, opt => opt.MapFrom(src => src.RenterId)) + .ForMember(dest => dest.BikeId, opt => opt.MapFrom(src => src.BikeId)); + + CreateMap() + .ForMember(dest => dest.Renter, opt => opt.Ignore()) + .ForMember(dest => dest.Bike, opt => opt.Ignore()); } } \ No newline at end of file diff --git a/Bikes/Bikes.Application/Services/AnalyticsService.cs b/Bikes/Bikes.Application/Services/AnalyticsService.cs index 4c2900dc5..85e009c84 100644 --- a/Bikes/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes/Bikes.Application/Services/AnalyticsService.cs @@ -13,6 +13,7 @@ public class AnalyticsService( IRepository bikeRepository, IRepository rentRepository, IRepository renterRepository, + IRepository bikeModelRepository, IMapper mapper) : IAnalyticsService { /// @@ -20,8 +21,17 @@ public class AnalyticsService( /// public List GetSportBikes() { - var sportBikes = bikeRepository.ReadAll() - .Where(bike => bike.Model.Type == BikeType.Sport) + var allBikes = bikeRepository.ReadAll(); + var allModels = bikeModelRepository.ReadAll(); + var modelDict = allModels.ToDictionary(m => m.Id); + foreach (var bike in allBikes) + { + if (modelDict.TryGetValue(bike.ModelId, out var model)) + bike.Model = model; + } + + var sportBikes = allBikes + .Where(bike => bike.Model != null && bike.Model.Type == BikeType.Sport) .ToList(); return mapper.Map>(sportBikes); @@ -32,8 +42,28 @@ public List GetSportBikes() /// public List GetTopFiveModelsByRentDuration() { - var topModels = rentRepository.ReadAll() - .GroupBy(rent => rent.Bike.Model) + var allRents = rentRepository.ReadAll(); + var allBikes = bikeRepository.ReadAll(); + var allModels = bikeModelRepository.ReadAll(); + + var bikeDict = allBikes.ToDictionary(b => b.Id); + var modelDict = allModels.ToDictionary(m => m.Id); + + foreach (var bike in allBikes) + { + if (modelDict.TryGetValue(bike.ModelId, out var model)) + bike.Model = model; + } + + foreach (var rent in allRents) + { + if (bikeDict.TryGetValue(rent.BikeId, out var bike)) + rent.Bike = bike; + } + + var topModels = allRents + .Where(rent => rent.Bike != null && rent.Bike.Model != null) + .GroupBy(rent => rent.Bike!.Model) .Select(group => new { Model = group.Key, @@ -52,12 +82,32 @@ public List GetTopFiveModelsByRentDuration() /// public List GetTopFiveModelsByProfit() { - var topModels = rentRepository.ReadAll() - .GroupBy(rent => rent.Bike.Model) + var allRents = rentRepository.ReadAll(); + var allBikes = bikeRepository.ReadAll(); + var allModels = bikeModelRepository.ReadAll(); + + var bikeDict = allBikes.ToDictionary(b => b.Id); + var modelDict = allModels.ToDictionary(m => m.Id); + + foreach (var bike in allBikes) + { + if (modelDict.TryGetValue(bike.ModelId, out var model)) + bike.Model = model; + } + + foreach (var rent in allRents) + { + if (bikeDict.TryGetValue(rent.BikeId, out var bike)) + rent.Bike = bike; + } + + var topModels = allRents + .Where(rent => rent.Bike != null && rent.Bike.Model != null) + .GroupBy(rent => rent.Bike!.Model) .Select(group => new { Model = group.Key, - TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike.Model.RentPrice) + TotalProfit = group.Sum(rent => rent.RentalDuration * rent.Bike!.Model!.RentPrice) }) .OrderByDescending(x => x.TotalProfit) .Take(5) @@ -89,8 +139,28 @@ public RentalDurationStatsDto GetRentalDurationStats() /// public Dictionary GetTotalRentalTimeByType() { - return rentRepository.ReadAll() - .GroupBy(rent => rent.Bike.Model.Type) + var allRents = rentRepository.ReadAll(); + var allBikes = bikeRepository.ReadAll(); + var allModels = bikeModelRepository.ReadAll(); + + var bikeDict = allBikes.ToDictionary(b => b.Id); + var modelDict = allModels.ToDictionary(m => m.Id); + + foreach (var bike in allBikes) + { + if (modelDict.TryGetValue(bike.ModelId, out var model)) + bike.Model = model; + } + + foreach (var rent in allRents) + { + if (bikeDict.TryGetValue(rent.BikeId, out var bike)) + rent.Bike = bike; + } + + return allRents + .Where(rent => rent.Bike != null && rent.Bike.Model != null) + .GroupBy(rent => rent.Bike!.Model!.Type) .ToDictionary( group => group.Key, group => group.Sum(rent => rent.RentalDuration) @@ -102,10 +172,19 @@ public Dictionary GetTotalRentalTimeByType() /// public List GetTopThreeRenters() { - var renters = renterRepository.ReadAll(); + var allRents = rentRepository.ReadAll(); + var allRenters = renterRepository.ReadAll(); + + var renterDict = allRenters.ToDictionary(r => r.Id); + foreach (var rent in allRents) + { + if (renterDict.TryGetValue(rent.RenterId, out var renter)) + rent.Renter = renter; + } - var topRenters = rentRepository.ReadAll() - .GroupBy(rent => rent.Renter.Id) + var topRenters = allRents + .Where(rent => rent.Renter != null) + .GroupBy(rent => rent.Renter!.Id) .Select(group => new { RenterId = group.Key, @@ -113,7 +192,7 @@ public List GetTopThreeRenters() }) .OrderByDescending(r => r.TotalRentals) .Take(3) - .Join(renters, + .Join(allRenters, x => x.RenterId, renter => renter.Id, (x, renter) => renter) diff --git a/Bikes/Bikes.Application/Services/BikeService.cs b/Bikes/Bikes.Application/Services/BikeService.cs index 2cb0b666f..abf14e994 100644 --- a/Bikes/Bikes.Application/Services/BikeService.cs +++ b/Bikes/Bikes.Application/Services/BikeService.cs @@ -26,6 +26,7 @@ public BikeGetDto CreateBike(BikeCreateUpdateDto bikeDto) var bike = mapper.Map(bikeDto); bike.Model = model; + bike.ModelId = bikeDto.ModelId; var id = bikeRepository.Create(bike); diff --git a/Bikes/Bikes.Application/Services/RentService.cs b/Bikes/Bikes.Application/Services/RentService.cs index 41f3571a3..9e313fe4e 100644 --- a/Bikes/Bikes.Application/Services/RentService.cs +++ b/Bikes/Bikes.Application/Services/RentService.cs @@ -32,6 +32,8 @@ public RentGetDto CreateRent(RentCreateUpdateDto rentDto) var rent = mapper.Map(rentDto); rent.Bike = bike; rent.Renter = renter; + rent.BikeId = rentDto.BikeId; + rent.RenterId = rentDto.RenterId; var id = rentRepository.Create(rent); var createdRent = rentRepository.Read(id); diff --git a/Bikes/Bikes.Domain/Models/Bike.cs b/Bikes/Bikes.Domain/Models/Bike.cs index f686c1a91..75e4a0b32 100644 --- a/Bikes/Bikes.Domain/Models/Bike.cs +++ b/Bikes/Bikes.Domain/Models/Bike.cs @@ -19,6 +19,11 @@ public class Bike /// Bike's color /// public required string Color { get; set; } + + /// + /// Bike's model id for database + /// + public required int ModelId { get; set; } /// /// Bike's model diff --git a/Bikes/Bikes.Domain/Models/Rent.cs b/Bikes/Bikes.Domain/Models/Rent.cs index 0d2b173f9..0ebfc2d22 100644 --- a/Bikes/Bikes.Domain/Models/Rent.cs +++ b/Bikes/Bikes.Domain/Models/Rent.cs @@ -20,6 +20,16 @@ public class Rent /// public required int RentalDuration { get; set; } + /// + /// Renter's id for database + /// + public required int RenterId { get; set; } + + /// + /// Bike's id for database + /// + public required int BikeId { get; set; } + /// /// Renter /// diff --git a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs index dd3b9366f..a2d465c7d 100644 --- a/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs +++ b/Bikes/Bikes.Infrastructure.InMemory/Seeders/InMemorySeeder.cs @@ -36,16 +36,16 @@ public static List GetBikes() return [ - new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", Model = models[0] }, - new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", Model = models[1] }, - new() { Id = 3, SerialNumber = "CTY202203001", Color = "Синий", Model = models[2] }, - new() { Id = 4, SerialNumber = "MTB202302001", Color = "Зеленый", Model = models[3] }, - new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", Model = models[4] }, - new() { Id = 6, SerialNumber = "CTY202304001", Color = "Белый", Model = models[5] }, - new() { Id = 7, SerialNumber = "MTB202404001", Color = "Оранжевый", Model = models[6] }, - new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", Model = models[7] }, - new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", Model = models[8] }, - new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", Model = models[9] } + new() { Id = 1, SerialNumber = "MTB202301001", Color = "Черный", ModelId = models[0].Id, Model = models[0] }, + new() { Id = 2, SerialNumber = "SPT202402001", Color = "Красный", ModelId = models[1].Id, Model = models[1] }, + new() { Id = 3, SerialNumber = "CTY202203001", Color = "Синий", ModelId = models[2].Id, Model = models[2] }, + new() { Id = 4, SerialNumber = "MTB202302001", Color = "Зеленый", ModelId = models[3].Id, Model = models[3] }, + new() { Id = 5, SerialNumber = "SPT202403001", Color = "Желтый", ModelId = models[4].Id, Model = models[4] }, + new() { Id = 6, SerialNumber = "CTY202304001", Color = "Белый", ModelId = models[5].Id, Model = models[5] }, + new() { Id = 7, SerialNumber = "MTB202404001", Color = "Оранжевый", ModelId = models[6].Id, Model = models[6] }, + new() { Id = 8, SerialNumber = "SPT202305001", Color = "Фиолетовый", ModelId = models[7].Id, Model = models[7] }, + new() { Id = 9, SerialNumber = "CTY202205001", Color = "Серый", ModelId = models[8].Id, Model = models[8] }, + new() { Id = 10, SerialNumber = "MTB202405001", Color = "Голубой", ModelId = models[9].Id, Model = models[9] } ]; } @@ -79,26 +79,26 @@ public static List GetRents() return [ - new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, Renter = renters[0], Bike = bikes[0] }, - new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, Renter = renters[1], Bike = bikes[0] }, - new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, Renter = renters[2], Bike = bikes[0] }, - new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, Renter = renters[3], Bike = bikes[1] }, - new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, Renter = renters[4], Bike = bikes[1] }, - new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, Renter = renters[5], Bike = bikes[1] }, - new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, Renter = renters[6], Bike = bikes[2] }, - new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, Renter = renters[7], Bike = bikes[2] }, - new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, Renter = renters[8], Bike = bikes[3] }, - new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, Renter = renters[9], Bike = bikes[3] }, - new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, Renter = renters[0], Bike = bikes[4] }, - new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, Renter = renters[0], Bike = bikes[4] }, - new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, Renter = renters[0], Bike = bikes[5] }, - new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, Renter = renters[0], Bike = bikes[6] }, - new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, Renter = renters[1], Bike = bikes[7] }, - new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, Renter = renters[1], Bike = bikes[8] }, - new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, Renter = renters[1], Bike = bikes[9] }, - new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, Renter = renters[5], Bike = bikes[9] }, - new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, Renter = renters[5], Bike = bikes[9] }, - new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, Renter = renters[2], Bike = bikes[9] } + new() { Id = 1, RentalStartTime = new DateTime(2025, 6, 10, 9, 0, 0), RentalDuration = 3, RenterId = renters[0].Id, BikeId = bikes[0].Id, Renter = renters[0], Bike = bikes[0] }, + new() { Id = 2, RentalStartTime = new DateTime(2025, 6, 12, 14, 30, 0), RentalDuration = 2, RenterId = renters[1].Id, BikeId = bikes[0].Id, Renter = renters[1], Bike = bikes[0] }, + new() { Id = 3, RentalStartTime = new DateTime(2025, 6, 15, 10, 0, 0), RentalDuration = 4, RenterId = renters[2].Id, BikeId = bikes[0].Id, Renter = renters[2], Bike = bikes[0] }, + new() { Id = 4, RentalStartTime = new DateTime(2025, 6, 18, 16, 0, 0), RentalDuration = 1, RenterId = renters[3].Id, BikeId = bikes[1].Id, Renter = renters[3], Bike = bikes[1] }, + new() { Id = 5, RentalStartTime = new DateTime(2025, 6, 20, 11, 0, 0), RentalDuration = 5, RenterId = renters[4].Id, BikeId = bikes[1].Id, Renter = renters[4], Bike = bikes[1] }, + new() { Id = 6, RentalStartTime = new DateTime(2025, 6, 22, 13, 0, 0), RentalDuration = 2, RenterId = renters[5].Id, BikeId = bikes[1].Id, Renter = renters[5], Bike = bikes[1] }, + new() { Id = 7, RentalStartTime = new DateTime(2025, 6, 25, 15, 30, 0), RentalDuration = 3, RenterId = renters[6].Id, BikeId = bikes[2].Id, Renter = renters[6], Bike = bikes[2] }, + new() { Id = 8, RentalStartTime = new DateTime(2025, 6, 28, 9, 30, 0), RentalDuration = 4, RenterId = renters[7].Id, BikeId = bikes[2].Id, Renter = renters[7], Bike = bikes[2] }, + new() { Id = 9, RentalStartTime = new DateTime(2025, 7, 1, 12, 0, 0), RentalDuration = 1, RenterId = renters[8].Id, BikeId = bikes[3].Id, Renter = renters[8], Bike = bikes[3] }, + new() { Id = 10, RentalStartTime = new DateTime(2025, 7, 3, 17, 0, 0), RentalDuration = 2, RenterId = renters[9].Id, BikeId = bikes[3].Id, Renter = renters[9], Bike = bikes[3] }, + new() { Id = 11, RentalStartTime = new DateTime(2025, 7, 5, 10, 0, 0), RentalDuration = 3, RenterId = renters[0].Id, BikeId = bikes[4].Id, Renter = renters[0], Bike = bikes[4] }, + new() { Id = 12, RentalStartTime = new DateTime(2025, 7, 8, 14, 0, 0), RentalDuration = 5, RenterId = renters[0].Id, BikeId = bikes[4].Id, Renter = renters[0], Bike = bikes[4] }, + new() { Id = 13, RentalStartTime = new DateTime(2025, 7, 10, 16, 30, 0), RentalDuration = 2, RenterId = renters[0].Id, BikeId = bikes[5].Id, Renter = renters[0], Bike = bikes[5] }, + new() { Id = 14, RentalStartTime = new DateTime(2025, 7, 12, 11, 0, 0), RentalDuration = 4, RenterId = renters[0].Id, BikeId = bikes[6].Id, Renter = renters[0], Bike = bikes[6] }, + new() { Id = 15, RentalStartTime = new DateTime(2025, 7, 15, 13, 0, 0), RentalDuration = 1, RenterId = renters[1].Id, BikeId = bikes[7].Id, Renter = renters[1], Bike = bikes[7] }, + new() { Id = 16, RentalStartTime = new DateTime(2025, 7, 18, 15, 0, 0), RentalDuration = 3, RenterId = renters[1].Id, BikeId = bikes[8].Id, Renter = renters[1], Bike = bikes[8] }, + new() { Id = 17, RentalStartTime = new DateTime(2025, 7, 20, 9, 0, 0), RentalDuration = 2, RenterId = renters[1].Id, BikeId = bikes[9].Id, Renter = renters[1], Bike = bikes[9] }, + new() { Id = 18, RentalStartTime = new DateTime(2025, 7, 22, 12, 30, 0), RentalDuration = 5, RenterId = renters[5].Id, BikeId = bikes[9].Id, Renter = renters[5], Bike = bikes[9] }, + new() { Id = 19, RentalStartTime = new DateTime(2025, 7, 25, 14, 0, 0), RentalDuration = 3, RenterId = renters[5].Id, BikeId = bikes[9].Id, Renter = renters[5], Bike = bikes[9] }, + new() { Id = 20, RentalStartTime = new DateTime(2025, 7, 28, 16, 0, 0), RentalDuration = 4, RenterId = renters[2].Id, BikeId = bikes[9].Id, Renter = renters[2], Bike = bikes[9] } ]; } diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj index 1334912f0..d17b0eee6 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj +++ b/Bikes/Bikes.Infrastructure.MongoDb/Bikes.Infrastructure.MongoDb.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/Bikes/Bikes.Infrastructure.MongoDb/BikesDbContext.cs b/Bikes/Bikes.Infrastructure.MongoDb/BikesDbContext.cs new file mode 100644 index 000000000..e8ae42d62 --- /dev/null +++ b/Bikes/Bikes.Infrastructure.MongoDb/BikesDbContext.cs @@ -0,0 +1,41 @@ +using Bikes.Domain.Models; +using Microsoft.EntityFrameworkCore; +using MongoDB.EntityFrameworkCore.Extensions; + +namespace Bikes.Infrastructure.MongoDb; + +/// +/// Database context for working with MongoDB +/// +public class BikesDbContext : DbContext +{ + public BikesDbContext(DbContextOptions options) : base(options) + { + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + } + + public DbSet BikeModels => Set(); + public DbSet Bikes => Set(); + public DbSet Renters => Set(); + public DbSet Rents => Set(); + + /// + /// Configuring the database model. + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().ToCollection("bike_models"); + modelBuilder.Entity().ToCollection("bikes"); + modelBuilder.Entity().ToCollection("renters"); + modelBuilder.Entity().ToCollection("rents"); + + modelBuilder.Entity() + .Ignore(b => b.Model); + + modelBuilder.Entity() + .Ignore(r => r.Renter) + .Ignore(r => r.Bike); + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs index 1d028b0b8..c68ce0e3f 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; using Bikes.Domain.Repositories; using Bikes.Infrastructure.MongoDb.Configuration; using Bikes.Infrastructure.MongoDb.Repositories; @@ -12,17 +13,17 @@ namespace Bikes.Infrastructure.MongoDb.Extensions; public static class ServiceCollectionExtensions { /// - /// The method that registers services + /// A method for registering MongoDB services through the Entity Framework Core /// - /// - /// - /// + /// Collection of services + /// Application Configuration + /// Collection of services public static IServiceCollection AddMongoDbInfrastructure( this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString("MongoDB"); - var databaseName = configuration["MongoDb:DatabaseName"] ?? "BikesDB"; + var databaseName = configuration["MongoDb:DatabaseName"]!; services.Configure(options => { @@ -31,13 +32,17 @@ public static IServiceCollection AddMongoDbInfrastructure( options.DatabaseName = databaseName; }); - services.AddSingleton(); - services.AddSingleton(); + services.AddDbContext(options => + { + options.UseMongoDB(connectionString ?? "mongodb://localhost:27017", databaseName); + }); + + services.AddScoped(); - services.AddSingleton, MongoBikeRepository>(); - services.AddSingleton, MongoBikeModelRepository>(); - services.AddSingleton, MongoRenterRepository>(); - services.AddSingleton, MongoRentRepository>(); + services.AddScoped, MongoBikeRepository>(); + services.AddScoped, MongoBikeModelRepository>(); + services.AddScoped, MongoRenterRepository>(); + services.AddScoped, MongoRentRepository>(); return services; } diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs deleted file mode 100644 index 15a13e05e..000000000 --- a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbContext.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Extensions.Options; -using MongoDB.Driver; -using Bikes.Domain.Models; -using Bikes.Infrastructure.MongoDb.Configuration; - -namespace Bikes.Infrastructure.MongoDb; - -/// -/// The context of connecting to MongoDB. -/// -public class MongoDbContext -{ - /// - /// Link to the MongoDB database - /// - private readonly IMongoDatabase _database; - - /// - /// A constructor that accepts settings via the IOptions pattern. - /// - /// MongoDB connection settings transmitted from the AppHost - /// - /// - public MongoDbContext(IOptions settings) - { - if (settings?.Value == null) - throw new ArgumentNullException(nameof(settings), "MongoDbSettings is not configured"); - - var mongoDbSettings = settings.Value; - - if (string.IsNullOrEmpty(mongoDbSettings.ConnectionString)) - throw new ArgumentException("MongoDB connection string is not configured"); - - if (string.IsNullOrEmpty(mongoDbSettings.DatabaseName)) - mongoDbSettings.DatabaseName = "BikesDB"; - - var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); - _database = mongoClient.GetDatabase(mongoDbSettings.DatabaseName); - } - - /// - /// Bike collection. Corresponds to the "bikes" collection in MongoDB. - /// - public IMongoCollection Bikes => _database.GetCollection("bikes"); - - /// - /// A collection of bike models. Corresponds to the collection "bike_models" in MongoDB. - /// - public IMongoCollection BikeModels => _database.GetCollection("bike_models"); - - /// - /// Collection of renters. Corresponds to the "renters" collection in MongoDB - /// - public IMongoCollection Renters => _database.GetCollection("renters"); - - /// - /// A collection of rental records. Corresponds to the "rents" collection in MongoDB. - /// - public IMongoCollection Rents => _database.GetCollection("rents"); -} \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs index 8efae53ff..3331f608a 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs @@ -1,4 +1,4 @@ -using MongoDB.Driver; +using Microsoft.EntityFrameworkCore; using Bikes.Infrastructure.InMemory.Seeders; using Microsoft.Extensions.Logging; @@ -9,31 +9,18 @@ namespace Bikes.Infrastructure.MongoDb; /// public class MongoDbSeeder { - - private readonly MongoDbContext _context; - + private readonly BikesDbContext _context; private readonly ILogger _logger; - /// - /// Constructor of the MongoDbSeeder class - /// - /// MongoDB context for working with the database - /// Logger for recording diagnostic information - public MongoDbSeeder(MongoDbContext context, ILogger logger) + public MongoDbSeeder(BikesDbContext context, ILogger logger) { _context = context; _logger = logger; } - /// - /// The main method for filling the database with initial data - /// - /// Asynchronous task public async Task SeedAsync() { - var hasModels = await _context.BikeModels.Find(_ => true).AnyAsync(); - - if (hasModels) + if (await _context.Bikes.AnyAsync()) { _logger.LogInformation("Database already contains data. Skipping seeding."); return; @@ -46,30 +33,13 @@ public async Task SeedAsync() var renters = InMemorySeeder.GetRenters(); var rents = InMemorySeeder.GetRents(); - if (models.Any()) - { - await _context.BikeModels.InsertManyAsync(models); - _logger.LogInformation("Inserted {Count} bike models into database", models.Count); - } + await _context.BikeModels.AddRangeAsync(models); + await _context.Bikes.AddRangeAsync(bikes); + await _context.Renters.AddRangeAsync(renters); + await _context.Rents.AddRangeAsync(rents); - if (bikes.Any()) - { - await _context.Bikes.InsertManyAsync(bikes); - _logger.LogInformation("Inserted {Count} bikes into database", bikes.Count); - } - - if (renters.Any()) - { - await _context.Renters.InsertManyAsync(renters); - _logger.LogInformation("Inserted {Count} renters into database", renters.Count); - } - - if (rents.Any()) - { - await _context.Rents.InsertManyAsync(rents); - _logger.LogInformation("Inserted {Count} rents into database", rents.Count); - } + await _context.SaveChangesAsync(); - _logger.LogInformation("MongoDB database seeding completed successfully"); + _logger.LogInformation("Seeding completed."); } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs index 691fb03f8..0186b8ef0 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs @@ -1,22 +1,18 @@ -using MongoDB.Driver; -using Bikes.Domain.Models; +using Bikes.Domain.Models; using Bikes.Domain.Repositories; namespace Bikes.Infrastructure.MongoDb.Repositories; /// -/// Repository for working with bike models in MongoDB +/// A repository for working with bike models in MongoDB /// public class MongoBikeModelRepository : IRepository { - private readonly IMongoCollection _collection; + private readonly BikesDbContext _context; - public MongoBikeModelRepository(MongoDbContext context) + public MongoBikeModelRepository(BikesDbContext context) { - _collection = context.BikeModels; - - var indexKeysDefinition = Builders.IndexKeys.Ascending(m => m.Id); - _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + _context = context; } /// @@ -26,13 +22,18 @@ public MongoBikeModelRepository(MongoDbContext context) /// ID of the created object public int Create(BikeModel entity) { - var maxId = _collection.Find(_ => true) - .SortByDescending(m => m.Id) - .Limit(1) - .FirstOrDefault()?.Id ?? 0; + if (entity.Id == 0) + { + var lastId = _context.BikeModels + .OrderByDescending(b => b.Id) + .Select(b => b.Id) + .FirstOrDefault(); + + entity.Id = lastId + 1; + } - entity.Id = maxId + 1; - _collection.InsertOne(entity); + _context.BikeModels.Add(entity); + _context.SaveChanges(); return entity.Id; } @@ -42,7 +43,7 @@ public int Create(BikeModel entity) /// List of existing objects public List ReadAll() { - return _collection.Find(_ => true).ToList(); + return _context.BikeModels.ToList(); } /// @@ -52,7 +53,7 @@ public List ReadAll() /// Object if exist public BikeModel? Read(int id) { - return _collection.Find(m => m.Id == id).FirstOrDefault(); + return _context.BikeModels.Find(id); } /// @@ -63,9 +64,15 @@ public List ReadAll() /// Object if exist public BikeModel? Update(int id, BikeModel entity) { - entity.Id = id; - var result = _collection.ReplaceOne(m => m.Id == id, entity); - return result.ModifiedCount > 0 ? entity : null; + var existingModel = _context.BikeModels.Find(id); + if (existingModel == null) return null; + + _context.Entry(existingModel).CurrentValues.SetValues(entity); + + existingModel.Id = id; + + _context.SaveChanges(); + return existingModel; } /// @@ -75,7 +82,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var result = _collection.DeleteOne(m => m.Id == id); - return result.DeletedCount > 0; + var model = _context.BikeModels.Find(id); + if (model == null) return false; + + _context.BikeModels.Remove(model); + _context.SaveChanges(); + return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs index e343f346f..c9549f811 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs @@ -1,22 +1,18 @@ -using MongoDB.Driver; -using Bikes.Domain.Models; +using Bikes.Domain.Models; using Bikes.Domain.Repositories; namespace Bikes.Infrastructure.MongoDb.Repositories; /// -/// Repository for working with bikes in MongoDB +/// A repository for working with bikes in MongoDB /// public class MongoBikeRepository : IRepository { - private readonly IMongoCollection _collection; + private readonly BikesDbContext _context; - public MongoBikeRepository(MongoDbContext context) + public MongoBikeRepository(BikesDbContext context) { - _collection = context.Bikes; - - var indexKeysDefinition = Builders.IndexKeys.Ascending(b => b.Id); - _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + _context = context; } /// @@ -26,13 +22,18 @@ public MongoBikeRepository(MongoDbContext context) /// ID of the created object public int Create(Bike entity) { - var maxId = _collection.Find(_ => true) - .SortByDescending(b => b.Id) - .Limit(1) - .FirstOrDefault()?.Id ?? 0; + if (entity.Id == 0) + { + var lastId = _context.Bikes + .OrderByDescending(b => b.Id) + .Select(b => b.Id) + .FirstOrDefault(); + + entity.Id = lastId + 1; + } - entity.Id = maxId + 1; - _collection.InsertOne(entity); + _context.Bikes.Add(entity); + _context.SaveChanges(); return entity.Id; } @@ -42,7 +43,7 @@ public int Create(Bike entity) /// List of existing objects public List ReadAll() { - return _collection.Find(_ => true).ToList(); + return _context.Bikes.ToList(); } /// @@ -52,7 +53,7 @@ public List ReadAll() /// Object if exist public Bike? Read(int id) { - return _collection.Find(b => b.Id == id).FirstOrDefault(); + return _context.Bikes.FirstOrDefault(b => b.Id == id); } /// @@ -63,9 +64,15 @@ public List ReadAll() /// Object if exist public Bike? Update(int id, Bike entity) { - entity.Id = id; - var result = _collection.ReplaceOne(b => b.Id == id, entity); - return result.ModifiedCount > 0 ? entity : null; + var existingBike = _context.Bikes.FirstOrDefault(b => b.Id == id); + if (existingBike == null) return null; + + existingBike.SerialNumber = entity.SerialNumber; + existingBike.Color = entity.Color; + existingBike.ModelId = entity.ModelId; + + _context.SaveChanges(); + return existingBike; } /// @@ -75,7 +82,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var result = _collection.DeleteOne(b => b.Id == id); - return result.DeletedCount > 0; + var bike = _context.Bikes.Find(id); + if (bike == null) return false; + + _context.Bikes.Remove(bike); + _context.SaveChanges(); + return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs index 0257bd120..230633e86 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs @@ -1,24 +1,18 @@ -using MongoDB.Driver; -using Bikes.Domain.Models; +using Bikes.Domain.Models; using Bikes.Domain.Repositories; namespace Bikes.Infrastructure.MongoDb.Repositories; /// -/// Repository for working with rents in MongoDB +/// A repository for working with rents in MongoDB /// public class MongoRentRepository : IRepository { - private readonly IMongoCollection _collection; - private readonly MongoDbContext _context; + private readonly BikesDbContext _context; - public MongoRentRepository(MongoDbContext context) + public MongoRentRepository(BikesDbContext context) { _context = context; - _collection = context.Rents; - - var indexKeysDefinition = Builders.IndexKeys.Ascending(r => r.Id); - _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } /// @@ -28,23 +22,18 @@ public MongoRentRepository(MongoDbContext context) /// ID of the created object public int Create(Rent entity) { - if (entity.Bike != null && entity.Bike.Id > 0) + if (entity.Id == 0) { - entity.Bike = _context.Bikes.Find(b => b.Id == entity.Bike.Id).FirstOrDefault(); - } + var lastId = _context.Rents + .OrderByDescending(b => b.Id) + .Select(b => b.Id) + .FirstOrDefault(); - if (entity.Renter != null && entity.Renter.Id > 0) - { - entity.Renter = _context.Renters.Find(r => r.Id == entity.Renter.Id).FirstOrDefault(); + entity.Id = lastId + 1; } - var maxId = _collection.Find(_ => true) - .SortByDescending(r => r.Id) - .Limit(1) - .FirstOrDefault()?.Id ?? 0; - - entity.Id = maxId + 1; - _collection.InsertOne(entity); + _context.Rents.Add(entity); + _context.SaveChanges(); return entity.Id; } @@ -54,15 +43,7 @@ public int Create(Rent entity) /// List of existing objects public List ReadAll() { - - var rents = _collection.Find(_ => true).ToList(); - - foreach (var rent in rents) - { - LoadRelatedData(rent); - } - - return rents; + return _context.Rents.ToList(); } /// @@ -72,12 +53,7 @@ public List ReadAll() /// Object if exist public Rent? Read(int id) { - var rent = _collection.Find(r => r.Id == id).FirstOrDefault(); - if (rent != null) - { - LoadRelatedData(rent); - } - return rent; + return _context.Rents.FirstOrDefault(r => r.Id == id); } /// @@ -88,12 +64,16 @@ public List ReadAll() /// Object if exist public Rent? Update(int id, Rent entity) { - entity.Id = id; + var existingRent = _context.Rents.FirstOrDefault(r => r.Id == id); + if (existingRent == null) return null; - LoadRelatedData(entity); + existingRent.RentalStartTime = entity.RentalStartTime; + existingRent.RentalDuration = entity.RentalDuration; + existingRent.BikeId = entity.BikeId; + existingRent.RenterId = entity.RenterId; - var result = _collection.ReplaceOne(r => r.Id == id, entity); - return result.ModifiedCount > 0 ? entity : null; + _context.SaveChanges(); + return existingRent; } /// @@ -103,24 +83,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var result = _collection.DeleteOne(r => r.Id == id); - return result.DeletedCount > 0; - } - - /// - /// Load related data - /// - /// - private void LoadRelatedData(Rent rent) - { - if (rent.Bike != null && rent.Bike.Id > 0) - { - rent.Bike = _context.Bikes.Find(b => b.Id == rent.Bike.Id).FirstOrDefault(); - } + var rent = _context.Rents.Find(id); + if (rent == null) return false; - if (rent.Renter != null && rent.Renter.Id > 0) - { - rent.Renter = _context.Renters.Find(r => r.Id == rent.Renter.Id).FirstOrDefault(); - } + _context.Rents.Remove(rent); + _context.SaveChanges(); + return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs index 5edc0f617..7c5868a95 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs @@ -1,22 +1,18 @@ -using MongoDB.Driver; -using Bikes.Domain.Models; +using Bikes.Domain.Models; using Bikes.Domain.Repositories; namespace Bikes.Infrastructure.MongoDb.Repositories; /// -/// Repository for working with renters in MongoDB +/// A repository for working with renters in MongoDB /// public class MongoRenterRepository : IRepository { - private readonly IMongoCollection _collection; + private readonly BikesDbContext _context; - public MongoRenterRepository(MongoDbContext context) + public MongoRenterRepository(BikesDbContext context) { - _collection = context.Renters; - - var indexKeysDefinition = Builders.IndexKeys.Ascending(r => r.Id); - _collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + _context = context; } /// @@ -26,13 +22,18 @@ public MongoRenterRepository(MongoDbContext context) /// ID of the created object public int Create(Renter entity) { - var maxId = _collection.Find(_ => true) - .SortByDescending(r => r.Id) - .Limit(1) - .FirstOrDefault()?.Id ?? 0; + if (entity.Id == 0) + { + var lastId = _context.Renters + .OrderByDescending(b => b.Id) + .Select(b => b.Id) + .FirstOrDefault(); + + entity.Id = lastId + 1; + } - entity.Id = maxId + 1; - _collection.InsertOne(entity); + _context.Renters.Add(entity); + _context.SaveChanges(); return entity.Id; } @@ -42,7 +43,7 @@ public int Create(Renter entity) /// List of existing objects public List ReadAll() { - return _collection.Find(_ => true).ToList(); + return _context.Renters.ToList(); } /// @@ -52,7 +53,7 @@ public List ReadAll() /// Object if exist public Renter? Read(int id) { - return _collection.Find(r => r.Id == id).FirstOrDefault(); + return _context.Renters.Find(id); } /// @@ -63,9 +64,14 @@ public List ReadAll() /// Object if exist public Renter? Update(int id, Renter entity) { - entity.Id = id; - var result = _collection.ReplaceOne(r => r.Id == id, entity); - return result.ModifiedCount > 0 ? entity : null; + var existingRenter = _context.Renters.Find(id); + if (existingRenter == null) return null; + + _context.Entry(existingRenter).CurrentValues.SetValues(entity); + existingRenter.Id = id; + + _context.SaveChanges(); + return existingRenter; } /// @@ -75,7 +81,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var result = _collection.DeleteOne(r => r.Id == id); - return result.DeletedCount > 0; + var renter = _context.Renters.Find(id); + if (renter == null) return false; + + _context.Renters.Remove(renter); + _context.SaveChanges(); + return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Tests/BikesFixture.cs b/Bikes/Bikes.Tests/BikesFixture.cs index bda3dba9f..af3590f7a 100644 --- a/Bikes/Bikes.Tests/BikesFixture.cs +++ b/Bikes/Bikes.Tests/BikesFixture.cs @@ -32,6 +32,6 @@ public BikesFixture() _mapper = configuration.CreateMapper(); - AnalyticsService = new AnalyticsService(bikeRepo, rentRepo, renterRepo, _mapper); + AnalyticsService = new AnalyticsService(bikeRepo, rentRepo, renterRepo, modelRepo, _mapper); } } From 8f9208643b918313216922d83a5bfc05a27a7125 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Tue, 16 Dec 2025 17:18:14 +0400 Subject: [PATCH 33/41] Update readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index e29af7491..0e59cbb50 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,18 @@ ### Bikes.Tests - модульные тесты для проверки функциональности +### Bikes.Contracts - слой DTO + +## Лабораторная работа 3 - "ORM" + +В рамках третьей лабораторной работы хранение данных было переделано с InMemory коллекий на базу данных. +Также был настроен оркестратор Aspire на запуск сервера и базы данных + +Были добавлены слои: + +### Bikes.Infrastructure.MongoDb - инфраструктурный слой с реализацией хранения данных в БД MongoDb + +### Bikes.ServiceDefaults - слой инфраструктурных стандартов, cодержащий конфигурации и расширения по умолчанию для всех сервисов приложения + +### Bikes.AppHost - слой оркестрации приложений + From adf69fad53149737f5f9a18ef2d78ad0e58d31b4 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 17 Dec 2025 08:32:49 +0400 Subject: [PATCH 34/41] Small fixes --- Bikes/Bikes.AppHost/Program.cs | 2 +- .../MongoDbSeeder.cs | 31 +++++++----------- .../Repositories/MongoBikeModelRepository.cs | 32 ++++++++----------- .../Repositories/MongoBikeRepository.cs | 30 +++++++---------- .../Repositories/MongoRentRepository.cs | 30 +++++++---------- .../Repositories/MongoRenterRepository.cs | 32 ++++++++----------- 6 files changed, 63 insertions(+), 94 deletions(-) diff --git a/Bikes/Bikes.AppHost/Program.cs b/Bikes/Bikes.AppHost/Program.cs index 50be2abeb..0e19116cc 100644 --- a/Bikes/Bikes.AppHost/Program.cs +++ b/Bikes/Bikes.AppHost/Program.cs @@ -3,7 +3,7 @@ var mongodb = builder.AddMongoDB("mongodb") .WithDataVolume(); -var api = builder.AddProject("bikes-api") +_ = builder.AddProject("bikes-api") .WithReference(mongodb); builder.Build().Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs index 3331f608a..91f21da24 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/MongoDbSeeder.cs @@ -7,39 +7,32 @@ namespace Bikes.Infrastructure.MongoDb; /// /// A class for initializing initial data in MongoDB /// -public class MongoDbSeeder +public class MongoDbSeeder( + BikesDbContext context, + ILogger logger) { - private readonly BikesDbContext _context; - private readonly ILogger _logger; - - public MongoDbSeeder(BikesDbContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - public async Task SeedAsync() { - if (await _context.Bikes.AnyAsync()) + if (await context.Bikes.AnyAsync()) { - _logger.LogInformation("Database already contains data. Skipping seeding."); + logger.LogInformation("Database already contains data. Skipping seeding."); return; } - _logger.LogInformation("Starting MongoDB database seeding..."); + logger.LogInformation("Starting MongoDB database seeding..."); var models = InMemorySeeder.GetBikeModels(); var bikes = InMemorySeeder.GetBikes(); var renters = InMemorySeeder.GetRenters(); var rents = InMemorySeeder.GetRents(); - await _context.BikeModels.AddRangeAsync(models); - await _context.Bikes.AddRangeAsync(bikes); - await _context.Renters.AddRangeAsync(renters); - await _context.Rents.AddRangeAsync(rents); + await context.BikeModels.AddRangeAsync(models); + await context.Bikes.AddRangeAsync(bikes); + await context.Renters.AddRangeAsync(renters); + await context.Rents.AddRangeAsync(rents); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); - _logger.LogInformation("Seeding completed."); + logger.LogInformation("Seeding completed."); } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs index 0186b8ef0..e80e4a475 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeModelRepository.cs @@ -6,15 +6,9 @@ namespace Bikes.Infrastructure.MongoDb.Repositories; /// /// A repository for working with bike models in MongoDB /// -public class MongoBikeModelRepository : IRepository +public class MongoBikeModelRepository( + BikesDbContext context) : IRepository { - private readonly BikesDbContext _context; - - public MongoBikeModelRepository(BikesDbContext context) - { - _context = context; - } - /// /// Creates a new object /// @@ -24,7 +18,7 @@ public int Create(BikeModel entity) { if (entity.Id == 0) { - var lastId = _context.BikeModels + var lastId = context.BikeModels .OrderByDescending(b => b.Id) .Select(b => b.Id) .FirstOrDefault(); @@ -32,8 +26,8 @@ public int Create(BikeModel entity) entity.Id = lastId + 1; } - _context.BikeModels.Add(entity); - _context.SaveChanges(); + context.BikeModels.Add(entity); + context.SaveChanges(); return entity.Id; } @@ -43,7 +37,7 @@ public int Create(BikeModel entity) /// List of existing objects public List ReadAll() { - return _context.BikeModels.ToList(); + return [.. context.BikeModels]; } /// @@ -53,7 +47,7 @@ public List ReadAll() /// Object if exist public BikeModel? Read(int id) { - return _context.BikeModels.Find(id); + return context.BikeModels.Find(id); } /// @@ -64,14 +58,14 @@ public List ReadAll() /// Object if exist public BikeModel? Update(int id, BikeModel entity) { - var existingModel = _context.BikeModels.Find(id); + var existingModel = context.BikeModels.Find(id); if (existingModel == null) return null; - _context.Entry(existingModel).CurrentValues.SetValues(entity); + context.Entry(existingModel).CurrentValues.SetValues(entity); existingModel.Id = id; - _context.SaveChanges(); + context.SaveChanges(); return existingModel; } @@ -82,11 +76,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var model = _context.BikeModels.Find(id); + var model = context.BikeModels.Find(id); if (model == null) return false; - _context.BikeModels.Remove(model); - _context.SaveChanges(); + context.BikeModels.Remove(model); + context.SaveChanges(); return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs index c9549f811..babbc8847 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoBikeRepository.cs @@ -6,15 +6,9 @@ namespace Bikes.Infrastructure.MongoDb.Repositories; /// /// A repository for working with bikes in MongoDB /// -public class MongoBikeRepository : IRepository +public class MongoBikeRepository( + BikesDbContext context) : IRepository { - private readonly BikesDbContext _context; - - public MongoBikeRepository(BikesDbContext context) - { - _context = context; - } - /// /// Creates a new object /// @@ -24,7 +18,7 @@ public int Create(Bike entity) { if (entity.Id == 0) { - var lastId = _context.Bikes + var lastId = context.Bikes .OrderByDescending(b => b.Id) .Select(b => b.Id) .FirstOrDefault(); @@ -32,8 +26,8 @@ public int Create(Bike entity) entity.Id = lastId + 1; } - _context.Bikes.Add(entity); - _context.SaveChanges(); + context.Bikes.Add(entity); + context.SaveChanges(); return entity.Id; } @@ -43,7 +37,7 @@ public int Create(Bike entity) /// List of existing objects public List ReadAll() { - return _context.Bikes.ToList(); + return [.. context.Bikes]; } /// @@ -53,7 +47,7 @@ public List ReadAll() /// Object if exist public Bike? Read(int id) { - return _context.Bikes.FirstOrDefault(b => b.Id == id); + return context.Bikes.FirstOrDefault(b => b.Id == id); } /// @@ -64,14 +58,14 @@ public List ReadAll() /// Object if exist public Bike? Update(int id, Bike entity) { - var existingBike = _context.Bikes.FirstOrDefault(b => b.Id == id); + var existingBike = context.Bikes.FirstOrDefault(b => b.Id == id); if (existingBike == null) return null; existingBike.SerialNumber = entity.SerialNumber; existingBike.Color = entity.Color; existingBike.ModelId = entity.ModelId; - _context.SaveChanges(); + context.SaveChanges(); return existingBike; } @@ -82,11 +76,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var bike = _context.Bikes.Find(id); + var bike = context.Bikes.Find(id); if (bike == null) return false; - _context.Bikes.Remove(bike); - _context.SaveChanges(); + context.Bikes.Remove(bike); + context.SaveChanges(); return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs index 230633e86..e0c711cc9 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRentRepository.cs @@ -6,15 +6,9 @@ namespace Bikes.Infrastructure.MongoDb.Repositories; /// /// A repository for working with rents in MongoDB /// -public class MongoRentRepository : IRepository +public class MongoRentRepository( + BikesDbContext context) : IRepository { - private readonly BikesDbContext _context; - - public MongoRentRepository(BikesDbContext context) - { - _context = context; - } - /// /// Creates a new object /// @@ -24,7 +18,7 @@ public int Create(Rent entity) { if (entity.Id == 0) { - var lastId = _context.Rents + var lastId = context.Rents .OrderByDescending(b => b.Id) .Select(b => b.Id) .FirstOrDefault(); @@ -32,8 +26,8 @@ public int Create(Rent entity) entity.Id = lastId + 1; } - _context.Rents.Add(entity); - _context.SaveChanges(); + context.Rents.Add(entity); + context.SaveChanges(); return entity.Id; } @@ -43,7 +37,7 @@ public int Create(Rent entity) /// List of existing objects public List ReadAll() { - return _context.Rents.ToList(); + return [.. context.Rents]; } /// @@ -53,7 +47,7 @@ public List ReadAll() /// Object if exist public Rent? Read(int id) { - return _context.Rents.FirstOrDefault(r => r.Id == id); + return context.Rents.FirstOrDefault(r => r.Id == id); } /// @@ -64,7 +58,7 @@ public List ReadAll() /// Object if exist public Rent? Update(int id, Rent entity) { - var existingRent = _context.Rents.FirstOrDefault(r => r.Id == id); + var existingRent = context.Rents.FirstOrDefault(r => r.Id == id); if (existingRent == null) return null; existingRent.RentalStartTime = entity.RentalStartTime; @@ -72,7 +66,7 @@ public List ReadAll() existingRent.BikeId = entity.BikeId; existingRent.RenterId = entity.RenterId; - _context.SaveChanges(); + context.SaveChanges(); return existingRent; } @@ -83,11 +77,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var rent = _context.Rents.Find(id); + var rent = context.Rents.Find(id); if (rent == null) return false; - _context.Rents.Remove(rent); - _context.SaveChanges(); + context.Rents.Remove(rent); + context.SaveChanges(); return true; } } \ No newline at end of file diff --git a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs index 7c5868a95..eef8a4860 100644 --- a/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs +++ b/Bikes/Bikes.Infrastructure.MongoDb/Repositories/MongoRenterRepository.cs @@ -6,15 +6,9 @@ namespace Bikes.Infrastructure.MongoDb.Repositories; /// /// A repository for working with renters in MongoDB /// -public class MongoRenterRepository : IRepository +public class MongoRenterRepository( + BikesDbContext context) : IRepository { - private readonly BikesDbContext _context; - - public MongoRenterRepository(BikesDbContext context) - { - _context = context; - } - /// /// Creates a new object /// @@ -24,7 +18,7 @@ public int Create(Renter entity) { if (entity.Id == 0) { - var lastId = _context.Renters + var lastId = context.Renters .OrderByDescending(b => b.Id) .Select(b => b.Id) .FirstOrDefault(); @@ -32,8 +26,8 @@ public int Create(Renter entity) entity.Id = lastId + 1; } - _context.Renters.Add(entity); - _context.SaveChanges(); + context.Renters.Add(entity); + context.SaveChanges(); return entity.Id; } @@ -43,7 +37,7 @@ public int Create(Renter entity) /// List of existing objects public List ReadAll() { - return _context.Renters.ToList(); + return [.. context.Renters]; } /// @@ -53,7 +47,7 @@ public List ReadAll() /// Object if exist public Renter? Read(int id) { - return _context.Renters.Find(id); + return context.Renters.Find(id); } /// @@ -64,13 +58,13 @@ public List ReadAll() /// Object if exist public Renter? Update(int id, Renter entity) { - var existingRenter = _context.Renters.Find(id); + var existingRenter = context.Renters.Find(id); if (existingRenter == null) return null; - _context.Entry(existingRenter).CurrentValues.SetValues(entity); + context.Entry(existingRenter).CurrentValues.SetValues(entity); existingRenter.Id = id; - _context.SaveChanges(); + context.SaveChanges(); return existingRenter; } @@ -81,11 +75,11 @@ public List ReadAll() /// True or false? result of deleting public bool Delete(int id) { - var renter = _context.Renters.Find(id); + var renter = context.Renters.Find(id); if (renter == null) return false; - _context.Renters.Remove(renter); - _context.SaveChanges(); + context.Renters.Remove(renter); + context.SaveChanges(); return true; } } \ No newline at end of file From c8f1079994662504c3a2051fd3d81d8968f5cedb Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 21 Dec 2025 18:21:14 +0400 Subject: [PATCH 35/41] Add Bikes.Generator project and add kafka in AppHost --- Bikes/Bikes.AppHost/Bikes.AppHost.csproj | 1 + Bikes/Bikes.AppHost/Program.cs | 9 ++++++-- Bikes/Bikes.Generator/Bikes.Generator.csproj | 23 ++++++++++++++++++++ Bikes/Bikes.Generator/Program.cs | 2 ++ Bikes/Bikes.sln | 6 +++++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 Bikes/Bikes.Generator/Bikes.Generator.csproj create mode 100644 Bikes/Bikes.Generator/Program.cs diff --git a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj index ece33e39a..0c5ef5fbc 100644 --- a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj +++ b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj @@ -13,6 +13,7 @@ + diff --git a/Bikes/Bikes.AppHost/Program.cs b/Bikes/Bikes.AppHost/Program.cs index 0e19116cc..5484b2692 100644 --- a/Bikes/Bikes.AppHost/Program.cs +++ b/Bikes/Bikes.AppHost/Program.cs @@ -1,9 +1,14 @@ var builder = DistributedApplication.CreateBuilder(args); +var kafka = builder.AddKafka("kafka") + .WithKafkaUI() + .WithDataVolume(); + var mongodb = builder.AddMongoDB("mongodb") .WithDataVolume(); -_ = builder.AddProject("bikes-api") - .WithReference(mongodb); +var api = builder.AddProject("bikes-api") + .WithReference(mongodb) + .WithReference(kafka); builder.Build().Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Bikes.Generator.csproj b/Bikes/Bikes.Generator/Bikes.Generator.csproj new file mode 100644 index 000000000..239b4ef6f --- /dev/null +++ b/Bikes/Bikes.Generator/Bikes.Generator.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Program.cs b/Bikes/Bikes.Generator/Program.cs new file mode 100644 index 000000000..3751555cb --- /dev/null +++ b/Bikes/Bikes.Generator/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/Bikes/Bikes.sln b/Bikes/Bikes.sln index 858657d23..8f2e3d1ba 100644 --- a/Bikes/Bikes.sln +++ b/Bikes/Bikes.sln @@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.ServiceDefaults", "Bi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.AppHost", "Bikes.AppHost\Bikes.AppHost.csproj", "{C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Generator", "Bikes.Generator\Bikes.Generator.csproj", "{C6E99932-52ED-4DC1-B308-991E13074111}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,10 @@ Global {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5C91635-69D6-41B4-8A1C-67D6A6AADE1D}.Release|Any CPU.Build.0 = Release|Any CPU + {C6E99932-52ED-4DC1-B308-991E13074111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6E99932-52ED-4DC1-B308-991E13074111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6E99932-52ED-4DC1-B308-991E13074111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6E99932-52ED-4DC1-B308-991E13074111}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 694af2a581ab8f980e907da2c933e67e54e6a67f Mon Sep 17 00:00:00 2001 From: comandir26 Date: Sun, 21 Dec 2025 21:14:07 +0400 Subject: [PATCH 36/41] Success build Bikes.Generator --- Bikes/Bikes.Generator/Bikes.Generator.csproj | 6 + Bikes/Bikes.Generator/ContractGenerator.cs | 77 +++++++++++ .../Bikes.Generator/IKafkaProducerFactory.cs | 8 ++ .../KafkaProducerFactory.cs.cs | 76 +++++++++++ Bikes/Bikes.Generator/KafkaProducerService.cs | 129 ++++++++++++++++++ .../Options/GeneratorOptions.cs | 9 ++ Bikes/Bikes.Generator/Options/KafkaOptions.cs | 9 ++ Bikes/Bikes.Generator/Program.cs | 38 +++++- Bikes/Bikes.Generator/appsettings.json | 21 +++ 9 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 Bikes/Bikes.Generator/ContractGenerator.cs create mode 100644 Bikes/Bikes.Generator/IKafkaProducerFactory.cs create mode 100644 Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs create mode 100644 Bikes/Bikes.Generator/KafkaProducerService.cs create mode 100644 Bikes/Bikes.Generator/Options/GeneratorOptions.cs create mode 100644 Bikes/Bikes.Generator/Options/KafkaOptions.cs create mode 100644 Bikes/Bikes.Generator/appsettings.json diff --git a/Bikes/Bikes.Generator/Bikes.Generator.csproj b/Bikes/Bikes.Generator/Bikes.Generator.csproj index 239b4ef6f..4cb45f28f 100644 --- a/Bikes/Bikes.Generator/Bikes.Generator.csproj +++ b/Bikes/Bikes.Generator/Bikes.Generator.csproj @@ -20,4 +20,10 @@ + + + PreserveNewest + + + \ No newline at end of file diff --git a/Bikes/Bikes.Generator/ContractGenerator.cs b/Bikes/Bikes.Generator/ContractGenerator.cs new file mode 100644 index 000000000..b81a73727 --- /dev/null +++ b/Bikes/Bikes.Generator/ContractGenerator.cs @@ -0,0 +1,77 @@ +using Bikes.Contracts.Dto; +using Bikes.Domain.Models; +using Bogus; + +namespace Bikes.Generator; + +public class ContractGenerator +{ + private readonly Faker _bikeFaker; + private readonly Faker _bikeModelFaker; + private readonly Faker _renterFaker; + private readonly Faker _rentFaker; + + public ContractGenerator() + { + _bikeFaker = new Faker() + .CustomInstantiator(f => new BikeCreateUpdateDto + { + SerialNumber = $"BIKE{f.Random.Int(1000, 9999)}", + Color = f.PickRandom("Красный", "Синий", "Зеленый", "Черный", "Белый"), + ModelId = f.Random.Int(1, 10) + }); + + _bikeModelFaker = new Faker() + .CustomInstantiator(f => new BikeModelCreateUpdateDto + { + Type = f.PickRandom(), + WheelSize = f.Random.Int(20, 29), + MaxPassengerWeight = f.Random.Int(70, 120), + Weight = f.Random.Int(10, 20), + BrakeType = f.PickRandom("Дисковые гидравлические", "Ободные v-brake", "Дисковые механические"), + Year = f.Random.Int(2020, 2024), + RentPrice = f.Random.Int(300, 1000) + }); + + _renterFaker = new Faker() + .CustomInstantiator(f => new RenterCreateUpdateDto + { + FullName = f.Name.FullName(), + Number = $"+7 ({f.Random.Int(900, 999)}) {f.Random.Int(100, 999)}-{f.Random.Int(10, 99)}-{f.Random.Int(10, 99)}" + }); + + _rentFaker = new Faker() + .CustomInstantiator(f => new RentCreateUpdateDto + { + RentalStartTime = f.Date.Soon(1), + RentalDuration = f.Random.Int(1, 24), + RenterId = f.Random.Int(1, 10), + BikeId = f.Random.Int(1, 10) + }); + } + + public BikeCreateUpdateDto GenerateBike() => _bikeFaker.Generate(); + public BikeModelCreateUpdateDto GenerateBikeModel() => _bikeModelFaker.Generate(); + public RenterCreateUpdateDto GenerateRenter() => _renterFaker.Generate(); + public RentCreateUpdateDto GenerateRent() => _rentFaker.Generate(); + + public List GenerateBatch(int size) + { + var batch = new List(); + + for (var i = 0; i < size; i++) + { + var entityType = new Random().Next(0, 4); + batch.Add(entityType switch + { + 0 => GenerateBike(), + 1 => GenerateBikeModel(), + 2 => GenerateRenter(), + 3 => GenerateRent(), + _ => GenerateBike() + }); + } + + return batch; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/IKafkaProducerFactory.cs b/Bikes/Bikes.Generator/IKafkaProducerFactory.cs new file mode 100644 index 000000000..31e10294c --- /dev/null +++ b/Bikes/Bikes.Generator/IKafkaProducerFactory.cs @@ -0,0 +1,8 @@ +using Confluent.Kafka; + +namespace Bikes.Generator; + +public interface IKafkaProducerFactory +{ + public IProducer CreateProducer(); +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs b/Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs new file mode 100644 index 000000000..7cb52b668 --- /dev/null +++ b/Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs @@ -0,0 +1,76 @@ +using Bikes.Generator.Options; +using Confluent.Kafka; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Bikes.Generator; + +public class KafkaProducerFactory : IKafkaProducerFactory, IDisposable +{ + private readonly KafkaOptions _options; + private readonly ILogger _logger; + private IProducer? _producer; + + public KafkaProducerFactory( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + public IProducer CreateProducer() + { + if (_producer != null) + return _producer; + + var config = new ProducerConfig + { + BootstrapServers = _options.BootstrapServers, + MessageTimeoutMs = _options.MessageTimeoutMs, + EnableDeliveryReports = true + }; + + var retryCount = 0; + while (retryCount < _options.MaxRetryAttempts) + { + try + { + _producer = new ProducerBuilder(config) + .SetLogHandler((_, message) => + _logger.LogInformation("Kafka: {Facility} - {Message}", message.Facility, message.Message)) + .SetErrorHandler((_, error) => + _logger.LogError("Kafka Error: {Reason} (Code: {Code})", error.Reason, error.Code)) + .Build(); + + _logger.LogInformation("Kafka producer connected successfully to {BootstrapServers}", + _options.BootstrapServers); + + return _producer; + } + catch (Exception ex) + { + retryCount++; + _logger.LogWarning(ex, + "Failed to connect to Kafka (attempt {RetryCount}/{MaxRetries}). Retrying in {DelayMs}ms...", + retryCount, _options.MaxRetryAttempts, _options.RetryDelayMs); + + if (retryCount >= _options.MaxRetryAttempts) + { + _logger.LogError(ex, "Max retry attempts reached. Failed to connect to Kafka."); + throw; + } + + Thread.Sleep(_options.RetryDelayMs); + } + } + + throw new InvalidOperationException("Failed to create Kafka producer"); + } + + public void Dispose() + { + _producer?.Dispose(); + _producer = null; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/KafkaProducerService.cs b/Bikes/Bikes.Generator/KafkaProducerService.cs new file mode 100644 index 000000000..002304afb --- /dev/null +++ b/Bikes/Bikes.Generator/KafkaProducerService.cs @@ -0,0 +1,129 @@ +using Bikes.Generator.Options; +using Confluent.Kafka; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace Bikes.Generator; + +public class KafkaProducerService : BackgroundService +{ + private readonly GeneratorOptions _generatorOptions; + private readonly ContractGenerator _contractGenerator; + private readonly IKafkaProducerFactory _producerFactory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public KafkaProducerService( + IOptions generatorOptions, + ContractGenerator contractGenerator, + IKafkaProducerFactory producerFactory, + ILogger logger) + { + _generatorOptions = generatorOptions.Value; + _contractGenerator = contractGenerator; + _producerFactory = producerFactory; + _logger = logger; + + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Starting Kafka Producer Service"); + _logger.LogInformation("Configuration: Interval={IntervalMs}ms, BatchSize={BatchSize}, Topic={Topic}", + _generatorOptions.IntervalMs, _generatorOptions.BatchSize, _generatorOptions.Topic); + + if (!_generatorOptions.Enabled) + { + _logger.LogInformation("Generator is disabled. Service will not produce messages."); + return; + } + + var producer = _producerFactory.CreateProducer(); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var batch = _contractGenerator.GenerateBatch(_generatorOptions.BatchSize); + + foreach (var contract in batch) + { + var message = CreateKafkaMessage(contract); + if (message != null) + { + await ProduceMessageAsync(producer, message, stoppingToken); + } + } + + _logger.LogDebug("Generated and sent batch of {Count} messages", batch.Count); + + await Task.Delay(_generatorOptions.IntervalMs, stoppingToken); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Producer service is stopping..."); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in producer service"); + await Task.Delay(5000, stoppingToken); + } + } + + producer.Flush(stoppingToken); + _logger.LogInformation("Kafka Producer Service stopped"); + } + + private Message? CreateKafkaMessage(object contract) + { + try + { + var json = JsonSerializer.Serialize(contract, contract.GetType(), _jsonOptions); + var message = new Message { Value = json }; + + // Добавляем метаданные в headers для определения типа контракта + message.Headers = new Headers + { + new Header("contract-type", System.Text.Encoding.UTF8.GetBytes(contract.GetType().Name)) + }; + + return message; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to serialize contract: {ContractType}", contract.GetType().Name); + return null; + } + } + + private async Task ProduceMessageAsync( + IProducer producer, + Message message, + CancellationToken cancellationToken) + { + try + { + var deliveryResult = await producer.ProduceAsync( + _generatorOptions.Topic, + message, + cancellationToken); + + _logger.LogDebug("Message delivered to {Topic} [{Partition}] @ {Offset}", + deliveryResult.Topic, + deliveryResult.Partition, + deliveryResult.Offset); + } + catch (ProduceException ex) + { + _logger.LogError(ex, "Failed to deliver message: {Error}", ex.Error.Reason); + } + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Options/GeneratorOptions.cs b/Bikes/Bikes.Generator/Options/GeneratorOptions.cs new file mode 100644 index 000000000..756d26bf7 --- /dev/null +++ b/Bikes/Bikes.Generator/Options/GeneratorOptions.cs @@ -0,0 +1,9 @@ +namespace Bikes.Generator.Options; + +public class GeneratorOptions +{ + public int IntervalMs { get; set; } = 5000; + public int BatchSize { get; set; } = 1; + public string Topic { get; set; } = "bikes-contracts"; + public bool Enabled { get; set; } = true; +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Options/KafkaOptions.cs b/Bikes/Bikes.Generator/Options/KafkaOptions.cs new file mode 100644 index 000000000..abf8c88d4 --- /dev/null +++ b/Bikes/Bikes.Generator/Options/KafkaOptions.cs @@ -0,0 +1,9 @@ +namespace Bikes.Generator.Options; + +public class KafkaOptions +{ + public string BootstrapServers { get; set; } = "localhost:9092"; + public int MaxRetryAttempts { get; set; } = 3; + public int RetryDelayMs { get; set; } = 1000; + public int MessageTimeoutMs { get; set; } = 5000; +} \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Program.cs b/Bikes/Bikes.Generator/Program.cs index 3751555cb..0e64a1b23 100644 --- a/Bikes/Bikes.Generator/Program.cs +++ b/Bikes/Bikes.Generator/Program.cs @@ -1,2 +1,36 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +// Удалите или закомментируйте эту строку: +// using Bikes.Generator; + +// Добавьте это в начало файла: +using Bikes.Generator; +using Bikes.Generator.Options; // Если создадите папку Options +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Регистрируем конфигурацию + services.Configure( + context.Configuration.GetSection("Generator")); + services.Configure( + context.Configuration.GetSection("Kafka")); + + // Регистрируем сервисы + services.AddSingleton(); + services.AddSingleton(); + + // Регистрируем BackgroundService + services.AddHostedService(); + + // Настраиваем логирование + services.AddLogging(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Information); + }); + }) + .Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/Bikes/Bikes.Generator/appsettings.json b/Bikes/Bikes.Generator/appsettings.json new file mode 100644 index 000000000..687c3e129 --- /dev/null +++ b/Bikes/Bikes.Generator/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Bikes.Generator": "Debug" + } + }, + "Generator": { + "IntervalMs": 3000, + "BatchSize": 2, + "Topic": "bikes-contracts", + "Enabled": true + }, + "Kafka": { + "BootstrapServers": "localhost:9092", + "MaxRetryAttempts": 5, + "RetryDelayMs": 1000, + "MessageTimeoutMs": 5000 + } +} \ No newline at end of file From 948b081870969eb7868f229c7747fec76b311ecc Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 24 Dec 2025 21:36:52 +0400 Subject: [PATCH 37/41] The generator and kafka are working correctly, the data is successfully added to the database. --- Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj | 1 + Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs | 262 ++++++++++++++++++ .../Kafka/KafkaConsumerExtensions.cs | 110 ++++++++ .../Kafka/KafkaConsumerOptions.cs | 67 +++++ Bikes/Bikes.Api.Host/Program.cs | 17 ++ Bikes/Bikes.Api.Host/appsettings.json | 18 +- Bikes/Bikes.AppHost/Bikes.AppHost.csproj | 3 + Bikes/Bikes.AppHost/Program.cs | 9 +- Bikes/Bikes.Generator/ContractGenerator.cs | 30 ++ .../Bikes.Generator/IKafkaProducerFactory.cs | 7 + ...rFactory.cs.cs => KafkaProducerFactory.cs} | 52 +++- Bikes/Bikes.Generator/KafkaProducerService.cs | 107 ++++++- .../Options/GeneratorOptions.cs | 18 ++ Bikes/Bikes.Generator/Options/KafkaOptions.cs | 18 ++ Bikes/Bikes.Generator/Program.cs | 15 +- Bikes/Bikes.Generator/appsettings.json | 11 +- 16 files changed, 720 insertions(+), 25 deletions(-) create mode 100644 Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs create mode 100644 Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs create mode 100644 Bikes/Bikes.Api.Host/Kafka/KafkaConsumerOptions.cs rename Bikes/Bikes.Generator/{KafkaProducerFactory.cs.cs => KafkaProducerFactory.cs} (57%) diff --git a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj index 9abe8e8ad..4e02a2e6c 100644 --- a/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -9,6 +9,7 @@ + diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs new file mode 100644 index 000000000..10af3ddf3 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs @@ -0,0 +1,262 @@ +using Bikes.Application.Interfaces; +using Bikes.Contracts.Dto; +using Confluent.Kafka; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace Bikes.Api.Host.Kafka; + +/// +/// Background service for consuming Kafka messages and processing contract DTOs +/// +public class KafkaConsumer : BackgroundService +{ + private readonly IConsumer _consumer; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly KafkaConsumerOptions _options; + private readonly JsonSerializerOptions _jsonOptions; + + /// + /// Initializes the Kafka consumer with configuration and dependencies + /// + public KafkaConsumer( + IConsumer consumer, + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _consumer = consumer; + _scopeFactory = scopeFactory; + _logger = logger; + _options = options.Value; + + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + /// + /// Main execution method that consumes and processes Kafka messages + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Waiting for application to fully start (20 seconds)..."); + await Task.Delay(20000, stoppingToken); + + _logger.LogInformation("Starting KafkaConsumer..."); + + try + { + _consumer.Subscribe(_options.Topic); + _logger.LogInformation("KafkaConsumer subscribed to topic: {Topic}", _options.Topic); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to subscribe to Kafka topic"); + return; + } + + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var consumeResult = _consumer.Consume(TimeSpan.FromSeconds(5)); + + if (consumeResult == null) + { + continue; + } + + if (string.IsNullOrEmpty(consumeResult.Message?.Value)) + { + _logger.LogWarning("Received empty message"); + continue; + } + + _logger.LogDebug("Processing message at offset {Offset}", + consumeResult.TopicPartitionOffset); + + var contractType = DetermineContractType(consumeResult.Message.Headers); + await ProcessMessageAsync(consumeResult.Message.Value, contractType, stoppingToken); + + _consumer.Commit(consumeResult); + } + catch (ConsumeException ex) + { + _logger.LogError(ex, "Kafka consumption error: {Error}. Waiting 10 seconds...", ex.Error.Reason); + await Task.Delay(10000, stoppingToken); + } + catch (OperationCanceledException) + { + _logger.LogInformation("KafkaConsumer is stopping..."); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in KafkaConsumer. Waiting 15 seconds..."); + await Task.Delay(15000, stoppingToken); + } + } + + _consumer.Close(); + _logger.LogInformation("KafkaConsumer stopped"); + } + + /// + /// Determines the contract type from Kafka message headers + /// + /// Kafka message headers + /// Contract type name or null if not found + private string? DetermineContractType(Headers headers) + { + if (headers == null) return null; + + var contractTypeHeader = headers.FirstOrDefault(h => h.Key == "contract-type"); + if (contractTypeHeader != null) + { + return System.Text.Encoding.UTF8.GetString(contractTypeHeader.GetValueBytes()); + } + + return null; + } + + /// + /// Processes a Kafka message based on its contract type + /// + /// JSON message content + /// Type of contract to process + private async Task ProcessMessageAsync(string messageJson, string? contractType, CancellationToken cancellationToken) + { + using var scope = _scopeFactory.CreateScope(); + + try + { + switch (contractType) + { + case "BikeCreateUpdateDto": + var bikeDto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (bikeDto != null) + { + var bikeService = scope.ServiceProvider.GetRequiredService(); + var result = bikeService.CreateBike(bikeDto); + _logger.LogInformation("Created bike with ID: {BikeId}", result?.Id); + } + break; + + case "BikeModelCreateUpdateDto": + var bikeModelDto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (bikeModelDto != null) + { + var bikeModelService = scope.ServiceProvider.GetRequiredService(); + var result = bikeModelService.CreateBikeModel(bikeModelDto); + _logger.LogInformation("Created bike model with ID: {ModelId}", result?.Id); + } + break; + + case "RenterCreateUpdateDto": + var renterDto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (renterDto != null) + { + var renterService = scope.ServiceProvider.GetRequiredService(); + var result = renterService.CreateRenter(renterDto); + _logger.LogInformation("Created renter with ID: {RenterId}", result?.Id); + } + break; + + case "RentCreateUpdateDto": + var rentDto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (rentDto != null) + { + var rentService = scope.ServiceProvider.GetRequiredService(); + var result = rentService.CreateRent(rentDto); + _logger.LogInformation("Created rent with ID: {RentId}", result?.Id); + } + break; + + default: + // Попробуем определить тип по структуре JSON + await TryAutoDetectAndProcessAsync(messageJson, scope, cancellationToken); + break; + } + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize message: {Message}", messageJson); + } + catch (ArgumentException ex) + { + _logger.LogWarning("Validation error: {ErrorMessage}", ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing message"); + } + } + + /// + /// Attempts to auto-detect contract type from JSON structure and process accordingly + /// + private async Task TryAutoDetectAndProcessAsync(string messageJson, IServiceScope scope, CancellationToken cancellationToken) + { + try + { + if (messageJson.Contains("SerialNumber") && messageJson.Contains("Color")) + { + var dto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (dto != null) + { + var service = scope.ServiceProvider.GetRequiredService(); + service.CreateBike(dto); + } + } + else if (messageJson.Contains("Type") && messageJson.Contains("WheelSize")) + { + var dto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (dto != null) + { + var service = scope.ServiceProvider.GetRequiredService(); + service.CreateBikeModel(dto); + } + } + else if (messageJson.Contains("FullName") && messageJson.Contains("Number")) + { + var dto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (dto != null) + { + var service = scope.ServiceProvider.GetRequiredService(); + service.CreateRenter(dto); + } + } + else if (messageJson.Contains("RentalStartTime") && messageJson.Contains("RentalDuration")) + { + var dto = JsonSerializer.Deserialize(messageJson, _jsonOptions); + if (dto != null) + { + var service = scope.ServiceProvider.GetRequiredService(); + service.CreateRent(dto); + } + } + else + { + _logger.LogWarning("Could not determine contract type for message: {Message}", messageJson); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to auto-detect and process message"); + } + } + + /// + /// Disposes the Kafka consumer instance + /// + public override void Dispose() + { + _consumer?.Dispose(); + base.Dispose(); + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs new file mode 100644 index 000000000..16ce58ce3 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs @@ -0,0 +1,110 @@ +using Confluent.Kafka; +using Microsoft.Extensions.Options; + +namespace Bikes.Api.Host.Kafka; + +/// +/// Provides extension methods for configuring Kafka consumer services +/// +public static class KafkaConsumerExtensions +{ + /// + /// Adds and configures Kafka consumer services to the service collection + /// + /// The service collection to configure + /// The configured service collection + public static IServiceCollection AddKafkaConsumer(this IServiceCollection services) + { + services.AddOptions() + .Configure((options, configuration) => + { + configuration.GetSection("Kafka").Bind(options); + + var aspireKafkaConnection = configuration.GetConnectionString("kafka"); + if (!string.IsNullOrEmpty(aspireKafkaConnection)) + { + Console.WriteLine($"Using Kafka connection from Aspire: {aspireKafkaConnection}"); + options.BootstrapServers = aspireKafkaConnection; + } + else + { + Console.WriteLine($"Using Kafka from appsettings: {options.BootstrapServers}"); + } + + Console.WriteLine($"Final Kafka BootstrapServers: {options.BootstrapServers}"); + }); + + services.AddSingleton>(provider => + { + var options = provider.GetRequiredService>().Value; + var logger = provider.GetRequiredService>(); + + Console.WriteLine($"Creating Kafka consumer for: {options.BootstrapServers}"); + + var config = new ConsumerConfig + { + BootstrapServers = options.BootstrapServers, + GroupId = options.GroupId, + EnableAutoCommit = options.EnableAutoCommit, + AutoOffsetReset = (AutoOffsetReset)options.AutoOffsetReset, + + ApiVersionRequest = false, + BrokerVersionFallback = "0.10.0.0", + + ApiVersionFallbackMs = 0, + + SecurityProtocol = SecurityProtocol.Plaintext, + SslEndpointIdentificationAlgorithm = SslEndpointIdentificationAlgorithm.None, + SocketTimeoutMs = 30000, + SessionTimeoutMs = 30000, + MetadataMaxAgeMs = 300000, + AllowAutoCreateTopics = false, + EnablePartitionEof = true, + EnableSslCertificateVerification = false, + Debug = "broker,protocol" + }; + + var retryCount = 0; + while (retryCount < options.MaxRetryAttempts) + { + try + { + var consumer = new ConsumerBuilder(config) + .SetErrorHandler((_, error) => + { + if (error.IsFatal) + logger.LogError("Kafka Fatal Error: {Reason} (Code: {Code})", error.Reason, error.Code); + else + logger.LogWarning("⚠Kafka Warning: {Reason} (Code: {Code})", error.Reason, error.Code); + }) + .SetLogHandler((_, logMessage) => + logger.LogDebug("Kafka log: {Facility} - {Message}", logMessage.Facility, logMessage.Message)) + .Build(); + + logger.LogInformation("Kafka consumer created successfully!"); + return consumer; + } + catch (Exception ex) + { + retryCount++; + logger.LogWarning(ex, + "Failed to create Kafka consumer (attempt {RetryCount}/{MaxRetries})", + retryCount, options.MaxRetryAttempts); + + if (retryCount >= options.MaxRetryAttempts) + { + logger.LogError(ex, "Max retry attempts reached for Kafka consumer"); + throw; + } + + Thread.Sleep(options.RetryDelayMs); + } + } + + throw new InvalidOperationException("Failed to create Kafka consumer"); + }); + + services.AddHostedService(); + return services; + } +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerOptions.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerOptions.cs new file mode 100644 index 000000000..801519755 --- /dev/null +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerOptions.cs @@ -0,0 +1,67 @@ +namespace Bikes.Api.Host.Kafka; + +/// +/// Configuration options for Kafka consumer +/// +public class KafkaConsumerOptions +{ + /// + /// Gets or sets the Kafka bootstrap servers address + /// + public string BootstrapServers { get; set; } = "localhost:9092"; + + /// + /// Gets or sets the topic name to consume from + /// + public string Topic { get; set; } = "bikes-contracts"; + + /// + /// Gets or sets the consumer group identifier + /// + public string GroupId { get; set; } = "bikes-api-consumer-group"; + + /// + /// Gets or sets a value indicating whether to enable automatic offset committing + /// + public bool EnableAutoCommit { get; set; } = false; + + /// + /// Gets or sets the auto offset reset behavior (0: earliest, 1: latest, 2: error) + /// + public int AutoOffsetReset { get; set; } = 0; + + /// + /// Gets or sets the maximum number of retry attempts for connection + /// + public int MaxRetryAttempts { get; set; } = 3; + + /// + /// Gets or sets the delay between retry attempts in milliseconds + /// + public int RetryDelayMs { get; set; } = 1000; + + /// + /// Gets or sets a value indicating whether to request API version from broker + /// + public bool ApiVersionRequest { get; set; } = false; + + /// + /// Gets or sets the API version fallback timeout in milliseconds + /// + public int ApiVersionFallbackMs { get; set; } = 0; + + /// + /// Gets or sets the broker version fallback string + /// + public string BrokerVersionFallback { get; set; } = "0.10.0.0"; + + /// + /// Gets or sets the security protocol for Kafka connection + /// + public string SecurityProtocol { get; set; } = "Plaintext"; + + /// + /// Gets or sets a value indicating whether to allow automatic topic creation + /// + public bool AllowAutoCreateTopics { get; set; } = false; +} \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Program.cs b/Bikes/Bikes.Api.Host/Program.cs index 8ade9cc11..e0172c75a 100644 --- a/Bikes/Bikes.Api.Host/Program.cs +++ b/Bikes/Bikes.Api.Host/Program.cs @@ -1,3 +1,4 @@ +using Bikes.Api.Host.Kafka; using Bikes.Application.Extensions; using Bikes.Infrastructure.MongoDb; using Bikes.Infrastructure.MongoDb.Extensions; @@ -13,9 +14,17 @@ builder.Services.AddMongoDbInfrastructure(builder.Configuration); builder.Services.AddBikeRentalServices(); +builder.Services.AddKafkaConsumer(); var app = builder.Build(); +app.Lifetime.ApplicationStarted.Register(() => +{ + var logger = app.Services.GetRequiredService>(); + logger.LogInformation("Waiting 5 seconds for services to stabilize..."); + Thread.Sleep(5000); +}); + app.MapDefaultEndpoints(); using (var scope = app.Services.CreateScope()) @@ -46,4 +55,12 @@ app.MapControllers(); +app.Lifetime.ApplicationStarted.Register(async () => +{ + var logger = app.Services.GetRequiredService>(); + logger.LogInformation("Application fully started. Waiting 3 seconds before accepting requests..."); + await Task.Delay(3000); + logger.LogInformation("Application ready to accept requests."); +}); + app.Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/appsettings.json b/Bikes/Bikes.Api.Host/appsettings.json index de382b4ed..266b026e2 100644 --- a/Bikes/Bikes.Api.Host/appsettings.json +++ b/Bikes/Bikes.Api.Host/appsettings.json @@ -5,9 +5,23 @@ "Microsoft.AspNetCore": "Warning" } }, + "Kafka": { + "Topic": "bikes-contracts", + "GroupId": "bikes-api-consumer-group", + "EnableAutoCommit": false, + "AutoOffsetReset": 0, + "MaxRetryAttempts": 10, + "RetryDelayMs": 2000, + + "ApiVersionRequest": false, + "ApiVersionFallbackMs": 0, + "BrokerVersionFallback": "0.10.0.0", + "SecurityProtocol": "Plaintext", + "AllowAutoCreateTopics": false + }, "MongoDb": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "BikesDb_v2" }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj index 0c5ef5fbc..61d6bb71d 100644 --- a/Bikes/Bikes.AppHost/Bikes.AppHost.csproj +++ b/Bikes/Bikes.AppHost/Bikes.AppHost.csproj @@ -20,6 +20,9 @@ + diff --git a/Bikes/Bikes.AppHost/Program.cs b/Bikes/Bikes.AppHost/Program.cs index 5484b2692..36ffc3085 100644 --- a/Bikes/Bikes.AppHost/Program.cs +++ b/Bikes/Bikes.AppHost/Program.cs @@ -1,14 +1,17 @@ var builder = DistributedApplication.CreateBuilder(args); var kafka = builder.AddKafka("kafka") - .WithKafkaUI() - .WithDataVolume(); + .WithKafkaUI() + .WithDataVolume(); var mongodb = builder.AddMongoDB("mongodb") .WithDataVolume(); var api = builder.AddProject("bikes-api") .WithReference(mongodb) - .WithReference(kafka); + .WithReference(kafka); + +var generator = builder.AddProject("bikes-generator") + .WithReference(kafka); builder.Build().Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Generator/ContractGenerator.cs b/Bikes/Bikes.Generator/ContractGenerator.cs index b81a73727..ca294cc80 100644 --- a/Bikes/Bikes.Generator/ContractGenerator.cs +++ b/Bikes/Bikes.Generator/ContractGenerator.cs @@ -4,6 +4,9 @@ namespace Bikes.Generator; +/// +/// Generates fake data for various entities using Bogus library +/// public class ContractGenerator { private readonly Faker _bikeFaker; @@ -11,6 +14,9 @@ public class ContractGenerator private readonly Faker _renterFaker; private readonly Faker _rentFaker; + /// + /// Initializes the generator with fake data rules for all entity types + /// public ContractGenerator() { _bikeFaker = new Faker() @@ -50,11 +56,35 @@ public ContractGenerator() }); } + /// + /// Generates a fake bike DTO with random data + /// + /// Generated bike DTO public BikeCreateUpdateDto GenerateBike() => _bikeFaker.Generate(); + + /// + /// Generates a fake bike model DTO with random data + /// + /// Generated bike model DTO public BikeModelCreateUpdateDto GenerateBikeModel() => _bikeModelFaker.Generate(); + + /// + /// Generates a fake renter DTO with random data + /// + /// Generated renter DTO public RenterCreateUpdateDto GenerateRenter() => _renterFaker.Generate(); + + /// + /// Generates a fake rent DTO with random data + /// + /// Generated rent DTO public RentCreateUpdateDto GenerateRent() => _rentFaker.Generate(); + /// + /// Generates a batch of random entities + /// + /// Number of entities to generate + /// List of generated entity DTOs public List GenerateBatch(int size) { var batch = new List(); diff --git a/Bikes/Bikes.Generator/IKafkaProducerFactory.cs b/Bikes/Bikes.Generator/IKafkaProducerFactory.cs index 31e10294c..848ac0c40 100644 --- a/Bikes/Bikes.Generator/IKafkaProducerFactory.cs +++ b/Bikes/Bikes.Generator/IKafkaProducerFactory.cs @@ -2,7 +2,14 @@ namespace Bikes.Generator; +/// +/// Factory interface for creating Kafka producers +/// public interface IKafkaProducerFactory { + /// + /// Creates and returns a Kafka producer instance + /// + /// Configured Kafka producer public IProducer CreateProducer(); } \ No newline at end of file diff --git a/Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs b/Bikes/Bikes.Generator/KafkaProducerFactory.cs similarity index 57% rename from Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs rename to Bikes/Bikes.Generator/KafkaProducerFactory.cs index 7cb52b668..54283ce12 100644 --- a/Bikes/Bikes.Generator/KafkaProducerFactory.cs.cs +++ b/Bikes/Bikes.Generator/KafkaProducerFactory.cs @@ -5,12 +5,18 @@ namespace Bikes.Generator; +/// +/// Factory implementation for creating and managing Kafka producer instances +/// public class KafkaProducerFactory : IKafkaProducerFactory, IDisposable { private readonly KafkaOptions _options; private readonly ILogger _logger; private IProducer? _producer; + /// + /// Initializes the factory with Kafka options and logger + /// public KafkaProducerFactory( IOptions options, ILogger logger) @@ -19,16 +25,51 @@ public KafkaProducerFactory( _logger = logger; } + /// + /// Gets the Kafka bootstrap servers from environment variable or configuration + /// + /// Bootstrap servers connection string + private string GetBootstrapServers() + { + var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__kafka"); + if (!string.IsNullOrEmpty(connectionString)) + { + return connectionString; + } + + return _options.BootstrapServers; + } + + /// + /// Creates a Kafka producer with retry logic and connection validation + /// + /// Configured Kafka producer instance public IProducer CreateProducer() { if (_producer != null) return _producer; + var bootstrapServers = GetBootstrapServers(); + + Console.WriteLine($"Kafka Producer connecting to: {bootstrapServers}"); + var config = new ProducerConfig { - BootstrapServers = _options.BootstrapServers, - MessageTimeoutMs = _options.MessageTimeoutMs, - EnableDeliveryReports = true + BootstrapServers = bootstrapServers, + EnableDeliveryReports = true, + + ApiVersionRequest = false, + ApiVersionFallbackMs = 0, + BrokerVersionFallback = "0.10.0.0", + SecurityProtocol = SecurityProtocol.Plaintext, + SslEndpointIdentificationAlgorithm = SslEndpointIdentificationAlgorithm.None, + + SocketTimeoutMs = 30000, + MessageTimeoutMs = 30000, + RequestTimeoutMs = 30000, + + EnableIdempotence = false, + Acks = Acks.Leader }; var retryCount = 0; @@ -44,7 +85,7 @@ public IProducer CreateProducer() .Build(); _logger.LogInformation("Kafka producer connected successfully to {BootstrapServers}", - _options.BootstrapServers); + bootstrapServers); return _producer; } @@ -68,6 +109,9 @@ public IProducer CreateProducer() throw new InvalidOperationException("Failed to create Kafka producer"); } + /// + /// Disposes the Kafka producer instance + /// public void Dispose() { _producer?.Dispose(); diff --git a/Bikes/Bikes.Generator/KafkaProducerService.cs b/Bikes/Bikes.Generator/KafkaProducerService.cs index 002304afb..3aca55d24 100644 --- a/Bikes/Bikes.Generator/KafkaProducerService.cs +++ b/Bikes/Bikes.Generator/KafkaProducerService.cs @@ -1,5 +1,6 @@ using Bikes.Generator.Options; using Confluent.Kafka; +using Confluent.Kafka.Admin; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -7,6 +8,9 @@ namespace Bikes.Generator; +/// +/// Background service for generating and publishing fake data to Kafka +/// public class KafkaProducerService : BackgroundService { private readonly GeneratorOptions _generatorOptions; @@ -14,9 +18,14 @@ public class KafkaProducerService : BackgroundService private readonly IKafkaProducerFactory _producerFactory; private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions; + private readonly string _bootstrapServers; + /// + /// Initializes the producer service with configuration and dependencies + /// public KafkaProducerService( IOptions generatorOptions, + IOptions kafkaOptions, ContractGenerator contractGenerator, IKafkaProducerFactory producerFactory, ILogger logger) @@ -25,6 +34,7 @@ public KafkaProducerService( _contractGenerator = contractGenerator; _producerFactory = producerFactory; _logger = logger; + _bootstrapServers = kafkaOptions.Value.BootstrapServers; _jsonOptions = new JsonSerializerOptions { @@ -33,6 +43,22 @@ public KafkaProducerService( }; } + /// + /// Gets the Kafka bootstrap servers from environment variable or configuration + /// + /// Bootstrap servers connection string + private string GetBootstrapServers() + { + var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__kafka"); + if (!string.IsNullOrEmpty(connectionString)) + return connectionString; + + return _bootstrapServers; + } + + /// + /// Main execution method that generates and publishes data to Kafka + /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting Kafka Producer Service"); @@ -45,8 +71,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) return; } + await CreateTopicIfNotExistsAsync(stoppingToken); + var producer = _producerFactory.CreateProducer(); + await Task.Delay(2000, stoppingToken); + + _logger.LogInformation("Starting message generation..."); + while (!stoppingToken.IsCancellationRequested) { try @@ -82,6 +114,77 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("Kafka Producer Service stopped"); } + /// + /// Creates the Kafka topic if it doesn't already exist + /// + private async Task CreateTopicIfNotExistsAsync(CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Checking if topic '{Topic}' exists...", _generatorOptions.Topic); + + var bootstrapServers = GetBootstrapServers(); + + _logger.LogInformation($"Using Kafka at: {bootstrapServers}"); + + using var adminClient = new AdminClientBuilder(new AdminClientConfig + { + BootstrapServers = bootstrapServers, + ApiVersionRequest = false, + BrokerVersionFallback = "0.10.0.0", + SecurityProtocol = SecurityProtocol.Plaintext + }).Build(); + + try + { + var metadata = adminClient.GetMetadata(TimeSpan.FromSeconds(10)); + var topicExists = metadata.Topics.Any(t => t.Topic == _generatorOptions.Topic && !t.Error.IsError); + + if (topicExists) + { + _logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); + return; + } + } + catch (KafkaException) + { + + } + + _logger.LogInformation("Creating topic '{Topic}'...", _generatorOptions.Topic); + + try + { + await adminClient.CreateTopicsAsync(new[] + { + new TopicSpecification + { + Name = _generatorOptions.Topic, + NumPartitions = 1, + ReplicationFactor = 1 + } + }); + + _logger.LogInformation("Topic '{Topic}' created successfully", _generatorOptions.Topic); + + await Task.Delay(3000, cancellationToken); + } + catch (CreateTopicsException ex) when (ex.Results[0].Error.Code == ErrorCode.TopicAlreadyExists) + { + _logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error checking/creating topic. Will try to continue..."); + } + } + + /// + /// Creates a Kafka message from a contract object + /// + /// The contract object to serialize + /// Kafka message or null if serialization fails private Message? CreateKafkaMessage(object contract) { try @@ -89,7 +192,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var json = JsonSerializer.Serialize(contract, contract.GetType(), _jsonOptions); var message = new Message { Value = json }; - // Добавляем метаданные в headers для определения типа контракта message.Headers = new Headers { new Header("contract-type", System.Text.Encoding.UTF8.GetBytes(contract.GetType().Name)) @@ -104,6 +206,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } + /// + /// Publishes a message to Kafka topic + /// private async Task ProduceMessageAsync( IProducer producer, Message message, diff --git a/Bikes/Bikes.Generator/Options/GeneratorOptions.cs b/Bikes/Bikes.Generator/Options/GeneratorOptions.cs index 756d26bf7..017ced6e5 100644 --- a/Bikes/Bikes.Generator/Options/GeneratorOptions.cs +++ b/Bikes/Bikes.Generator/Options/GeneratorOptions.cs @@ -1,9 +1,27 @@ namespace Bikes.Generator.Options; +/// +/// Configuration options for the bikes data generator +/// public class GeneratorOptions { + /// + /// Gets or sets the generation interval in milliseconds + /// public int IntervalMs { get; set; } = 5000; + + /// + /// Gets or sets the number of bike records to generate per batch + /// public int BatchSize { get; set; } = 1; + + /// + /// Gets or sets the Kafka topic name for publishing generated data + /// public string Topic { get; set; } = "bikes-contracts"; + + /// + /// Gets or sets a value indicating whether the generator is enabled + /// public bool Enabled { get; set; } = true; } \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Options/KafkaOptions.cs b/Bikes/Bikes.Generator/Options/KafkaOptions.cs index abf8c88d4..0c6b21b78 100644 --- a/Bikes/Bikes.Generator/Options/KafkaOptions.cs +++ b/Bikes/Bikes.Generator/Options/KafkaOptions.cs @@ -1,9 +1,27 @@ namespace Bikes.Generator.Options; +/// +/// Configuration options for Kafka producer +/// public class KafkaOptions { + /// + /// Gets or sets the Kafka bootstrap servers address + /// public string BootstrapServers { get; set; } = "localhost:9092"; + + /// + /// Gets or sets the maximum number of retry attempts for failed operations + /// public int MaxRetryAttempts { get; set; } = 3; + + /// + /// Gets or sets the delay between retry attempts in milliseconds + /// public int RetryDelayMs { get; set; } = 1000; + + /// + /// Gets or sets the message timeout in milliseconds + /// { - // Регистрируем конфигурацию services.Configure( context.Configuration.GetSection("Generator")); services.Configure( context.Configuration.GetSection("Kafka")); - // Регистрируем сервисы services.AddSingleton(); services.AddSingleton(); - // Регистрируем BackgroundService - services.AddHostedService(); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); - // Настраиваем логирование services.AddLogging(builder => { builder.AddConsole(); diff --git a/Bikes/Bikes.Generator/appsettings.json b/Bikes/Bikes.Generator/appsettings.json index 687c3e129..af2ee7bae 100644 --- a/Bikes/Bikes.Generator/appsettings.json +++ b/Bikes/Bikes.Generator/appsettings.json @@ -13,9 +13,12 @@ "Enabled": true }, "Kafka": { - "BootstrapServers": "localhost:9092", - "MaxRetryAttempts": 5, - "RetryDelayMs": 1000, - "MessageTimeoutMs": 5000 + "MaxRetryAttempts": 10, + "RetryDelayMs": 2000, + "MessageTimeoutMs": 15000, + "ApiVersionRequest": false, + "ApiVersionFallbackMs": 0, + "BrokerVersionFallback": "0.10.0.0", + "SecurityProtocol": "Plaintext" } } \ No newline at end of file From a86c1bfa8332c74e1d514fa1e0d7b648aaeb23ed Mon Sep 17 00:00:00 2001 From: comandir26 Date: Wed, 24 Dec 2025 22:02:52 +0400 Subject: [PATCH 38/41] update readme and small fixes --- Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs | 1 - .../Kafka/KafkaConsumerExtensions.cs | 33 ++++++++----------- README.md | 15 +++++++++ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs index 10af3ddf3..81b4fdf53 100644 --- a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs @@ -178,7 +178,6 @@ private async Task ProcessMessageAsync(string messageJson, string? contractType, break; default: - // Попробуем определить тип по структуре JSON await TryAutoDetectAndProcessAsync(messageJson, scope, cancellationToken); break; } diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs index 16ce58ce3..33f9fb38a 100644 --- a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs @@ -21,17 +21,16 @@ public static IServiceCollection AddKafkaConsumer(this IServiceCollection servic configuration.GetSection("Kafka").Bind(options); var aspireKafkaConnection = configuration.GetConnectionString("kafka"); + if (!string.IsNullOrEmpty(aspireKafkaConnection)) { - Console.WriteLine($"Using Kafka connection from Aspire: {aspireKafkaConnection}"); options.BootstrapServers = aspireKafkaConnection; } - else + + else if (string.IsNullOrEmpty(options.BootstrapServers)) { - Console.WriteLine($"Using Kafka from appsettings: {options.BootstrapServers}"); + options.BootstrapServers = "localhost:9092"; } - - Console.WriteLine($"Final Kafka BootstrapServers: {options.BootstrapServers}"); }); services.AddSingleton>(provider => @@ -39,8 +38,6 @@ public static IServiceCollection AddKafkaConsumer(this IServiceCollection servic var options = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); - Console.WriteLine($"Creating Kafka consumer for: {options.BootstrapServers}"); - var config = new ConsumerConfig { BootstrapServers = options.BootstrapServers, @@ -50,18 +47,15 @@ public static IServiceCollection AddKafkaConsumer(this IServiceCollection servic ApiVersionRequest = false, BrokerVersionFallback = "0.10.0.0", - ApiVersionFallbackMs = 0, - SecurityProtocol = SecurityProtocol.Plaintext, - SslEndpointIdentificationAlgorithm = SslEndpointIdentificationAlgorithm.None, + SocketTimeoutMs = 30000, SessionTimeoutMs = 30000, MetadataMaxAgeMs = 300000, + AllowAutoCreateTopics = false, - EnablePartitionEof = true, - EnableSslCertificateVerification = false, - Debug = "broker,protocol" + EnablePartitionEof = true }; var retryCount = 0; @@ -73,15 +67,16 @@ public static IServiceCollection AddKafkaConsumer(this IServiceCollection servic .SetErrorHandler((_, error) => { if (error.IsFatal) - logger.LogError("Kafka Fatal Error: {Reason} (Code: {Code})", error.Reason, error.Code); - else - logger.LogWarning("⚠Kafka Warning: {Reason} (Code: {Code})", error.Reason, error.Code); + logger.LogError("Kafka Fatal Error: {Reason} (Code: {Code})", + error.Reason, error.Code); + else if (error.Code != ErrorCode.Local_Transport) + logger.LogWarning("Kafka Warning: {Reason} (Code: {Code})", + error.Reason, error.Code); }) - .SetLogHandler((_, logMessage) => - logger.LogDebug("Kafka log: {Facility} - {Message}", logMessage.Facility, logMessage.Message)) .Build(); - logger.LogInformation("Kafka consumer created successfully!"); + logger.LogInformation("Kafka consumer created successfully for bootstrap servers: {BootstrapServers}", + options.BootstrapServers); return consumer; } catch (Exception ex) diff --git a/README.md b/README.md index 0e59cbb50..8f4218301 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,18 @@ ### Bikes.AppHost - слой оркестрации приложений +В четвертой лабораторной работе был имплементирован сервис, который генерирует контракты. +Контракты далее передаются в сервер и сохраняются в бд + +### Bikes.Generator - сервис генерации данных + +### Интеграция Kafka в существующие проекты: + +#### Bikes.Api.Host: + +##### KafkaConsumer - фоновая служба для потребления сообщений + +#### Bikes.AppHost: + +##### Добавлен Kafka-контейнер для брокера сообщений + From 1d0c94cd5957cd85346de952d003f5c2f58fb47d Mon Sep 17 00:00:00 2001 From: Vladislav <126959557+comandir26@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:06:31 +0400 Subject: [PATCH 39/41] Update readme2 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8f4218301..073213269 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ ### Bikes.AppHost - слой оркестрации приложений +## Лабораторная работа 4 - "Инфраструктура" + В четвертой лабораторной работе был имплементирован сервис, который генерирует контракты. Контракты далее передаются в сервер и сохраняются в бд From 22ca967b39838c10201e01032c184eac8a5a90a6 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 26 Dec 2025 17:10:17 +0400 Subject: [PATCH 40/41] Fixes and fixes --- Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs | 184 +++++++++--------- .../Kafka/KafkaConsumerExtensions.cs | 2 +- Bikes/Bikes.Api.Host/Program.cs | 15 -- Bikes/Bikes.AppHost/Program.cs | 11 +- Bikes/Bikes.Generator/Bikes.Generator.csproj | 3 +- Bikes/Bikes.Generator/KafkaProducerFactory.cs | 47 +++-- Bikes/Bikes.Generator/KafkaProducerService.cs | 105 +++++----- Bikes/Bikes.Generator/Program.cs | 36 ++-- 8 files changed, 190 insertions(+), 213 deletions(-) diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs index 81b4fdf53..7e605651c 100644 --- a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs @@ -3,107 +3,95 @@ using Confluent.Kafka; using Microsoft.Extensions.Options; using System.Text.Json; +using static Confluent.Kafka.ConfigPropertyNames; namespace Bikes.Api.Host.Kafka; /// /// Background service for consuming Kafka messages and processing contract DTOs /// -public class KafkaConsumer : BackgroundService +public class KafkaConsumer( + IConsumer consumer, + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) : BackgroundService { - private readonly IConsumer _consumer; - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - private readonly KafkaConsumerOptions _options; - private readonly JsonSerializerOptions _jsonOptions; - - /// - /// Initializes the Kafka consumer with configuration and dependencies - /// - public KafkaConsumer( - IConsumer consumer, - IServiceScopeFactory scopeFactory, - IOptions options, - ILogger logger) + private readonly KafkaConsumerOptions _options = options.Value; + private readonly JsonSerializerOptions _jsonOptions = new() { - _consumer = consumer; - _scopeFactory = scopeFactory; - _logger = logger; - _options = options.Value; - - _jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - } + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; /// /// Main execution method that consumes and processes Kafka messages /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Waiting for application to fully start (20 seconds)..."); - await Task.Delay(20000, stoppingToken); - - _logger.LogInformation("Starting KafkaConsumer..."); - - try + _ = Task.Run(async () => { - _consumer.Subscribe(_options.Topic); - _logger.LogInformation("KafkaConsumer subscribed to topic: {Topic}", _options.Topic); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to subscribe to Kafka topic"); - return; - } + await Task.Delay(5000, stoppingToken); + logger.LogInformation("Starting KafkaConsumer in background thread..."); - while (!stoppingToken.IsCancellationRequested) - { try { - var consumeResult = _consumer.Consume(TimeSpan.FromSeconds(5)); + consumer.Subscribe(_options.Topic); + logger.LogInformation("KafkaConsumer subscribed to topic: {Topic}", _options.Topic); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to subscribe to Kafka topic"); + return; + } - if (consumeResult == null) + while (!stoppingToken.IsCancellationRequested) + { + try { - continue; - } + var consumeResult = consumer.Consume(TimeSpan.FromSeconds(5)); - if (string.IsNullOrEmpty(consumeResult.Message?.Value)) - { - _logger.LogWarning("Received empty message"); - continue; - } + if (consumeResult == null) + { + continue; + } - _logger.LogDebug("Processing message at offset {Offset}", - consumeResult.TopicPartitionOffset); + if (string.IsNullOrEmpty(consumeResult.Message?.Value)) + { + logger.LogWarning("Received empty message"); + continue; + } - var contractType = DetermineContractType(consumeResult.Message.Headers); - await ProcessMessageAsync(consumeResult.Message.Value, contractType, stoppingToken); + logger.LogDebug("Processing message at offset {Offset}", + consumeResult.TopicPartitionOffset); - _consumer.Commit(consumeResult); - } - catch (ConsumeException ex) - { - _logger.LogError(ex, "Kafka consumption error: {Error}. Waiting 10 seconds...", ex.Error.Reason); - await Task.Delay(10000, stoppingToken); - } - catch (OperationCanceledException) - { - _logger.LogInformation("KafkaConsumer is stopping..."); - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error in KafkaConsumer. Waiting 15 seconds..."); - await Task.Delay(15000, stoppingToken); + var contractType = DetermineContractType(consumeResult.Message.Headers); + ProcessMessage(consumeResult.Message.Value, contractType); + + consumer.Commit(consumeResult); + } + catch (ConsumeException ex) + { + logger.LogError(ex, "Kafka consumption error: {Error}. Waiting 10 seconds...", ex.Error.Reason); + await Task.Delay(10000, stoppingToken); + } + catch (OperationCanceledException) + { + logger.LogInformation("KafkaConsumer is stopping..."); + break; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error in KafkaConsumer. Waiting 15 seconds..."); + await Task.Delay(15000, stoppingToken); + } } - } - _consumer.Close(); - _logger.LogInformation("KafkaConsumer stopped"); + consumer.Close(); + logger.LogInformation("KafkaConsumer stopped"); + }, stoppingToken); + + await Task.CompletedTask; } /// @@ -111,7 +99,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// /// Kafka message headers /// Contract type name or null if not found - private string? DetermineContractType(Headers headers) + private static string? DetermineContractType(Headers headers) { if (headers == null) return null; @@ -129,9 +117,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// /// JSON message content /// Type of contract to process - private async Task ProcessMessageAsync(string messageJson, string? contractType, CancellationToken cancellationToken) + private void ProcessMessage(string messageJson, string? contractType) { - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); try { @@ -143,7 +131,7 @@ private async Task ProcessMessageAsync(string messageJson, string? contractType, { var bikeService = scope.ServiceProvider.GetRequiredService(); var result = bikeService.CreateBike(bikeDto); - _logger.LogInformation("Created bike with ID: {BikeId}", result?.Id); + logger.LogInformation("Created bike with ID: {BikeId}", result?.Id); } break; @@ -153,7 +141,7 @@ private async Task ProcessMessageAsync(string messageJson, string? contractType, { var bikeModelService = scope.ServiceProvider.GetRequiredService(); var result = bikeModelService.CreateBikeModel(bikeModelDto); - _logger.LogInformation("Created bike model with ID: {ModelId}", result?.Id); + logger.LogInformation("Created bike model with ID: {ModelId}", result?.Id); } break; @@ -163,7 +151,7 @@ private async Task ProcessMessageAsync(string messageJson, string? contractType, { var renterService = scope.ServiceProvider.GetRequiredService(); var result = renterService.CreateRenter(renterDto); - _logger.LogInformation("Created renter with ID: {RenterId}", result?.Id); + logger.LogInformation("Created renter with ID: {RenterId}", result?.Id); } break; @@ -173,33 +161,33 @@ private async Task ProcessMessageAsync(string messageJson, string? contractType, { var rentService = scope.ServiceProvider.GetRequiredService(); var result = rentService.CreateRent(rentDto); - _logger.LogInformation("Created rent with ID: {RentId}", result?.Id); + logger.LogInformation("Created rent with ID: {RentId}", result?.Id); } break; default: - await TryAutoDetectAndProcessAsync(messageJson, scope, cancellationToken); + TryAutoDetectAndProcess(messageJson, scope); break; } } catch (JsonException ex) { - _logger.LogError(ex, "Failed to deserialize message: {Message}", messageJson); + logger.LogError(ex, "Failed to deserialize message: {Message}", messageJson); } catch (ArgumentException ex) { - _logger.LogWarning("Validation error: {ErrorMessage}", ex.Message); + logger.LogWarning("Validation error: {ErrorMessage}", ex.Message); } catch (Exception ex) { - _logger.LogError(ex, "Error processing message"); + logger.LogError(ex, "Error processing message"); } } /// /// Attempts to auto-detect contract type from JSON structure and process accordingly /// - private async Task TryAutoDetectAndProcessAsync(string messageJson, IServiceScope scope, CancellationToken cancellationToken) + private void TryAutoDetectAndProcess(string messageJson, IServiceScope scope) { try { @@ -241,12 +229,12 @@ private async Task TryAutoDetectAndProcessAsync(string messageJson, IServiceScop } else { - _logger.LogWarning("Could not determine contract type for message: {Message}", messageJson); + logger.LogWarning("Could not determine contract type for message: {Message}", messageJson); } } catch (Exception ex) { - _logger.LogError(ex, "Failed to auto-detect and process message"); + logger.LogError(ex, "Failed to auto-detect and process message"); } } @@ -255,7 +243,23 @@ private async Task TryAutoDetectAndProcessAsync(string messageJson, IServiceScop /// public override void Dispose() { - _consumer?.Dispose(); - base.Dispose(); + try + { + if (consumer != null) + { + consumer.Close(); + consumer.Dispose(); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Error while disposing Kafka consumer"); + } + finally + { + base.Dispose(); + + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs index 33f9fb38a..0bd65cbec 100644 --- a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumerExtensions.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddKafkaConsumer(this IServiceCollection servic } }); - services.AddSingleton>(provider => + services.AddSingleton(provider => { var options = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); diff --git a/Bikes/Bikes.Api.Host/Program.cs b/Bikes/Bikes.Api.Host/Program.cs index e0172c75a..6c3d60639 100644 --- a/Bikes/Bikes.Api.Host/Program.cs +++ b/Bikes/Bikes.Api.Host/Program.cs @@ -18,13 +18,6 @@ var app = builder.Build(); -app.Lifetime.ApplicationStarted.Register(() => -{ - var logger = app.Services.GetRequiredService>(); - logger.LogInformation("Waiting 5 seconds for services to stabilize..."); - Thread.Sleep(5000); -}); - app.MapDefaultEndpoints(); using (var scope = app.Services.CreateScope()) @@ -55,12 +48,4 @@ app.MapControllers(); -app.Lifetime.ApplicationStarted.Register(async () => -{ - var logger = app.Services.GetRequiredService>(); - logger.LogInformation("Application fully started. Waiting 3 seconds before accepting requests..."); - await Task.Delay(3000); - logger.LogInformation("Application ready to accept requests."); -}); - app.Run(); \ No newline at end of file diff --git a/Bikes/Bikes.AppHost/Program.cs b/Bikes/Bikes.AppHost/Program.cs index 36ffc3085..155e4f64d 100644 --- a/Bikes/Bikes.AppHost/Program.cs +++ b/Bikes/Bikes.AppHost/Program.cs @@ -7,11 +7,14 @@ var mongodb = builder.AddMongoDB("mongodb") .WithDataVolume(); -var api = builder.AddProject("bikes-api") +var _ = builder.AddProject("bikes-api") .WithReference(mongodb) - .WithReference(kafka); + .WaitFor(mongodb) + .WithReference(kafka) + .WaitFor(kafka); -var generator = builder.AddProject("bikes-generator") - .WithReference(kafka); +var _2 = builder.AddProject("bikes-generator") + .WithReference(kafka) + .WaitFor(kafka); builder.Build().Run(); \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Bikes.Generator.csproj b/Bikes/Bikes.Generator/Bikes.Generator.csproj index 4cb45f28f..8fc72e53a 100644 --- a/Bikes/Bikes.Generator/Bikes.Generator.csproj +++ b/Bikes/Bikes.Generator/Bikes.Generator.csproj @@ -12,12 +12,13 @@ - + + diff --git a/Bikes/Bikes.Generator/KafkaProducerFactory.cs b/Bikes/Bikes.Generator/KafkaProducerFactory.cs index 54283ce12..8f5561120 100644 --- a/Bikes/Bikes.Generator/KafkaProducerFactory.cs +++ b/Bikes/Bikes.Generator/KafkaProducerFactory.cs @@ -8,23 +8,13 @@ namespace Bikes.Generator; /// /// Factory implementation for creating and managing Kafka producer instances /// -public class KafkaProducerFactory : IKafkaProducerFactory, IDisposable +public class KafkaProducerFactory( + IOptions options, + ILogger logger) : IKafkaProducerFactory, IDisposable { - private readonly KafkaOptions _options; - private readonly ILogger _logger; + private readonly KafkaOptions _options = options.Value; private IProducer? _producer; - /// - /// Initializes the factory with Kafka options and logger - /// - public KafkaProducerFactory( - IOptions options, - ILogger logger) - { - _options = options.Value; - _logger = logger; - } - /// /// Gets the Kafka bootstrap servers from environment variable or configuration /// @@ -79,12 +69,12 @@ public IProducer CreateProducer() { _producer = new ProducerBuilder(config) .SetLogHandler((_, message) => - _logger.LogInformation("Kafka: {Facility} - {Message}", message.Facility, message.Message)) + logger.LogInformation("Kafka: {Facility} - {Message}", message.Facility, message.Message)) .SetErrorHandler((_, error) => - _logger.LogError("Kafka Error: {Reason} (Code: {Code})", error.Reason, error.Code)) + logger.LogError("Kafka Error: {Reason} (Code: {Code})", error.Reason, error.Code)) .Build(); - _logger.LogInformation("Kafka producer connected successfully to {BootstrapServers}", + logger.LogInformation("Kafka producer connected successfully to {BootstrapServers}", bootstrapServers); return _producer; @@ -92,13 +82,13 @@ public IProducer CreateProducer() catch (Exception ex) { retryCount++; - _logger.LogWarning(ex, + logger.LogWarning(ex, "Failed to connect to Kafka (attempt {RetryCount}/{MaxRetries}). Retrying in {DelayMs}ms...", retryCount, _options.MaxRetryAttempts, _options.RetryDelayMs); if (retryCount >= _options.MaxRetryAttempts) { - _logger.LogError(ex, "Max retry attempts reached. Failed to connect to Kafka."); + logger.LogError(ex, "Max retry attempts reached. Failed to connect to Kafka."); throw; } @@ -114,7 +104,22 @@ public IProducer CreateProducer() /// public void Dispose() { - _producer?.Dispose(); - _producer = null; + try + { + if (_producer != null) + { + _producer.Flush(TimeSpan.FromSeconds(5)); + _producer.Dispose(); + _producer = null; + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Error while disposing Kafka producer"); + } + finally + { + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/Bikes/Bikes.Generator/KafkaProducerService.cs b/Bikes/Bikes.Generator/KafkaProducerService.cs index 3aca55d24..c4aab5b8e 100644 --- a/Bikes/Bikes.Generator/KafkaProducerService.cs +++ b/Bikes/Bikes.Generator/KafkaProducerService.cs @@ -11,37 +11,20 @@ namespace Bikes.Generator; /// /// Background service for generating and publishing fake data to Kafka /// -public class KafkaProducerService : BackgroundService +public class KafkaProducerService( + IOptions generatorOptions, + IOptions kafkaOptions, + ContractGenerator contractGenerator, + IKafkaProducerFactory producerFactory, + ILogger logger) : BackgroundService { - private readonly GeneratorOptions _generatorOptions; - private readonly ContractGenerator _contractGenerator; - private readonly IKafkaProducerFactory _producerFactory; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - private readonly string _bootstrapServers; - - /// - /// Initializes the producer service with configuration and dependencies - /// - public KafkaProducerService( - IOptions generatorOptions, - IOptions kafkaOptions, - ContractGenerator contractGenerator, - IKafkaProducerFactory producerFactory, - ILogger logger) + private readonly GeneratorOptions _generatorOptions = generatorOptions.Value; + private readonly string _bootstrapServers = kafkaOptions.Value.BootstrapServers; + private readonly JsonSerializerOptions _jsonOptions = new() { - _generatorOptions = generatorOptions.Value; - _contractGenerator = contractGenerator; - _producerFactory = producerFactory; - _logger = logger; - _bootstrapServers = kafkaOptions.Value.BootstrapServers; - - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = false - }; - } + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; /// /// Gets the Kafka bootstrap servers from environment variable or configuration @@ -61,29 +44,29 @@ private string GetBootstrapServers() /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Starting Kafka Producer Service"); - _logger.LogInformation("Configuration: Interval={IntervalMs}ms, BatchSize={BatchSize}, Topic={Topic}", + logger.LogInformation("Starting Kafka Producer Service"); + logger.LogInformation("Configuration: Interval={IntervalMs}ms, BatchSize={BatchSize}, Topic={Topic}", _generatorOptions.IntervalMs, _generatorOptions.BatchSize, _generatorOptions.Topic); if (!_generatorOptions.Enabled) { - _logger.LogInformation("Generator is disabled. Service will not produce messages."); + logger.LogInformation("Generator is disabled. Service will not produce messages."); return; } await CreateTopicIfNotExistsAsync(stoppingToken); - var producer = _producerFactory.CreateProducer(); + var producer = producerFactory.CreateProducer(); await Task.Delay(2000, stoppingToken); - _logger.LogInformation("Starting message generation..."); + logger.LogInformation("Starting message generation..."); while (!stoppingToken.IsCancellationRequested) { try { - var batch = _contractGenerator.GenerateBatch(_generatorOptions.BatchSize); + var batch = contractGenerator.GenerateBatch(_generatorOptions.BatchSize); foreach (var contract in batch) { @@ -94,24 +77,24 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - _logger.LogDebug("Generated and sent batch of {Count} messages", batch.Count); + logger.LogDebug("Generated and sent batch of {Count} messages", batch.Count); await Task.Delay(_generatorOptions.IntervalMs, stoppingToken); } catch (OperationCanceledException) { - _logger.LogInformation("Producer service is stopping..."); + logger.LogInformation("Producer service is stopping..."); break; } catch (Exception ex) { - _logger.LogError(ex, "Error in producer service"); + logger.LogError(ex, "Error in producer service"); await Task.Delay(5000, stoppingToken); } } producer.Flush(stoppingToken); - _logger.LogInformation("Kafka Producer Service stopped"); + logger.LogInformation("Kafka Producer Service stopped"); } /// @@ -121,11 +104,11 @@ private async Task CreateTopicIfNotExistsAsync(CancellationToken cancellationTok { try { - _logger.LogInformation("Checking if topic '{Topic}' exists...", _generatorOptions.Topic); + logger.LogInformation("Checking if topic '{Topic}' exists...", _generatorOptions.Topic); var bootstrapServers = GetBootstrapServers(); - _logger.LogInformation($"Using Kafka at: {bootstrapServers}"); + logger.LogInformation("Using Kafka at: {BootstrapServers}", bootstrapServers); using var adminClient = new AdminClientBuilder(new AdminClientConfig { @@ -142,41 +125,41 @@ private async Task CreateTopicIfNotExistsAsync(CancellationToken cancellationTok if (topicExists) { - _logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); + logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); return; } } - catch (KafkaException) + catch (KafkaException ex) { - + logger.LogWarning(ex, "Failed to get Kafka metadata"); } - _logger.LogInformation("Creating topic '{Topic}'...", _generatorOptions.Topic); + logger.LogInformation("Creating topic '{Topic}'...", _generatorOptions.Topic); try { - await adminClient.CreateTopicsAsync(new[] - { - new TopicSpecification + await adminClient.CreateTopicsAsync( + [ + new() { Name = _generatorOptions.Topic, NumPartitions = 1, ReplicationFactor = 1 } - }); + ]); - _logger.LogInformation("Topic '{Topic}' created successfully", _generatorOptions.Topic); + logger.LogInformation("Topic '{Topic}' created successfully", _generatorOptions.Topic); await Task.Delay(3000, cancellationToken); } catch (CreateTopicsException ex) when (ex.Results[0].Error.Code == ErrorCode.TopicAlreadyExists) { - _logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); + logger.LogInformation("Topic '{Topic}' already exists", _generatorOptions.Topic); } } catch (Exception ex) { - _logger.LogWarning(ex, "Error checking/creating topic. Will try to continue..."); + logger.LogWarning(ex, "Error checking/creating topic. Will try to continue..."); } } @@ -190,18 +173,18 @@ await adminClient.CreateTopicsAsync(new[] try { var json = JsonSerializer.Serialize(contract, contract.GetType(), _jsonOptions); - var message = new Message { Value = json }; - - message.Headers = new Headers + return new Message { - new Header("contract-type", System.Text.Encoding.UTF8.GetBytes(contract.GetType().Name)) + Value = json, + Headers = + [ + new Header("contract-type", System.Text.Encoding.UTF8.GetBytes(contract.GetType().Name)) + ] }; - - return message; } catch (Exception ex) { - _logger.LogError(ex, "Failed to serialize contract: {ContractType}", contract.GetType().Name); + logger.LogError(ex, "Failed to serialize contract: {ContractType}", contract.GetType().Name); return null; } } @@ -221,14 +204,14 @@ private async Task ProduceMessageAsync( message, cancellationToken); - _logger.LogDebug("Message delivered to {Topic} [{Partition}] @ {Offset}", + logger.LogDebug("Message delivered to {Topic} [{Partition}] @ {Offset}", deliveryResult.Topic, deliveryResult.Partition, deliveryResult.Offset); } catch (ProduceException ex) { - _logger.LogError(ex, "Failed to deliver message: {Error}", ex.Error.Reason); + logger.LogError(ex, "Failed to deliver message: {Error}", ex.Error.Reason); } } } \ No newline at end of file diff --git a/Bikes/Bikes.Generator/Program.cs b/Bikes/Bikes.Generator/Program.cs index 6a318b4c5..cc19f1bd6 100644 --- a/Bikes/Bikes.Generator/Program.cs +++ b/Bikes/Bikes.Generator/Program.cs @@ -1,29 +1,25 @@ using Bikes.Generator; -using Bikes.Generator.Options; +using Bikes.Generator.Options; +using Bikes.ServiceDefaults; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -var host = Host.CreateDefaultBuilder(args) - .ConfigureServices((context, services) => - { - services.Configure( - context.Configuration.GetSection("Generator")); - services.Configure( - context.Configuration.GetSection("Kafka")); +var builder = Host.CreateApplicationBuilder(args); - services.AddSingleton(); - services.AddSingleton(); +builder.AddServiceDefaults(); - services.AddSingleton(); - services.AddHostedService(provider => provider.GetRequiredService()); +builder.Services.Configure( + builder.Configuration.GetSection("Generator")); +builder.Services.Configure( + builder.Configuration.GetSection("Kafka")); - services.AddLogging(builder => - { - builder.AddConsole(); - builder.SetMinimumLevel(LogLevel.Information); - }); - }) - .Build(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddHostedService(provider => + provider.GetRequiredService()); + +var host = builder.Build(); await host.RunAsync(); \ No newline at end of file From 5f3a85ea0cebb28da6ad305078e891e9d9f7f477 Mon Sep 17 00:00:00 2001 From: comandir26 Date: Fri, 26 Dec 2025 17:15:52 +0400 Subject: [PATCH 41/41] smallest fix --- Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs index 7e605651c..99779ea24 100644 --- a/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs +++ b/Bikes/Bikes.Api.Host/Kafka/KafkaConsumer.cs @@ -3,7 +3,6 @@ using Confluent.Kafka; using Microsoft.Extensions.Options; using System.Text.Json; -using static Confluent.Kafka.ConfigPropertyNames; namespace Bikes.Api.Host.Kafka;