From a3eb106383a18af6833459e5abb93f2ddfc8e532 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 24 Oct 2025 17:37:39 +0400 Subject: [PATCH 01/19] Add classes --- lab1/Bikes.Domain/Bikes.Domain.csproj | 14 ++++++++ lab1/Bikes.Domain/Bikes.Domain.sln | 33 +++++++++++++++++ lab1/Bikes.Domain/Models/Bike.cs | 32 +++++++++++++++++ lab1/Bikes.Domain/Models/BikeModel.cs | 52 +++++++++++++++++++++++++++ lab1/Bikes.Domain/Models/BikeType.cs | 32 +++++++++++++++++ lab1/Bikes.Domain/Models/Rent.cs | 37 +++++++++++++++++++ lab1/Bikes.Domain/Models/Renter.cs | 22 ++++++++++++ lab1/Bikes.Domain/Program.cs | 11 ++++++ 8 files changed, 233 insertions(+) create mode 100644 lab1/Bikes.Domain/Bikes.Domain.csproj create mode 100644 lab1/Bikes.Domain/Bikes.Domain.sln create mode 100644 lab1/Bikes.Domain/Models/Bike.cs create mode 100644 lab1/Bikes.Domain/Models/BikeModel.cs create mode 100644 lab1/Bikes.Domain/Models/BikeType.cs create mode 100644 lab1/Bikes.Domain/Models/Rent.cs create mode 100644 lab1/Bikes.Domain/Models/Renter.cs create mode 100644 lab1/Bikes.Domain/Program.cs diff --git a/lab1/Bikes.Domain/Bikes.Domain.csproj b/lab1/Bikes.Domain/Bikes.Domain.csproj new file mode 100644 index 000000000..a9539e07a --- /dev/null +++ b/lab1/Bikes.Domain/Bikes.Domain.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/lab1/Bikes.Domain/Bikes.Domain.sln b/lab1/Bikes.Domain/Bikes.Domain.sln new file mode 100644 index 000000000..8addb5d2a --- /dev/null +++ b/lab1/Bikes.Domain/Bikes.Domain.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36603.0 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Domain", "Bikes.Domain.csproj", "{05557513-7E27-4EDC-A5C1-6A421582D538}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Tests", "..\Bikes.Tests\Bikes.Tests.csproj", "{BE6F3941-FC43-4217-9120-94E2C759AA9F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {05557513-7E27-4EDC-A5C1-6A421582D538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05557513-7E27-4EDC-A5C1-6A421582D538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05557513-7E27-4EDC-A5C1-6A421582D538}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05557513-7E27-4EDC-A5C1-6A421582D538}.Release|Any CPU.Build.0 = Release|Any CPU + {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4BCE112F-1E42-40D3-89D9-1006B2FE29CE} + EndGlobalSection +EndGlobal diff --git a/lab1/Bikes.Domain/Models/Bike.cs b/lab1/Bikes.Domain/Models/Bike.cs new file mode 100644 index 000000000..56fd7757b --- /dev/null +++ b/lab1/Bikes.Domain/Models/Bike.cs @@ -0,0 +1,32 @@ +namespace Bikes.Domain.Models; + +/// +/// Bicycle entity +/// +public class Bike +{ + /// + /// Unique identifier + /// + public int Id { get; set; } + + /// + /// Serial number of the bicycle + /// + public required string SerialNumber { get; set; } + + /// + /// Reference to bike model + /// + public required BikeModel Model { get; set; } + + /// + /// Color of the bicycle + /// + public required string Color { get; set; } + + /// + /// Availability status for rental + /// + public bool IsAvailable { get; set; } = true; +} \ No newline at end of file diff --git a/lab1/Bikes.Domain/Models/BikeModel.cs b/lab1/Bikes.Domain/Models/BikeModel.cs new file mode 100644 index 000000000..6e6045aa5 --- /dev/null +++ b/lab1/Bikes.Domain/Models/BikeModel.cs @@ -0,0 +1,52 @@ +namespace Bikes.Domain.Models; + +/// +/// Bicycle model information +/// +public class BikeModel +{ + /// + /// Unique identifier + /// + public int Id { get; set; } + + /// + /// Model name + /// + public required string Name { get; set; } + + /// + /// Type of bicycle + /// + public required BikeType Type { get; set; } + + /// + /// Wheel size in inches + /// + public required decimal WheelSize { get; set; } + + /// + /// Maximum permissible passenger weight in kg + /// + public required decimal MaxWeight { get; set; } + + /// + /// Bicycle weight in kg + /// + public required decimal Weight { get; set; } + + /// + /// Type of brakes + /// + public required string BrakeType { get; set; } + + /// + /// Model year + /// + public required int ModelYear { get; set; } + + /// + /// Price per hour of rental + /// + public required decimal PricePerHour { get; set; } +} \ No newline at end of file diff --git a/lab1/Bikes.Domain/Models/BikeType.cs b/lab1/Bikes.Domain/Models/BikeType.cs new file mode 100644 index 000000000..d7e75aa9f --- /dev/null +++ b/lab1/Bikes.Domain/Models/BikeType.cs @@ -0,0 +1,32 @@ +namespace Bikes.Domain.Models; + +/// +/// Type of bicycle +/// +public enum BikeType +{ + /// + /// Road bike + /// + Road, + + /// + /// Mountain bike + /// + Mountain, + + /// + /// Sport bike + /// + Sport, + + /// + /// Hybrid bike + /// + Hybrid, + + /// + /// Electric bike + /// + Electric +} \ No newline at end of file diff --git a/lab1/Bikes.Domain/Models/Rent.cs b/lab1/Bikes.Domain/Models/Rent.cs new file mode 100644 index 000000000..4b48bed26 --- /dev/null +++ b/lab1/Bikes.Domain/Models/Rent.cs @@ -0,0 +1,37 @@ +namespace Bikes.Domain.Models; + +/// +/// Bicycle rental record +/// +public class Rent +{ + /// + /// Unique identifier + /// + public int Id { get; set; } + + /// + /// Reference to rented bicycle + /// + public required Bike Bike { get; set; } + + /// + /// Reference to renter + /// + public required Renter Renter { get; set; } + + /// + /// Rental start time + /// + public required DateTime StartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int DurationHours { get; set; } + + /// + /// Total rental cost + /// + public decimal TotalCost => Bike.Model.PricePerHour * DurationHours; +} \ No newline at end of file diff --git a/lab1/Bikes.Domain/Models/Renter.cs b/lab1/Bikes.Domain/Models/Renter.cs new file mode 100644 index 000000000..c6925dca4 --- /dev/null +++ b/lab1/Bikes.Domain/Models/Renter.cs @@ -0,0 +1,22 @@ +namespace Bikes.Domain.Models; + +/// +/// Bicycle renter information +/// +public class Renter +{ + /// + /// Unique identifier + /// + public int Id { get; set; } + + /// + /// Full name of the renter + /// + public required string FullName { get; set; } + + /// + /// Contact phone number + /// + public required string Phone { get; set; } +} \ No newline at end of file diff --git a/lab1/Bikes.Domain/Program.cs b/lab1/Bikes.Domain/Program.cs new file mode 100644 index 000000000..14e422604 --- /dev/null +++ b/lab1/Bikes.Domain/Program.cs @@ -0,0 +1,11 @@ +using Bikes.Domain.Models; + +namespace Bikes.Domain; + +class Program +{ + static void Main() + { + Console.WriteLine("Bike Rental System - Domain Models"); + } +} \ No newline at end of file From 4b39a73dba9e4ca82ad49d9348ed8761612ed978 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 24 Oct 2025 17:38:31 +0400 Subject: [PATCH 02/19] Add tests with fixture --- lab1/Bikes.Tests/Bikes.Tests.csproj | 27 ++++ lab1/Bikes.Tests/BikesFixture.cs | 184 ++++++++++++++++++++++++++++ lab1/Bikes.Tests/BikesTests.cs | 132 ++++++++++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 lab1/Bikes.Tests/Bikes.Tests.csproj create mode 100644 lab1/Bikes.Tests/BikesFixture.cs create mode 100644 lab1/Bikes.Tests/BikesTests.cs diff --git a/lab1/Bikes.Tests/Bikes.Tests.csproj b/lab1/Bikes.Tests/Bikes.Tests.csproj new file mode 100644 index 000000000..3a1bf7982 --- /dev/null +++ b/lab1/Bikes.Tests/Bikes.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/lab1/Bikes.Tests/BikesFixture.cs b/lab1/Bikes.Tests/BikesFixture.cs new file mode 100644 index 000000000..72f4c5d21 --- /dev/null +++ b/lab1/Bikes.Tests/BikesFixture.cs @@ -0,0 +1,184 @@ +using Bikes.Domain.Models; + +namespace Bikes.Tests; + +/// +/// Fixture for bike rental tests providing test data +/// +public class BikesFixture +{ + /// + /// Gets the list of bike models + /// + public List Models { get; } + + /// + /// Gets the list of bikes + /// + public List Bikes { get; } + + /// + /// Gets the list of renters + /// + public List Renters { get; } + + /// + /// Gets the list of rental records + /// + public List Rents { get; } + + /// + /// Initializes a new instance of the BikesFixture class + /// + public BikesFixture() + { + Models = InitializeModels(); + Bikes = InitializeBikes(Models); + Renters = InitializeRenters(); + Rents = InitializeRents(Bikes, Renters); + } + + /// + /// Initializes the list of bike models + /// + /// List of bike models + private static List InitializeModels() + { + return + [ + new() { Id = 1, Name = "Sport Pro 1000", Type = BikeType.Sport, WheelSize = 28, MaxWeight = 120, Weight = 10, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 15 }, + new() { Id = 2, Name = "Mountain Extreme", Type = BikeType.Mountain, WheelSize = 29, MaxWeight = 130, Weight = 12, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 12 }, + new() { Id = 3, Name = "Road Racer", Type = BikeType.Road, WheelSize = 26, MaxWeight = 110, Weight = 8, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 10 }, + new() { Id = 4, Name = "Sport Elite", Type = BikeType.Sport, WheelSize = 27.5m, MaxWeight = 125, Weight = 11, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 14 }, + new() { Id = 5, Name = "Hybrid Comfort", Type = BikeType.Hybrid, WheelSize = 28, MaxWeight = 135, Weight = 13, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 8 }, + new() { Id = 6, Name = "Electric City", Type = BikeType.Electric, WheelSize = 26, MaxWeight = 140, Weight = 20, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 20 }, + new() { Id = 7, Name = "Sport Lightning", Type = BikeType.Sport, WheelSize = 29, MaxWeight = 115, Weight = 9.5m, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 16 }, + new() { Id = 8, Name = "Mountain King", Type = BikeType.Mountain, WheelSize = 27.5m, MaxWeight = 128, Weight = 11.5m, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 13 }, + new() { Id = 9, Name = "Road Speed", Type = BikeType.Road, WheelSize = 28, MaxWeight = 105, Weight = 7.5m, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 11 }, + new() { Id = 10, Name = "Sport Thunder", Type = BikeType.Sport, WheelSize = 26, MaxWeight = 122, Weight = 10.5m, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 15.5m }, + new() { Id = 11, Name = "Electric Mountain", Type = BikeType.Electric, WheelSize = 29, MaxWeight = 145, Weight = 22, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 25 } + ]; + } + + /// + /// Initializes the list of bikes + /// + /// List of bike models + /// List of bikes + private static List InitializeBikes(List models) + { + var bikes = new List(); + var colors = new[] { "Red", "Blue", "Green", "Black", "White", "Yellow" }; + + // + var bikeConfigurations = new[] + { + new { ModelIndex = 0, ColorIndex = 0 }, // Sport Pro 1000 - Red + new { ModelIndex = 1, ColorIndex = 1 }, // Mountain Extreme - Blue + new { ModelIndex = 2, ColorIndex = 2 }, // Road Racer - Green + new { ModelIndex = 3, ColorIndex = 3 }, // Sport Elite - Black + new { ModelIndex = 4, ColorIndex = 4 }, // Hybrid Comfort - White + new { ModelIndex = 5, ColorIndex = 5 }, // Electric City - Yellow + new { ModelIndex = 6, ColorIndex = 0 }, // Sport Lightning - Red + new { ModelIndex = 7, ColorIndex = 1 }, // Mountain King - Blue + new { ModelIndex = 8, ColorIndex = 2 }, // Road Speed - Green + new { ModelIndex = 9, ColorIndex = 3 }, // Sport Thunder - Black + new { ModelIndex = 10, ColorIndex = 4 }, // Electric Mountain - White + new { ModelIndex = 0, ColorIndex = 5 }, // Sport Pro 1000 - Yellow + new { ModelIndex = 1, ColorIndex = 0 }, // Mountain Extreme - Red + new { ModelIndex = 2, ColorIndex = 1 }, // Road Racer - Blue + new { ModelIndex = 3, ColorIndex = 2 } // Sport Elite - Green + }; + + for (int i = 0; i < bikeConfigurations.Length; i++) + { + var config = bikeConfigurations[i]; + var model = models[config.ModelIndex]; + var color = colors[config.ColorIndex]; + + bikes.Add(new Bike + { + Id = i + 1, + SerialNumber = $"SN{(i + 1):D6}", + Model = model, + Color = color, + IsAvailable = i % 2 == 0 // , - + }); + } + + return bikes; + } + + /// + /// Initializes the list of renters + /// + /// List of renters + private static List InitializeRenters() + { + return + [ + new() { Id = 1, FullName = "Ivanov Ivan", Phone = "+79111111111" }, + new() { Id = 2, FullName = "Petrov Petr", Phone = "+79112222222" }, + new() { Id = 3, FullName = "Sidorov Alexey", Phone = "+79113333333" }, + new() { Id = 4, FullName = "Kuznetsova Maria", Phone = "+79114444444" }, + new() { Id = 5, FullName = "Smirnov Dmitry", Phone = "+79115555555" }, + new() { Id = 6, FullName = "Vasilyeva Ekaterina", Phone = "+79116666666" }, + new() { Id = 7, FullName = "Popov Artem", Phone = "+79117777777" }, + new() { Id = 8, FullName = "Lebedeva Olga", Phone = "+79118888888" }, + new() { Id = 9, FullName = "Novikov Sergey", Phone = "+79119999999" }, + new() { Id = 10, FullName = "Morozova Anna", Phone = "+79110000000" }, + new() { Id = 11, FullName = "Volkov Pavel", Phone = "+79121111111" }, + new() { Id = 12, FullName = "Sokolova Irina", Phone = "+79122222222" } + ]; + } + + /// + /// Initializes the list of rental records + /// + private static List InitializeRents(List bikes, List renters) + { + var rents = new List(); + var rentId = 1; + + var rentalData = new[] + { + new { BikeIndex = 0, RenterIndex = 0, Duration = 5, DaysAgo = 2 }, + new { BikeIndex = 1, RenterIndex = 1, Duration = 3, DaysAgo = 5 }, + new { BikeIndex = 2, RenterIndex = 2, Duration = 8, DaysAgo = 1 }, + new { BikeIndex = 0, RenterIndex = 3, Duration = 2, DaysAgo = 7 }, + new { BikeIndex = 3, RenterIndex = 4, Duration = 12, DaysAgo = 3 }, + new { BikeIndex = 1, RenterIndex = 5, Duration = 6, DaysAgo = 4 }, + new { BikeIndex = 4, RenterIndex = 6, Duration = 4, DaysAgo = 6 }, + new { BikeIndex = 2, RenterIndex = 7, Duration = 10, DaysAgo = 2 }, + new { BikeIndex = 5, RenterIndex = 8, Duration = 7, DaysAgo = 1 }, + new { BikeIndex = 3, RenterIndex = 9, Duration = 3, DaysAgo = 8 }, + new { BikeIndex = 6, RenterIndex = 10, Duration = 15, DaysAgo = 2 }, + new { BikeIndex = 4, RenterIndex = 11, Duration = 9, DaysAgo = 5 }, + new { BikeIndex = 7, RenterIndex = 0, Duration = 2, DaysAgo = 3 }, + new { BikeIndex = 5, RenterIndex = 1, Duration = 6, DaysAgo = 4 }, + new { BikeIndex = 8, RenterIndex = 2, Duration = 11, DaysAgo = 1 }, + new { BikeIndex = 6, RenterIndex = 3, Duration = 4, DaysAgo = 7 }, + new { BikeIndex = 9, RenterIndex = 4, Duration = 8, DaysAgo = 2 }, + new { BikeIndex = 7, RenterIndex = 5, Duration = 5, DaysAgo = 5 }, + new { BikeIndex = 10, RenterIndex = 6, Duration = 13, DaysAgo = 3 }, + new { BikeIndex = 8, RenterIndex = 7, Duration = 7, DaysAgo = 4 } + }; + + foreach (var data in rentalData) + { + var bike = bikes[data.BikeIndex]; + var renter = renters[data.RenterIndex]; + + rents.Add(new Rent + { + Id = rentId++, + Bike = bike, + Renter = renter, + StartTime = DateTime.Now.AddDays(-data.DaysAgo), + DurationHours = data.Duration + }); + } + + return rents; + } +} \ No newline at end of file diff --git a/lab1/Bikes.Tests/BikesTests.cs b/lab1/Bikes.Tests/BikesTests.cs new file mode 100644 index 000000000..c94f15714 --- /dev/null +++ b/lab1/Bikes.Tests/BikesTests.cs @@ -0,0 +1,132 @@ +using Bikes.Domain.Models; + +namespace Bikes.Tests +{ + public class BikesTests : IClassFixture + { + private readonly BikesFixture _fixture; + + public BikesTests(BikesFixture fixture) + { + _fixture = fixture; + } + + /// + /// Test for retrieving all sport bikes information + /// + [Fact] + public void GetAllSportBikes() + { + var sportBikes = _fixture.Bikes + .Where(b => b.Model.Type == BikeType.Sport) + .ToList(); + + Assert.NotNull(sportBikes); + Assert.All(sportBikes, bike => Assert.Equal(BikeType.Sport, bike.Model.Type)); + } + + /// + /// Test for retrieving top 5 bike models by rental profit + /// + [Fact] + public void Top5ModelsByProfit() + { + var topModelsByProfit = _fixture.Rents + .GroupBy(r => r.Bike.Model) + .Select(g => new + { + Model = g.Key, + TotalProfit = g.Sum(r => r.TotalCost) + }) + .OrderByDescending(x => x.TotalProfit) + .Take(5) + .ToList(); + + Assert.NotNull(topModelsByProfit); + Assert.True(topModelsByProfit.Count <= 5); + } + + /// + /// Test for retrieving top 5 bike models by rental duration + /// + [Fact] + public void Top5ModelsByRentalDuration() + { + var topModelsByDuration = _fixture.Rents + .GroupBy(r => r.Bike.Model) + .Select(g => new + { + Model = g.Key, + TotalDuration = g.Sum(r => r.DurationHours) + }) + .OrderByDescending(x => x.TotalDuration) + .Take(5) + .ToList(); + + Assert.NotNull(topModelsByDuration); + Assert.True(topModelsByDuration.Count <= 5); + } + + /// + /// Test for rental statistics - min, max and average rental duration + /// + [Fact] + public void RentalStatistics() + { + var durations = _fixture.Rents.Select(r => r.DurationHours).ToList(); + + var minDuration = durations.Min(); + var maxDuration = durations.Max(); + var avgDuration = durations.Average(); + + Assert.Equal(2, minDuration); + Assert.Equal(15, maxDuration); + Assert.Equal(7, avgDuration); + } + + /// + /// Test for total rental time by bike type + /// + [Theory] + [InlineData(BikeType.Sport, 49)] + [InlineData(BikeType.Mountain, 16)] + [InlineData(BikeType.Road, 36)] + [InlineData(BikeType.Hybrid, 13)] + [InlineData(BikeType.Electric, 26)] + public void TotalRentalTimeByBikeType(BikeType bikeType, int expectedTotalTime) + { + var actualTotalTime = _fixture.Rents + .Where(r => r.Bike.Model.Type == bikeType) + .Sum(r => r.DurationHours); + + Assert.Equal(expectedTotalTime, actualTotalTime); + } + + /// + /// Test for top renters by rental count + /// + [Fact] + public void TopRentersByRentalCount() + { + var topRenters = _fixture.Rents + .GroupBy(r => r.Renter) + .Select(g => new + { + Renter = g.Key, + RentalCount = g.Count() + }) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToList(); + + Assert.NotNull(topRenters); + Assert.True(topRenters.Count <= 5); + + if (topRenters.Count > 0) + { + var maxRentalCount = topRenters.Max(x => x.RentalCount); + Assert.True(topRenters[0].RentalCount == maxRentalCount); + } + } + } +} \ No newline at end of file From edf335a83798366d6ac576d9c49495492d62ae22 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 24 Oct 2025 17:40:02 +0400 Subject: [PATCH 03/19] Add dotnet file --- lab1/.github/workflows/dotnet_tests.yml | 31 +++++++++++++++++++++++++ lab1/lab1.sln | 27 +++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 lab1/.github/workflows/dotnet_tests.yml create mode 100644 lab1/lab1.sln diff --git a/lab1/.github/workflows/dotnet_tests.yml b/lab1/.github/workflows/dotnet_tests.yml new file mode 100644 index 000000000..d850909a4 --- /dev/null +++ b/lab1/.github/workflows/dotnet_tests.yml @@ -0,0 +1,31 @@ +name: .NET Tests + +on: + push: + branches: ["main", "master"] + pull_request: + branches: ["main", "master"] + pull_request_target: + branches: ["main", "master"] + +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 Bikes/Bikes.sln + + - name: Build + run: dotnet build --no-restore --configuration Release lab1/lab1.sln + + - name: Run tests + run: dotnet test lab1/Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release --verbosity normal \ No newline at end of file diff --git a/lab1/lab1.sln b/lab1/lab1.sln new file mode 100644 index 000000000..104bc0d89 --- /dev/null +++ b/lab1/lab1.sln @@ -0,0 +1,27 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Domain", "Bikes.Domain\Bikes.Domain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal From 044a188300877a709389eef8d02e672524c81b01 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 24 Oct 2025 18:31:13 +0400 Subject: [PATCH 04/19] Add README file --- .../workflows/dotnet_tests.yml | 0 .../Bikes.Domain.csproj | 0 .../Bikes.Domain.sln | 0 .../Models/Bike.cs | 0 .../Models/BikeModel.cs | 0 .../Models/BikeType.cs | 0 .../Models/Rent.cs | 0 .../Models/Renter.cs | 0 .../Bikes.Domain => Bikes.Domain}/Program.cs | 0 .../Bikes.Tests.csproj | 0 .../BikesFixture.cs | 0 .../Bikes.Tests => Bikes.Tests}/BikesTests.cs | 0 README.md | 164 ++++-------------- lab1/lab1.sln => lab1.sln | 0 14 files changed, 32 insertions(+), 132 deletions(-) rename {lab1/.github => .github}/workflows/dotnet_tests.yml (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Bikes.Domain.csproj (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Bikes.Domain.sln (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Models/Bike.cs (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Models/BikeModel.cs (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Models/BikeType.cs (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Models/Rent.cs (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Models/Renter.cs (100%) rename {lab1/Bikes.Domain => Bikes.Domain}/Program.cs (100%) rename {lab1/Bikes.Tests => Bikes.Tests}/Bikes.Tests.csproj (100%) rename {lab1/Bikes.Tests => Bikes.Tests}/BikesFixture.cs (100%) rename {lab1/Bikes.Tests => Bikes.Tests}/BikesTests.cs (100%) rename lab1/lab1.sln => lab1.sln (100%) diff --git a/lab1/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml similarity index 100% rename from lab1/.github/workflows/dotnet_tests.yml rename to .github/workflows/dotnet_tests.yml diff --git a/lab1/Bikes.Domain/Bikes.Domain.csproj b/Bikes.Domain/Bikes.Domain.csproj similarity index 100% rename from lab1/Bikes.Domain/Bikes.Domain.csproj rename to Bikes.Domain/Bikes.Domain.csproj diff --git a/lab1/Bikes.Domain/Bikes.Domain.sln b/Bikes.Domain/Bikes.Domain.sln similarity index 100% rename from lab1/Bikes.Domain/Bikes.Domain.sln rename to Bikes.Domain/Bikes.Domain.sln diff --git a/lab1/Bikes.Domain/Models/Bike.cs b/Bikes.Domain/Models/Bike.cs similarity index 100% rename from lab1/Bikes.Domain/Models/Bike.cs rename to Bikes.Domain/Models/Bike.cs diff --git a/lab1/Bikes.Domain/Models/BikeModel.cs b/Bikes.Domain/Models/BikeModel.cs similarity index 100% rename from lab1/Bikes.Domain/Models/BikeModel.cs rename to Bikes.Domain/Models/BikeModel.cs diff --git a/lab1/Bikes.Domain/Models/BikeType.cs b/Bikes.Domain/Models/BikeType.cs similarity index 100% rename from lab1/Bikes.Domain/Models/BikeType.cs rename to Bikes.Domain/Models/BikeType.cs diff --git a/lab1/Bikes.Domain/Models/Rent.cs b/Bikes.Domain/Models/Rent.cs similarity index 100% rename from lab1/Bikes.Domain/Models/Rent.cs rename to Bikes.Domain/Models/Rent.cs diff --git a/lab1/Bikes.Domain/Models/Renter.cs b/Bikes.Domain/Models/Renter.cs similarity index 100% rename from lab1/Bikes.Domain/Models/Renter.cs rename to Bikes.Domain/Models/Renter.cs diff --git a/lab1/Bikes.Domain/Program.cs b/Bikes.Domain/Program.cs similarity index 100% rename from lab1/Bikes.Domain/Program.cs rename to Bikes.Domain/Program.cs diff --git a/lab1/Bikes.Tests/Bikes.Tests.csproj b/Bikes.Tests/Bikes.Tests.csproj similarity index 100% rename from lab1/Bikes.Tests/Bikes.Tests.csproj rename to Bikes.Tests/Bikes.Tests.csproj diff --git a/lab1/Bikes.Tests/BikesFixture.cs b/Bikes.Tests/BikesFixture.cs similarity index 100% rename from lab1/Bikes.Tests/BikesFixture.cs rename to Bikes.Tests/BikesFixture.cs diff --git a/lab1/Bikes.Tests/BikesTests.cs b/Bikes.Tests/BikesTests.cs similarity index 100% rename from lab1/Bikes.Tests/BikesTests.cs rename to Bikes.Tests/BikesTests.cs diff --git a/README.md b/README.md index 39c9a8443..5749def8c 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,36 @@ # Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) - -## Задание ### Цель Реализация проекта сервисно-ориентированного приложения. -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. - -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) - -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. - -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма - -image1 - -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [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). +# Задание "Пункт велопроката" +# Лабораторная работа 1 - "Классы" +В рамках первой лабораторной работы была добавлена доменная модель с основными сущностями пункта велопроката и реализованы юнит-тесты. +В базе данных пункта проката хранятся сведения о велосипедах, их арендаторах и выданных в аренду транспортных средствах. +Каждый велосипед характеризуется серийным номером, моделью, цветом. +Модель велосипеда является справочником, содержащим сведения о типе велосипеда, размере колес, предельно допустимом весе пассажира, весе велосипеда, типе тормозов, модельном годе. Для каждой модели велосипеда указывается цена часа аренды. +Тип велосипеда является перечислением. +Арендатор характеризуется ФИО, телефоном. +При выдаче велосипеда арендатору фиксируется время начала аренды и отмечается ее продолжительность в часах. + +### Классы +Bike - характеризует велосипед, содержит идентификатор, серийный номер, модель, цвет и статус доступности для аренды +BikeModel - информация о модели велосипеда, содержит название, тип, размер колес, максимальный вес, вес велосипеда, тип тормозов, модельный год и цену за час аренды +BikeType - перечисление типов велосипедов (шоссейный, горный, спортивный, гибридный, электрический) +Renter - информация об арендаторе, содержит идентификатор, ФИО и контактный телефон +Rent - информация об аренде велосипеда, содержит ссылки на велосипед и арендатора, время начала аренды, продолжительность и автоматически вычисляемую общую стоимость + +### Тесты +BikesTests - юнит-тесты с использованием fixture для подготовки тестовых данных: +GetAllSportBikes - Вывести информацию обо всех спортивных велосипедах +Top5ModelsByProfit - Вывести топ 5 моделей велосипедов по прибыли от аренды +Top5ModelsByRentalDuration - Вывести топ 5 моделей велосипедов по длительности аренды +RentalStatistics - Вывести информацию о минимальном, максимальном и среднем времени аренды велосипедов +TotalRentalTimeByBikeType - Вывести суммарное время аренды велосипедов каждого типа +TopRentersByRentalCount - Вывести информацию о клиентах, бравших велосипеды на прокат больше всего раз + +### Тестовые данные +BikesFixture - предоставляет предопределенные тестовые данные: +11 моделей велосипедов различных типов +15 велосипедов с фиксированным распределением моделей и цветов +12 арендаторов с тестовыми данными +20 записей об арендах с различной продолжительностью \ No newline at end of file diff --git a/lab1/lab1.sln b/lab1.sln similarity index 100% rename from lab1/lab1.sln rename to lab1.sln From 2c47679c76867a0db1f86bc8c50860cf2c57c274 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 24 Oct 2025 18:52:04 +0400 Subject: [PATCH 05/19] Fixed dotnet file --- .github/workflows/dotnet_tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml index d850909a4..9397a3511 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -22,10 +22,10 @@ jobs: dotnet-version: '8.x' - name: Restore dependencies - run: dotnet restore Bikes/Bikes.sln + run: dotnet restore lab1.sln - name: Build - run: dotnet build --no-restore --configuration Release lab1/lab1.sln + run: dotnet build lab1.sln --no-restore --configuration Release - name: Run tests - run: dotnet test lab1/Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release --verbosity normal \ No newline at end of file + run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release --verbosity normal \ No newline at end of file From 7cff20489ec011ac0a2bf058ac85330d082b901e Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Tue, 28 Oct 2025 23:52:06 +0400 Subject: [PATCH 06/19] Fixed remarks --- Bikes.Domain/Bikes.Domain.csproj | 5 - Bikes.Domain/Program.cs | 11 -- Bikes.Tests/BikesFixture.cs | 35 +++-- Bikes.Tests/BikesTests.cs | 250 +++++++++++++++++-------------- 4 files changed, 154 insertions(+), 147 deletions(-) delete mode 100644 Bikes.Domain/Program.cs diff --git a/Bikes.Domain/Bikes.Domain.csproj b/Bikes.Domain/Bikes.Domain.csproj index a9539e07a..fa71b7ae6 100644 --- a/Bikes.Domain/Bikes.Domain.csproj +++ b/Bikes.Domain/Bikes.Domain.csproj @@ -1,14 +1,9 @@  - Exe net8.0 enable enable - - - - diff --git a/Bikes.Domain/Program.cs b/Bikes.Domain/Program.cs deleted file mode 100644 index 14e422604..000000000 --- a/Bikes.Domain/Program.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bikes.Domain.Models; - -namespace Bikes.Domain; - -class Program -{ - static void Main() - { - Console.WriteLine("Bike Rental System - Domain Models"); - } -} \ No newline at end of file diff --git a/Bikes.Tests/BikesFixture.cs b/Bikes.Tests/BikesFixture.cs index 72f4c5d21..d94d5934e 100644 --- a/Bikes.Tests/BikesFixture.cs +++ b/Bikes.Tests/BikesFixture.cs @@ -70,27 +70,26 @@ private static List InitializeBikes(List models) var bikes = new List(); var colors = new[] { "Red", "Blue", "Green", "Black", "White", "Yellow" }; - // var bikeConfigurations = new[] { - new { ModelIndex = 0, ColorIndex = 0 }, // Sport Pro 1000 - Red - new { ModelIndex = 1, ColorIndex = 1 }, // Mountain Extreme - Blue - new { ModelIndex = 2, ColorIndex = 2 }, // Road Racer - Green - new { ModelIndex = 3, ColorIndex = 3 }, // Sport Elite - Black - new { ModelIndex = 4, ColorIndex = 4 }, // Hybrid Comfort - White - new { ModelIndex = 5, ColorIndex = 5 }, // Electric City - Yellow - new { ModelIndex = 6, ColorIndex = 0 }, // Sport Lightning - Red - new { ModelIndex = 7, ColorIndex = 1 }, // Mountain King - Blue - new { ModelIndex = 8, ColorIndex = 2 }, // Road Speed - Green - new { ModelIndex = 9, ColorIndex = 3 }, // Sport Thunder - Black - new { ModelIndex = 10, ColorIndex = 4 }, // Electric Mountain - White - new { ModelIndex = 0, ColorIndex = 5 }, // Sport Pro 1000 - Yellow - new { ModelIndex = 1, ColorIndex = 0 }, // Mountain Extreme - Red - new { ModelIndex = 2, ColorIndex = 1 }, // Road Racer - Blue - new { ModelIndex = 3, ColorIndex = 2 } // Sport Elite - Green + new { ModelIndex = 0, ColorIndex = 0 }, + new { ModelIndex = 1, ColorIndex = 1 }, + new { ModelIndex = 2, ColorIndex = 2 }, + new { ModelIndex = 3, ColorIndex = 3 }, + new { ModelIndex = 4, ColorIndex = 4 }, + new { ModelIndex = 5, ColorIndex = 5 }, + new { ModelIndex = 6, ColorIndex = 0 }, + new { ModelIndex = 7, ColorIndex = 1 }, + new { ModelIndex = 8, ColorIndex = 2 }, + new { ModelIndex = 9, ColorIndex = 3 }, + new { ModelIndex = 10, ColorIndex = 4 }, + new { ModelIndex = 0, ColorIndex = 5 }, + new { ModelIndex = 1, ColorIndex = 0 }, + new { ModelIndex = 2, ColorIndex = 1 }, + new { ModelIndex = 3, ColorIndex = 2 } }; - for (int i = 0; i < bikeConfigurations.Length; i++) + for (var i = 0; i < bikeConfigurations.Length; i++) { var config = bikeConfigurations[i]; var model = models[config.ModelIndex]; @@ -102,7 +101,7 @@ private static List InitializeBikes(List models) SerialNumber = $"SN{(i + 1):D6}", Model = model, Color = color, - IsAvailable = i % 2 == 0 // , - + IsAvailable = i % 2 == 0 }); } diff --git a/Bikes.Tests/BikesTests.cs b/Bikes.Tests/BikesTests.cs index c94f15714..1710ddb8d 100644 --- a/Bikes.Tests/BikesTests.cs +++ b/Bikes.Tests/BikesTests.cs @@ -1,132 +1,156 @@ using Bikes.Domain.Models; -namespace Bikes.Tests +namespace Bikes.Tests; + +public class BikesTests(BikesFixture fixture) : IClassFixture { - public class BikesTests : IClassFixture + /// + /// Test for retrieving all sport bikes information + /// + [Fact] + public void GetAllSportBikes() { - private readonly BikesFixture _fixture; - - public BikesTests(BikesFixture fixture) - { - _fixture = fixture; - } + var sportBikes = fixture.Bikes + .Where(b => b.Model.Type == BikeType.Sport) + .ToList(); - /// - /// Test for retrieving all sport bikes information - /// - [Fact] - public void GetAllSportBikes() + var expectedSportBikeNames = new[] { - var sportBikes = _fixture.Bikes - .Where(b => b.Model.Type == BikeType.Sport) - .ToList(); + "Sport Pro 1000", "Sport Elite", "Sport Lightning", "Sport Thunder" + }; + var expectedCount = 6; + + Assert.NotNull(sportBikes); + Assert.Equal(expectedCount, sportBikes.Count); + Assert.All(sportBikes, bike => Assert.Equal(BikeType.Sport, bike.Model.Type)); + Assert.All(sportBikes, bike => Assert.Contains(bike.Model.Name, expectedSportBikeNames)); + } - Assert.NotNull(sportBikes); - Assert.All(sportBikes, bike => Assert.Equal(BikeType.Sport, bike.Model.Type)); - } + /// + /// Test for retrieving top 5 bike models by rental profit + /// + [Fact] + public void Top5ModelsByProfit() + { + var topModelsByProfit = fixture.Rents + .GroupBy(r => r.Bike.Model) + .Select(g => new + { + Model = g.Key, + TotalProfit = g.Sum(r => r.TotalCost) + }) + .OrderByDescending(x => x.TotalProfit) + .Take(5) + .ToList(); + + var expectedCount = 5; + var expectedTopModel = "Electric Mountain"; + + Assert.NotNull(topModelsByProfit); + Assert.True(topModelsByProfit.Count <= expectedCount); + Assert.Equal(expectedCount, topModelsByProfit.Count); + Assert.Equal(expectedTopModel, topModelsByProfit.First().Model.Name); + } - /// - /// Test for retrieving top 5 bike models by rental profit - /// - [Fact] - public void Top5ModelsByProfit() - { - var topModelsByProfit = _fixture.Rents - .GroupBy(r => r.Bike.Model) - .Select(g => new - { - Model = g.Key, - TotalProfit = g.Sum(r => r.TotalCost) - }) - .OrderByDescending(x => x.TotalProfit) - .Take(5) - .ToList(); - - Assert.NotNull(topModelsByProfit); - Assert.True(topModelsByProfit.Count <= 5); - } + /// + /// Test for retrieving top 5 bike models by rental duration + /// + [Fact] + public void Top5ModelsByRentalDuration() + { + var topModelsByDuration = fixture.Rents + .GroupBy(r => r.Bike.Model) + .Select(g => new + { + Model = g.Key, + TotalDuration = g.Sum(r => r.DurationHours) + }) + .OrderByDescending(x => x.TotalDuration) + .Take(5) + .ToList(); + + var expectedCount = 5; + var expectedTopModel = "Sport Lightning"; + var expectedTopDuration = 19; + + Assert.NotNull(topModelsByDuration); + Assert.True(topModelsByDuration.Count <= expectedCount); + Assert.Equal(expectedCount, topModelsByDuration.Count); + Assert.Equal(expectedTopModel, topModelsByDuration.First().Model.Name); + Assert.Equal(expectedTopDuration, topModelsByDuration.First().TotalDuration); + } - /// - /// Test for retrieving top 5 bike models by rental duration - /// - [Fact] - public void Top5ModelsByRentalDuration() - { - var topModelsByDuration = _fixture.Rents - .GroupBy(r => r.Bike.Model) - .Select(g => new - { - Model = g.Key, - TotalDuration = g.Sum(r => r.DurationHours) - }) - .OrderByDescending(x => x.TotalDuration) - .Take(5) - .ToList(); - - Assert.NotNull(topModelsByDuration); - Assert.True(topModelsByDuration.Count <= 5); - } + /// + /// Test for rental statistics - min, max and average rental duration + /// + [Fact] + public void RentalStatistics() + { + var durations = fixture.Rents.Select(r => r.DurationHours).ToList(); - /// - /// Test for rental statistics - min, max and average rental duration - /// - [Fact] - public void RentalStatistics() - { - var durations = _fixture.Rents.Select(r => r.DurationHours).ToList(); + var minDuration = durations.Min(); + var maxDuration = durations.Max(); + var avgDuration = durations.Average(); - var minDuration = durations.Min(); - var maxDuration = durations.Max(); - var avgDuration = durations.Average(); + + var expectedMinDuration = 2; + var expectedMaxDuration = 15; + var expectedAvgDuration = 7; - Assert.Equal(2, minDuration); - Assert.Equal(15, maxDuration); - Assert.Equal(7, avgDuration); - } + Assert.Equal(expectedMinDuration, minDuration); + Assert.Equal(expectedMaxDuration, maxDuration); + Assert.Equal(expectedAvgDuration, avgDuration); + } - /// - /// Test for total rental time by bike type - /// - [Theory] - [InlineData(BikeType.Sport, 49)] - [InlineData(BikeType.Mountain, 16)] - [InlineData(BikeType.Road, 36)] - [InlineData(BikeType.Hybrid, 13)] - [InlineData(BikeType.Electric, 26)] - public void TotalRentalTimeByBikeType(BikeType bikeType, int expectedTotalTime) - { - var actualTotalTime = _fixture.Rents - .Where(r => r.Bike.Model.Type == bikeType) - .Sum(r => r.DurationHours); + /// + /// Test for total rental time by bike type + /// + [Theory] + [InlineData(BikeType.Sport, 49)] + [InlineData(BikeType.Mountain, 16)] + [InlineData(BikeType.Road, 36)] + [InlineData(BikeType.Hybrid, 13)] + [InlineData(BikeType.Electric, 26)] + public void TotalRentalTimeByBikeType(BikeType bikeType, int expectedTotalTime) + { + var actualTotalTime = fixture.Rents + .Where(r => r.Bike.Model.Type == bikeType) + .Sum(r => r.DurationHours); - Assert.Equal(expectedTotalTime, actualTotalTime); - } + Assert.Equal(expectedTotalTime, actualTotalTime); + } - /// - /// Test for top renters by rental count - /// - [Fact] - public void TopRentersByRentalCount() - { - var topRenters = _fixture.Rents - .GroupBy(r => r.Renter) - .Select(g => new - { - Renter = g.Key, - RentalCount = g.Count() - }) - .OrderByDescending(x => x.RentalCount) - .Take(5) - .ToList(); - - Assert.NotNull(topRenters); - Assert.True(topRenters.Count <= 5); - - if (topRenters.Count > 0) + /// + /// Test for top renters by rental count + /// + [Fact] + public void TopRentersByRentalCount() + { + var topRenters = fixture.Rents + .GroupBy(r => r.Renter) + .Select(g => new { - var maxRentalCount = topRenters.Max(x => x.RentalCount); - Assert.True(topRenters[0].RentalCount == maxRentalCount); - } + Renter = g.Key, + RentalCount = g.Count() + }) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToList(); + + var expectedCount = 5; + var expectedTopRenterName = "Ivanov Ivan"; + var expectedMaxRentalCount = 2; + + Assert.NotNull(topRenters); + Assert.True(topRenters.Count <= expectedCount); + Assert.Equal(expectedCount, topRenters.Count); + Assert.Equal(expectedTopRenterName, topRenters[0].Renter.FullName); + Assert.Equal(expectedMaxRentalCount, topRenters[0].RentalCount); + + if (topRenters.Count > 0) + { + var maxRentalCount = topRenters.Max(x => x.RentalCount); + Assert.True(topRenters[0].RentalCount == maxRentalCount); } } } \ No newline at end of file From 640539aade154460b74ac1683184faa6950a370a Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 30 Oct 2025 06:15:31 +0400 Subject: [PATCH 07/19] Add contracts --- .../Analytics/AnalyticsDto.cs | 73 +++++++++++++++++++ .../Analytics/IAnalyticsService.cs | 40 ++++++++++ .../Bikes.Application.Contracts.csproj | 9 +++ .../Bikes/BikeCreateUpdateDto.cs | 22 ++++++ Bikes.Application.Contracts/Bikes/BikeDto.cs | 32 ++++++++ .../Bikes/IBikeService.cs | 34 +++++++++ .../IApplicationService.cs | 8 ++ .../Models/BikeModelCreateUpdateDto.cs | 47 ++++++++++++ .../Models/BikeModelDto.cs | 52 +++++++++++++ .../Models/IBikeModelService.cs | 34 +++++++++ .../Renters/IRenterService.cs | 34 +++++++++ .../Renters/RenterCreateUpdateDto.cs | 17 +++++ .../Renters/RenterDto.cs | 22 ++++++ .../Rents/IRentService.cs | 34 +++++++++ .../Rents/RentCreateUpdateDto.cs | 27 +++++++ Bikes.Application.Contracts/Rents/RentDto.cs | 37 ++++++++++ lab1.sln | 21 ++++++ 17 files changed, 543 insertions(+) create mode 100644 Bikes.Application.Contracts/Analytics/AnalyticsDto.cs create mode 100644 Bikes.Application.Contracts/Analytics/IAnalyticsService.cs create mode 100644 Bikes.Application.Contracts/Bikes.Application.Contracts.csproj create mode 100644 Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs create mode 100644 Bikes.Application.Contracts/Bikes/BikeDto.cs create mode 100644 Bikes.Application.Contracts/Bikes/IBikeService.cs create mode 100644 Bikes.Application.Contracts/IApplicationService.cs create mode 100644 Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs create mode 100644 Bikes.Application.Contracts/Models/BikeModelDto.cs create mode 100644 Bikes.Application.Contracts/Models/IBikeModelService.cs create mode 100644 Bikes.Application.Contracts/Renters/IRenterService.cs create mode 100644 Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs create mode 100644 Bikes.Application.Contracts/Renters/RenterDto.cs create mode 100644 Bikes.Application.Contracts/Rents/IRentService.cs create mode 100644 Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs create mode 100644 Bikes.Application.Contracts/Rents/RentDto.cs diff --git a/Bikes.Application.Contracts/Analytics/AnalyticsDto.cs b/Bikes.Application.Contracts/Analytics/AnalyticsDto.cs new file mode 100644 index 000000000..9b76610c0 --- /dev/null +++ b/Bikes.Application.Contracts/Analytics/AnalyticsDto.cs @@ -0,0 +1,73 @@ +namespace Bikes.Application.Contracts.Analytics; + +/// +/// DTO for bike models analytics +/// +public class BikeModelAnalyticsDto +{ + /// + /// Model identifier + /// + public int Id { get; set; } + + /// + /// Model name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Bike type + /// + public string Type { get; set; } = string.Empty; + + /// + /// Price per rental hour + /// + public decimal PricePerHour { get; set; } + + /// + /// Total profit + /// + public decimal TotalProfit { get; set; } + + /// + /// Total rental duration + /// + public int TotalDuration { get; set; } +} + +/// +/// DTO for renters analytics +/// +public class RenterAnalyticsDto +{ + /// + /// Renter full name + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Number of rentals + /// + public int RentalCount { get; set; } +} + +/// +/// Rental statistics +/// +public record RentalStatistics( + /// + /// Minimum rental duration + /// + int MinDuration, + + /// + /// Maximum rental duration + /// + int MaxDuration, + + /// + /// Average rental duration + /// + double AvgDuration +); \ No newline at end of file diff --git a/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs b/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs new file mode 100644 index 000000000..1c20ad0dc --- /dev/null +++ b/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs @@ -0,0 +1,40 @@ +using Bikes.Application.Contracts; +using Bikes.Application.Contracts.Bikes; + +namespace Bikes.Application.Contracts.Analytics; + +/// +/// Service for bike rental analytics +/// +public interface IAnalyticsService : IApplicationService +{ + /// + /// Get all sport bikes + /// + public List GetSportBikes(); + + /// + /// Get top 5 models by profit + /// + public List GetTop5ModelsByProfit(); + + /// + /// Get top 5 models by rental duration + /// + public List GetTop5ModelsByRentalDuration(); + + /// + /// Get rental statistics + /// + public RentalStatistics GetRentalStatistics(); + + /// + /// Get total rental time by bike type + /// + public Dictionary GetTotalRentalTimeByBikeType(); + + /// + /// Get top renters by rental count + /// + public List GetTopRentersByRentalCount(); +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj b/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs b/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs new file mode 100644 index 000000000..2a82cd49a --- /dev/null +++ b/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs @@ -0,0 +1,22 @@ +namespace Bikes.Application.Contracts.Bikes; + +/// +/// DTO for creating and updating bikes +/// +public class BikeCreateUpdateDto +{ + /// + /// Bike serial number + /// + public required string SerialNumber { get; set; } + + /// + /// Bike model identifier + /// + public required int ModelId { get; set; } + + /// + /// Bike color + /// + public required string Color { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Bikes/BikeDto.cs b/Bikes.Application.Contracts/Bikes/BikeDto.cs new file mode 100644 index 000000000..87da07606 --- /dev/null +++ b/Bikes.Application.Contracts/Bikes/BikeDto.cs @@ -0,0 +1,32 @@ +namespace Bikes.Application.Contracts.Bikes; + +/// +/// DTO for bike representation +/// +public class BikeDto +{ + /// + /// Bike identifier + /// + public int Id { get; set; } + + /// + /// Bike serial number + /// + public string SerialNumber { get; set; } = string.Empty; + + /// + /// Bike model identifier + /// + public int ModelId { get; set; } + + /// + /// Bike color + /// + public string Color { get; set; } = string.Empty; + + /// + /// Availability status for rental + /// + public bool IsAvailable { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Bikes/IBikeService.cs b/Bikes.Application.Contracts/Bikes/IBikeService.cs new file mode 100644 index 000000000..4510e276c --- /dev/null +++ b/Bikes.Application.Contracts/Bikes/IBikeService.cs @@ -0,0 +1,34 @@ +using Bikes.Application.Contracts; + +namespace Bikes.Application.Contracts.Bikes; + +/// +/// Service for bike management +/// +public interface IBikeService : IApplicationService +{ + /// + /// Get all bikes + /// + public List GetAllBikes(); + + /// + /// Get bike by identifier + /// + public BikeDto? GetBikeById(int id); + + /// + /// Create new bike + /// + public BikeDto CreateBike(BikeCreateUpdateDto request); + + /// + /// Update bike + /// + public BikeDto? UpdateBike(int id, BikeCreateUpdateDto request); + + /// + /// Delete bike + /// + public bool DeleteBike(int id); +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/IApplicationService.cs b/Bikes.Application.Contracts/IApplicationService.cs new file mode 100644 index 000000000..9a9107bca --- /dev/null +++ b/Bikes.Application.Contracts/IApplicationService.cs @@ -0,0 +1,8 @@ +namespace Bikes.Application.Contracts; + +/// +/// Base interface for all application services +/// +public interface IApplicationService +{ +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs b/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs new file mode 100644 index 000000000..3caebfc4c --- /dev/null +++ b/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs @@ -0,0 +1,47 @@ +namespace Bikes.Application.Contracts.Models; + +/// +/// DTO for creating and updating bike models +/// +public class BikeModelCreateUpdateDto +{ + /// + /// Model name + /// + public required string Name { get; set; } + + /// + /// Bike type + /// + public required string Type { get; set; } + + /// + /// Wheel size in inches + /// + public required decimal WheelSize { get; set; } + + /// + /// Maximum weight capacity in kg + /// + public required decimal MaxWeight { get; set; } + + /// + /// Bike weight in kg + /// + public required decimal Weight { get; set; } + + /// + /// Brake type + /// + public required string BrakeType { get; set; } + + /// + /// Model year + /// + public required int ModelYear { get; set; } + + /// + /// Price per rental hour + /// + public required decimal PricePerHour { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Models/BikeModelDto.cs b/Bikes.Application.Contracts/Models/BikeModelDto.cs new file mode 100644 index 000000000..82a96c08a --- /dev/null +++ b/Bikes.Application.Contracts/Models/BikeModelDto.cs @@ -0,0 +1,52 @@ +namespace Bikes.Application.Contracts.Models; + +/// +/// DTO for bike model representation +/// +public class BikeModelDto +{ + /// + /// Model identifier + /// + public int Id { get; set; } + + /// + /// Model name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Bike type + /// + public string Type { get; set; } = string.Empty; + + /// + /// Wheel size in inches + /// + public decimal WheelSize { get; set; } + + /// + /// Maximum weight capacity in kg + /// + public decimal MaxWeight { get; set; } + + /// + /// Bike weight in kg + /// + public decimal Weight { get; set; } + + /// + /// Brake type + /// + public string BrakeType { get; set; } = string.Empty; + + /// + /// Model year + /// + public int ModelYear { get; set; } + + /// + /// Price per rental hour + /// + public decimal PricePerHour { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Models/IBikeModelService.cs b/Bikes.Application.Contracts/Models/IBikeModelService.cs new file mode 100644 index 000000000..cbd3d0dfa --- /dev/null +++ b/Bikes.Application.Contracts/Models/IBikeModelService.cs @@ -0,0 +1,34 @@ +using Bikes.Application.Contracts; + +namespace Bikes.Application.Contracts.Models; + +/// +/// Service for bike model management +/// +public interface IBikeModelService : IApplicationService +{ + /// + /// Get all bike models + /// + public List GetAllModels(); + + /// + /// Get bike model by identifier + /// + public BikeModelDto? GetModelById(int id); + + /// + /// Create new bike model + /// + public BikeModelDto CreateModel(BikeModelCreateUpdateDto request); + + /// + /// Update bike model + /// + public BikeModelDto? UpdateModel(int id, BikeModelCreateUpdateDto request); + + /// + /// Delete bike model + /// + public bool DeleteModel(int id); +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Renters/IRenterService.cs b/Bikes.Application.Contracts/Renters/IRenterService.cs new file mode 100644 index 000000000..9734afe65 --- /dev/null +++ b/Bikes.Application.Contracts/Renters/IRenterService.cs @@ -0,0 +1,34 @@ +using Bikes.Application.Contracts; + +namespace Bikes.Application.Contracts.Renters; + +/// +/// Service for renter management +/// +public interface IRenterService : IApplicationService +{ + /// + /// Get all renters + /// + public List GetAllRenters(); + + /// + /// Get renter by identifier + /// + public RenterDto? GetRenterById(int id); + + /// + /// Create new renter + /// + public RenterDto CreateRenter(RenterCreateUpdateDto request); + + /// + /// Update renter + /// + public RenterDto? UpdateRenter(int id, RenterCreateUpdateDto request); + + /// + /// Delete renter + /// + public bool DeleteRenter(int id); +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs b/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs new file mode 100644 index 000000000..ac3dc7b53 --- /dev/null +++ b/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs @@ -0,0 +1,17 @@ +namespace Bikes.Application.Contracts.Renters; + +/// +/// DTO for creating and updating renters +/// +public class RenterCreateUpdateDto +{ + /// + /// Renter full name + /// + public required string FullName { get; set; } + + /// + /// Contact phone number + /// + public required string Phone { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Renters/RenterDto.cs b/Bikes.Application.Contracts/Renters/RenterDto.cs new file mode 100644 index 000000000..8d97e66cd --- /dev/null +++ b/Bikes.Application.Contracts/Renters/RenterDto.cs @@ -0,0 +1,22 @@ +namespace Bikes.Application.Contracts.Renters; + +/// +/// DTO for renter representation +/// +public class RenterDto +{ + /// + /// Renter identifier + /// + public int Id { get; set; } + + /// + /// Renter full name + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Contact phone number + /// + public string Phone { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/IRentService.cs b/Bikes.Application.Contracts/Rents/IRentService.cs new file mode 100644 index 000000000..fc2d5e3bb --- /dev/null +++ b/Bikes.Application.Contracts/Rents/IRentService.cs @@ -0,0 +1,34 @@ +using Bikes.Application.Contracts; + +namespace Bikes.Application.Contracts.Rents; + +/// +/// Service for rent management +/// +public interface IRentService : IApplicationService +{ + /// + /// Get all rents + /// + public List GetAllRents(); + + /// + /// Get rent by identifier + /// + public RentDto? GetRentById(int id); + + /// + /// Create new rent + /// + public RentDto CreateRent(RentCreateUpdateDto request); + + /// + /// Update rent + /// + public RentDto? UpdateRent(int id, RentCreateUpdateDto request); + + /// + /// Delete rent + /// + public bool DeleteRent(int id); +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs b/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs new file mode 100644 index 000000000..0eebcd51e --- /dev/null +++ b/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs @@ -0,0 +1,27 @@ +namespace Bikes.Application.Contracts.Rents; + +/// +/// DTO for creating and updating rents +/// +public class RentCreateUpdateDto +{ + /// + /// Bike identifier + /// + public required int BikeId { get; set; } + + /// + /// Renter identifier + /// + public required int RenterId { get; set; } + + /// + /// Rental start time + /// + public required DateTime StartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int DurationHours { get; set; } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/RentDto.cs b/Bikes.Application.Contracts/Rents/RentDto.cs new file mode 100644 index 000000000..529b0df6f --- /dev/null +++ b/Bikes.Application.Contracts/Rents/RentDto.cs @@ -0,0 +1,37 @@ +namespace Bikes.Application.Contracts.Rents; + +/// +/// DTO for rent representation +/// +public class RentDto +{ + /// + /// Rent identifier + /// + public int Id { get; set; } + + /// + /// Rented bike identifier + /// + public required int BikeId { get; set; } + + /// + /// Renter identifier + /// + public required int RenterId { get; set; } + + /// + /// Rental start time + /// + public required DateTime StartTime { get; set; } + + /// + /// Rental duration in hours + /// + public required int DurationHours { get; set; } + + /// + /// Total rental cost + /// + public required decimal TotalCost { get; set; } +} \ No newline at end of file diff --git a/lab1.sln b/lab1.sln index 104bc0d89..b2b4a9bd9 100644 --- a/lab1.sln +++ b/lab1.sln @@ -6,6 +6,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Domain", "Bikes.Domai EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application.Contracts", "Bikes.Application.Contracts\Bikes.Application.Contracts.csproj", "{375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes.Application\Bikes.Application.csproj", "{221D85D1-A79D-4C32-BA01-E781961721A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api.Host\Bikes.Api.Host.csproj", "{26BE026D-95E7-4C62-A832-7A7A9B6A7D48}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,8 +26,23 @@ Global {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.Build.0 = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.Build.0 = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5ABC640F-2193-41DE-939D-657478D0E14B} + EndGlobalSection EndGlobal From e1aaf6c8afd4bab9d82b78b21830f8b1366d2a9a Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 30 Oct 2025 06:17:35 +0400 Subject: [PATCH 08/19] Add services --- Bikes.Application/Bikes.Application.csproj | 15 ++ .../Services/AnalyticsService.cs | 123 ++++++++++++++++ .../Services/BikeModelService.cs | 132 ++++++++++++++++++ Bikes.Application/Services/BikeService.cs | 115 +++++++++++++++ Bikes.Application/Services/IBikeRepository.cs | 49 +++++++ .../Services/InMemoryBikeRepository.cs | 74 ++++++++++ Bikes.Application/Services/RentService.cs | 128 +++++++++++++++++ Bikes.Application/Services/RenterService.cs | 95 +++++++++++++ 8 files changed, 731 insertions(+) create mode 100644 Bikes.Application/Bikes.Application.csproj create mode 100644 Bikes.Application/Services/AnalyticsService.cs create mode 100644 Bikes.Application/Services/BikeModelService.cs create mode 100644 Bikes.Application/Services/BikeService.cs create mode 100644 Bikes.Application/Services/IBikeRepository.cs create mode 100644 Bikes.Application/Services/InMemoryBikeRepository.cs create mode 100644 Bikes.Application/Services/RentService.cs create mode 100644 Bikes.Application/Services/RenterService.cs diff --git a/Bikes.Application/Bikes.Application.csproj b/Bikes.Application/Bikes.Application.csproj new file mode 100644 index 000000000..15b06c758 --- /dev/null +++ b/Bikes.Application/Bikes.Application.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/Bikes.Application/Services/AnalyticsService.cs b/Bikes.Application/Services/AnalyticsService.cs new file mode 100644 index 000000000..6f28f6601 --- /dev/null +++ b/Bikes.Application/Services/AnalyticsService.cs @@ -0,0 +1,123 @@ +using Bikes.Application.Contracts.Analytics; +using Bikes.Application.Contracts.Bikes; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Implementation of analytics service +/// +public class AnalyticsService(IBikeRepository repository) : IAnalyticsService +{ + private readonly IBikeRepository _repository = repository; + + /// + /// Get all sport bikes + /// + public List GetSportBikes() + { + return _repository.GetAllBikes() + .Where(b => b.Model.Type == BikeType.Sport) + .Select(b => new BikeDto + { + Id = b.Id, + SerialNumber = b.SerialNumber, + ModelId = b.Model.Id, + Color = b.Color, + IsAvailable = b.IsAvailable + }) + .ToList(); + } + + /// + /// Get top 5 models by profit + /// + public List GetTop5ModelsByProfit() + { + var rents = _repository.GetAllRents(); + + return rents + .GroupBy(r => r.Bike.Model) + .Select(g => new BikeModelAnalyticsDto + { + Id = g.Key.Id, + Name = g.Key.Name, + Type = g.Key.Type.ToString(), + PricePerHour = g.Key.PricePerHour, + TotalProfit = g.Sum(r => r.TotalCost), + TotalDuration = g.Sum(r => r.DurationHours) + }) + .OrderByDescending(x => x.TotalProfit) + .Take(5) + .ToList(); + } + + /// + /// Get top 5 models by rental duration + /// + public List GetTop5ModelsByRentalDuration() + { + var rents = _repository.GetAllRents(); + + return rents + .GroupBy(r => r.Bike.Model) + .Select(g => new BikeModelAnalyticsDto + { + Id = g.Key.Id, + Name = g.Key.Name, + Type = g.Key.Type.ToString(), + PricePerHour = g.Key.PricePerHour, + TotalProfit = g.Sum(r => r.TotalCost), + TotalDuration = g.Sum(r => r.DurationHours) + }) + .OrderByDescending(x => x.TotalDuration) + .Take(5) + .ToList(); + } + + /// + /// Get rental statistics + /// + public RentalStatistics GetRentalStatistics() + { + var durations = _repository.GetAllRents() + .Select(r => r.DurationHours) + .ToList(); + + return new RentalStatistics( + MinDuration: durations.Min(), + MaxDuration: durations.Max(), + AvgDuration: durations.Average() + ); + } + + /// + /// Get total rental time by bike type + /// + public Dictionary GetTotalRentalTimeByBikeType() + { + return _repository.GetAllRents() + .GroupBy(r => r.Bike.Model.Type) + .ToDictionary( + g => g.Key.ToString(), + g => g.Sum(r => r.DurationHours) + ); + } + + /// + /// Get top renters by rental count + /// + public List GetTopRentersByRentalCount() + { + return _repository.GetAllRents() + .GroupBy(r => r.Renter) + .Select(g => new RenterAnalyticsDto + { + FullName = g.Key.FullName, + RentalCount = g.Count() + }) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToList(); + } +} \ No newline at end of file diff --git a/Bikes.Application/Services/BikeModelService.cs b/Bikes.Application/Services/BikeModelService.cs new file mode 100644 index 000000000..728e1abaf --- /dev/null +++ b/Bikes.Application/Services/BikeModelService.cs @@ -0,0 +1,132 @@ +using Bikes.Application.Contracts.Models; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Implementation of bike model service +/// +public class BikeModelService(IBikeRepository repository) : IBikeModelService +{ + private readonly IBikeRepository _repository = repository; + + /// + /// Get all bike models + /// + public List GetAllModels() + { + return _repository.GetAllModels().Select(m => new BikeModelDto + { + Id = m.Id, + Name = m.Name, + Type = m.Type.ToString(), + WheelSize = m.WheelSize, + MaxWeight = m.MaxWeight, + Weight = m.Weight, + BrakeType = m.BrakeType, + ModelYear = m.ModelYear, + PricePerHour = m.PricePerHour + }).ToList(); + } + + /// + /// Get bike model by identifier + /// + public BikeModelDto? GetModelById(int id) + { + var model = _repository.GetAllModels().FirstOrDefault(m => m.Id == id); + return model == null ? null : new BikeModelDto + { + Id = model.Id, + Name = model.Name, + Type = model.Type.ToString(), + WheelSize = model.WheelSize, + MaxWeight = model.MaxWeight, + Weight = model.Weight, + BrakeType = model.BrakeType, + ModelYear = model.ModelYear, + PricePerHour = model.PricePerHour + }; + } + + /// + /// Create new bike model + /// + public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) + { + if (!Enum.TryParse(request.Type, out var bikeType)) + throw new InvalidOperationException($"Invalid bike type: {request.Type}"); + + var models = _repository.GetAllModels(); + var newModel = new BikeModel + { + Id = models.Max(m => m.Id) + 1, + Name = request.Name, + Type = bikeType, + WheelSize = request.WheelSize, + MaxWeight = request.MaxWeight, + Weight = request.Weight, + BrakeType = request.BrakeType, + ModelYear = request.ModelYear, + PricePerHour = request.PricePerHour + }; + + return new BikeModelDto + { + Id = newModel.Id, + Name = newModel.Name, + Type = newModel.Type.ToString(), + WheelSize = newModel.WheelSize, + MaxWeight = newModel.MaxWeight, + Weight = newModel.Weight, + BrakeType = newModel.BrakeType, + ModelYear = newModel.ModelYear, + PricePerHour = newModel.PricePerHour + }; + } + + /// + /// Update bike model + /// + public BikeModelDto? UpdateModel(int id, BikeModelCreateUpdateDto request) + { + if (!Enum.TryParse(request.Type, out var bikeType)) + throw new InvalidOperationException($"Invalid bike type: {request.Type}"); + + var models = _repository.GetAllModels(); + var model = models.FirstOrDefault(m => m.Id == id); + if (model == null) return null; + + model.Name = request.Name; + model.Type = bikeType; + model.WheelSize = request.WheelSize; + model.MaxWeight = request.MaxWeight; + model.Weight = request.Weight; + model.BrakeType = request.BrakeType; + model.ModelYear = request.ModelYear; + model.PricePerHour = request.PricePerHour; + + return new BikeModelDto + { + Id = model.Id, + Name = model.Name, + Type = model.Type.ToString(), + WheelSize = model.WheelSize, + MaxWeight = model.MaxWeight, + Weight = model.Weight, + BrakeType = model.BrakeType, + ModelYear = model.ModelYear, + PricePerHour = model.PricePerHour + }; + } + + /// + /// Delete bike model + /// + public bool DeleteModel(int id) + { + var models = _repository.GetAllModels(); + var model = models.FirstOrDefault(m => m.Id == id); + return model != null; + } +} \ No newline at end of file diff --git a/Bikes.Application/Services/BikeService.cs b/Bikes.Application/Services/BikeService.cs new file mode 100644 index 000000000..f28d0964a --- /dev/null +++ b/Bikes.Application/Services/BikeService.cs @@ -0,0 +1,115 @@ +using Bikes.Application.Contracts.Bikes; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Implementation of bike service +/// +public class BikeService(IBikeRepository repository) : IBikeService +{ + private readonly IBikeRepository _repository = repository; + + /// + /// Get all bikes + /// + public List GetAllBikes() + { + return _repository.GetAllBikes().Select(b => new BikeDto + { + Id = b.Id, + SerialNumber = b.SerialNumber, + ModelId = b.Model.Id, + Color = b.Color, + IsAvailable = b.IsAvailable + }).ToList(); + } + + /// + /// Get bike by identifier + /// + public BikeDto? GetBikeById(int id) + { + var bike = _repository.GetBikeById(id); + return bike == null ? null : new BikeDto + { + Id = bike.Id, + SerialNumber = bike.SerialNumber, + ModelId = bike.Model.Id, + Color = bike.Color, + IsAvailable = bike.IsAvailable + }; + } + + /// + /// Create new bike + /// + public BikeDto CreateBike(BikeCreateUpdateDto request) + { + var models = _repository.GetAllModels(); + var model = models.FirstOrDefault(m => m.Id == request.ModelId); + if (model == null) + throw new InvalidOperationException("Model not found"); + + var newBike = new Bike + { + Id = _repository.GetAllBikes().Max(b => b.Id) + 1, + SerialNumber = request.SerialNumber, + Model = model, + Color = request.Color, + IsAvailable = true + }; + + _repository.AddBike(newBike); + + return new BikeDto + { + Id = newBike.Id, + SerialNumber = newBike.SerialNumber, + ModelId = newBike.Model.Id, + Color = newBike.Color, + IsAvailable = newBike.IsAvailable + }; + } + + /// + /// Update bike + /// + public BikeDto? UpdateBike(int id, BikeCreateUpdateDto request) + { + var bike = _repository.GetBikeById(id); + if (bike == null) return null; + + var models = _repository.GetAllModels(); + var model = models.FirstOrDefault(m => m.Id == request.ModelId); + if (model == null) + throw new InvalidOperationException("Model not found"); + + bike.SerialNumber = request.SerialNumber; + bike.Model = model; + bike.Color = request.Color; + + _repository.UpdateBike(bike); + + return new BikeDto + { + Id = bike.Id, + SerialNumber = bike.SerialNumber, + ModelId = bike.Model.Id, + Color = bike.Color, + IsAvailable = bike.IsAvailable + }; + } + + /// + /// Delete bike + /// + public bool DeleteBike(int id) + { + var bike = _repository.GetBikeById(id); + if (bike == null) return false; + + _repository.DeleteBike(id); + return true; + } +} \ No newline at end of file diff --git a/Bikes.Application/Services/IBikeRepository.cs b/Bikes.Application/Services/IBikeRepository.cs new file mode 100644 index 000000000..92dfaf020 --- /dev/null +++ b/Bikes.Application/Services/IBikeRepository.cs @@ -0,0 +1,49 @@ +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Repository for bike data access +/// +public interface IBikeRepository +{ + /// + /// Get all bikes + /// + public List GetAllBikes(); + + /// + /// Get bike by identifier + /// + public Bike? GetBikeById(int id); + + /// + /// Add new bike + /// + public void AddBike(Bike bike); + + /// + /// Update bike + /// + public void UpdateBike(Bike bike); + + /// + /// Delete bike by identifier + /// + public void DeleteBike(int id); + + /// + /// Get all bike models + /// + public List GetAllModels(); + + /// + /// Get all rental records + /// + public List GetAllRents(); + + /// + /// Get all renters + /// + public List GetAllRenters(); +} \ No newline at end of file diff --git a/Bikes.Application/Services/InMemoryBikeRepository.cs b/Bikes.Application/Services/InMemoryBikeRepository.cs new file mode 100644 index 000000000..b9cc52604 --- /dev/null +++ b/Bikes.Application/Services/InMemoryBikeRepository.cs @@ -0,0 +1,74 @@ +using Bikes.Domain.Models; +using Bikes.Tests; + +namespace Bikes.Application.Services; + +/// +/// In-memory implementation of bike repository +/// +public class InMemoryBikeRepository() : IBikeRepository +{ + private readonly BikesFixture _fixture = new(); + + private readonly List _bikes = [.. new BikesFixture().Bikes]; + private readonly List _models = [.. new BikesFixture().Models]; + private readonly List _rents = [.. new BikesFixture().Rents]; + private readonly List _renters = [.. new BikesFixture().Renters]; + + /// + /// Get all bikes + /// + public List GetAllBikes() => [.. _bikes]; + + /// + /// Get bike by identifier + /// + public Bike? GetBikeById(int id) => _bikes.FirstOrDefault(b => b.Id == id); + + /// + /// Add new bike + /// + public void AddBike(Bike bike) + { + if (bike == null) + throw new ArgumentNullException(nameof(bike)); + + _bikes.Add(bike); + } + + /// + /// Update bike + /// + public void UpdateBike(Bike bike) + { + if (bike == null) + throw new ArgumentNullException(nameof(bike)); + + var existingBike = _bikes.FirstOrDefault(b => b.Id == bike.Id); + if (existingBike != null) + { + _bikes.Remove(existingBike); + _bikes.Add(bike); + } + } + + /// + /// Delete bike by identifier + /// + public void DeleteBike(int id) => _bikes.RemoveAll(b => b.Id == id); + + /// + /// Get all bike models + /// + public List GetAllModels() => [.. _models]; + + /// + /// Get all rental records + /// + public List GetAllRents() => [.. _rents]; + + /// + /// Get all renters + /// + public List GetAllRenters() => [.. _renters]; +} \ No newline at end of file diff --git a/Bikes.Application/Services/RentService.cs b/Bikes.Application/Services/RentService.cs new file mode 100644 index 000000000..a85953fd1 --- /dev/null +++ b/Bikes.Application/Services/RentService.cs @@ -0,0 +1,128 @@ +using Bikes.Application.Contracts.Rents; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Implementation of rent service +/// +public class RentService(IBikeRepository repository) : IRentService +{ + private readonly IBikeRepository _repository = repository; + + /// + /// Get all rents + /// + public List GetAllRents() + { + return _repository.GetAllRents().Select(r => new RentDto + { + Id = r.Id, + BikeId = r.Bike.Id, + RenterId = r.Renter.Id, + StartTime = r.StartTime, + DurationHours = r.DurationHours, + TotalCost = r.TotalCost + }).ToList(); + } + + /// + /// Get rent by identifier + /// + public RentDto? GetRentById(int id) + { + var rent = _repository.GetAllRents().FirstOrDefault(r => r.Id == id); + return rent == null ? null : new RentDto + { + Id = rent.Id, + BikeId = rent.Bike.Id, + RenterId = rent.Renter.Id, + StartTime = rent.StartTime, + DurationHours = rent.DurationHours, + TotalCost = rent.TotalCost + }; + } + + /// + /// Create new rent + /// + public RentDto CreateRent(RentCreateUpdateDto request) + { + var bikes = _repository.GetAllBikes(); + var renters = _repository.GetAllRenters(); + var rents = _repository.GetAllRents(); + + var bike = bikes.FirstOrDefault(b => b.Id == request.BikeId); + var renter = renters.FirstOrDefault(r => r.Id == request.RenterId); + + if (bike == null) + throw new InvalidOperationException("Bike not found"); + if (renter == null) + throw new InvalidOperationException("Renter not found"); + + var newRent = new Rent + { + Id = rents.Max(r => r.Id) + 1, + Bike = bike, + Renter = renter, + StartTime = request.StartTime, + DurationHours = request.DurationHours + }; + + return new RentDto + { + Id = newRent.Id, + BikeId = newRent.Bike.Id, + RenterId = newRent.Renter.Id, + StartTime = newRent.StartTime, + DurationHours = newRent.DurationHours, + TotalCost = newRent.TotalCost + }; + } + + /// + /// Update rent + /// + public RentDto? UpdateRent(int id, RentCreateUpdateDto request) + { + var rents = _repository.GetAllRents(); + var rent = rents.FirstOrDefault(r => r.Id == id); + if (rent == null) return null; + + var bikes = _repository.GetAllBikes(); + var renters = _repository.GetAllRenters(); + + var bike = bikes.FirstOrDefault(b => b.Id == request.BikeId); + var renter = renters.FirstOrDefault(r => r.Id == request.RenterId); + + if (bike == null) + throw new InvalidOperationException("Bike not found"); + if (renter == null) + throw new InvalidOperationException("Renter not found"); + + rent.Bike = bike; + rent.Renter = renter; + rent.StartTime = request.StartTime; + rent.DurationHours = request.DurationHours; + + return new RentDto + { + Id = rent.Id, + BikeId = rent.Bike.Id, + RenterId = rent.Renter.Id, + StartTime = rent.StartTime, + DurationHours = rent.DurationHours, + TotalCost = rent.TotalCost + }; + } + + /// + /// Delete rent + /// + public bool DeleteRent(int id) + { + var rents = _repository.GetAllRents(); + var rent = rents.FirstOrDefault(r => r.Id == id); + return rent != null; + } +} \ No newline at end of file diff --git a/Bikes.Application/Services/RenterService.cs b/Bikes.Application/Services/RenterService.cs new file mode 100644 index 000000000..514621d17 --- /dev/null +++ b/Bikes.Application/Services/RenterService.cs @@ -0,0 +1,95 @@ +using Bikes.Application.Contracts.Renters; +using Bikes.Domain.Models; + +namespace Bikes.Application.Services; + +/// +/// Implementation of renter service +/// +public class RenterService(IBikeRepository repository) : IRenterService +{ + private readonly IBikeRepository _repository = repository; + + /// + /// Get all renters + /// + public List GetAllRenters() + { + return _repository.GetAllRenters().Select(r => new RenterDto + { + Id = r.Id, + FullName = r.FullName, + Phone = r.Phone + }).ToList(); + } + + /// + /// Get renter by identifier + /// + public RenterDto? GetRenterById(int id) + { + var renter = _repository.GetAllRenters().FirstOrDefault(r => r.Id == id); + return renter == null ? null : new RenterDto + { + Id = renter.Id, + FullName = renter.FullName, + Phone = renter.Phone + }; + } + + /// + /// Create new renter + /// + public RenterDto CreateRenter(RenterCreateUpdateDto request) + { + var renters = _repository.GetAllRenters(); + var newRenter = new Renter + { + Id = renters.Max(r => r.Id) + 1, + FullName = request.FullName, + Phone = request.Phone + }; + + // Note: In real application, we would add to repository + // _repository.AddRenter(newRenter); + + return new RenterDto + { + Id = newRenter.Id, + FullName = newRenter.FullName, + Phone = newRenter.Phone + }; + } + + /// + /// Update renter + /// + public RenterDto? UpdateRenter(int id, RenterCreateUpdateDto request) + { + var renters = _repository.GetAllRenters(); + var renter = renters.FirstOrDefault(r => r.Id == id); + if (renter == null) return null; + + renter.FullName = request.FullName; + renter.Phone = request.Phone; + + return new RenterDto + { + Id = renter.Id, + FullName = renter.FullName, + Phone = renter.Phone + }; + } + + /// + /// Delete renter + /// + public bool DeleteRenter(int id) + { + var renters = _repository.GetAllRenters(); + var renter = renters.FirstOrDefault(r => r.Id == id); + return renter != null; + // Note: In real application, we would remove from repository + // return _repository.DeleteRenter(id); + } +} \ No newline at end of file From 2de9a45be9702356cc63313d0510de4e44bf1341 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 30 Oct 2025 06:18:33 +0400 Subject: [PATCH 09/19] Add api.host --- Bikes.Api.Host/Bikes.Api.Host.csproj | 18 +++++ Bikes.Api.Host/Bikes.Api.Host.http | 6 ++ .../Controllers/AnalyticsController.cs | 75 +++++++++++++++++++ .../Controllers/BikeModelsController.cs | 65 ++++++++++++++++ Bikes.Api.Host/Controllers/BikesController.cs | 65 ++++++++++++++++ .../Controllers/CrudControllerBase.cs | 45 +++++++++++ .../Controllers/RentersController.cs | 65 ++++++++++++++++ Bikes.Api.Host/Controllers/RentsController.cs | 65 ++++++++++++++++ Bikes.Api.Host/Program.cs | 36 +++++++++ Bikes.Api.Host/Properties/launchSettings.json | 41 ++++++++++ Bikes.Api.Host/appsettings.Development.json | 8 ++ Bikes.Api.Host/appsettings.json | 9 +++ 12 files changed, 498 insertions(+) create mode 100644 Bikes.Api.Host/Bikes.Api.Host.csproj create mode 100644 Bikes.Api.Host/Bikes.Api.Host.http create mode 100644 Bikes.Api.Host/Controllers/AnalyticsController.cs create mode 100644 Bikes.Api.Host/Controllers/BikeModelsController.cs create mode 100644 Bikes.Api.Host/Controllers/BikesController.cs create mode 100644 Bikes.Api.Host/Controllers/CrudControllerBase.cs create mode 100644 Bikes.Api.Host/Controllers/RentersController.cs create mode 100644 Bikes.Api.Host/Controllers/RentsController.cs create mode 100644 Bikes.Api.Host/Program.cs create mode 100644 Bikes.Api.Host/Properties/launchSettings.json create mode 100644 Bikes.Api.Host/appsettings.Development.json create mode 100644 Bikes.Api.Host/appsettings.json diff --git a/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes.Api.Host/Bikes.Api.Host.csproj new file mode 100644 index 000000000..614b5d408 --- /dev/null +++ b/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Bikes.Api.Host/Bikes.Api.Host.http b/Bikes.Api.Host/Bikes.Api.Host.http new file mode 100644 index 000000000..00666aa4a --- /dev/null +++ b/Bikes.Api.Host/Bikes.Api.Host.http @@ -0,0 +1,6 @@ +@Bikes.Api.Host_HostAddress = http://localhost:5145 + +GET {{Bikes.Api.Host_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes.Api.Host/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..f1af8d61e --- /dev/null +++ b/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -0,0 +1,75 @@ +using Bikes.Application.Contracts.Analytics; +using Bikes.Application.Contracts.Bikes; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Controller for rental analytics +/// +[ApiController] +[Route("api/[controller]")] +public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase +{ + private readonly IAnalyticsService _analyticsService = analyticsService; + + /// + /// Get all sport bikes + /// + [HttpGet("sport-bikes")] + public ActionResult> GetSportBikes() + { + var sportBikes = _analyticsService.GetSportBikes(); + return Ok(sportBikes); + } + + /// + /// Get top 5 models by profit + /// + [HttpGet("top-models-by-profit")] + public ActionResult> GetTopModelsByProfit() + { + var topModels = _analyticsService.GetTop5ModelsByProfit(); + return Ok(topModels); + } + + /// + /// Get top 5 models by rental duration + /// + [HttpGet("top-models-by-duration")] + public ActionResult> GetTopModelsByDuration() + { + var topModels = _analyticsService.GetTop5ModelsByRentalDuration(); + return Ok(topModels); + } + + /// + /// Get rental statistics + /// + [HttpGet("rental-statistics")] + public ActionResult GetRentalStatistics() + { + var statistics = _analyticsService.GetRentalStatistics(); + return Ok(statistics); + } + + /// + /// Get total rental time by bike type + /// + [HttpGet("rental-time-by-type")] + public ActionResult> GetRentalTimeByType() + { + var rentalTime = _analyticsService.GetTotalRentalTimeByBikeType(); + return Ok(rentalTime); + } + + /// + /// Get top renters by rental count + /// + [HttpGet("top-renters")] + public ActionResult> GetTopRenters() + { + var topRenters = _analyticsService.GetTopRentersByRentalCount(); + return Ok(topRenters); + } +} \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes.Api.Host/Controllers/BikeModelsController.cs new file mode 100644 index 000000000..3a97ec460 --- /dev/null +++ b/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -0,0 +1,65 @@ +using Bikes.Application.Contracts.Models; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Controller for bike model management +/// +[ApiController] +[Route("api/[controller]")] +public class BikeModelsController(IBikeModelService bikeModelService) : CrudControllerBase +{ + private readonly IBikeModelService _bikeModelService = bikeModelService; + + /// + /// Get all bike models + /// + [HttpGet] + public override Task>> GetAll() + { + var models = _bikeModelService.GetAllModels(); + return Task.FromResult>>(Ok(models)); + } + + /// + /// Get bike model by id + /// + [HttpGet("{id}")] + public override Task> GetById(int id) + { + var model = _bikeModelService.GetModelById(id); + return Task.FromResult>(model == null ? NotFound() : Ok(model)); + } + + /// + /// Create new bike model + /// + [HttpPost] + public override Task> Create([FromBody] BikeModelCreateUpdateDto request) + { + var model = _bikeModelService.CreateModel(request); + return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = model.Id }, model)); + } + + /// + /// Update bike model + /// + [HttpPut("{id}")] + public override Task> Update(int id, [FromBody] BikeModelCreateUpdateDto request) + { + var model = _bikeModelService.UpdateModel(id, request); + return Task.FromResult>(model == null ? NotFound() : Ok(model)); + } + + /// + /// Delete bike model + /// + [HttpDelete("{id}")] + public override Task Delete(int id) + { + var result = _bikeModelService.DeleteModel(id); + return Task.FromResult(result ? NoContent() : NotFound()); + } +} \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes.Api.Host/Controllers/BikesController.cs new file mode 100644 index 000000000..baf236434 --- /dev/null +++ b/Bikes.Api.Host/Controllers/BikesController.cs @@ -0,0 +1,65 @@ +using Bikes.Application.Contracts.Bikes; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Controller for bike management +/// +[ApiController] +[Route("api/[controller]")] +public class BikesController(IBikeService bikeService) : CrudControllerBase +{ + private readonly IBikeService _bikeService = bikeService; + + /// + /// Get all bikes + /// + [HttpGet] + public override Task>> GetAll() + { + var bikes = _bikeService.GetAllBikes(); + return Task.FromResult>>(Ok(bikes)); + } + + /// + /// Get bike by id + /// + [HttpGet("{id}")] + public override Task> GetById(int id) + { + var bike = _bikeService.GetBikeById(id); + return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); + } + + /// + /// Create new bike + /// + [HttpPost] + public override Task> Create([FromBody] BikeCreateUpdateDto request) + { + var bike = _bikeService.CreateBike(request); + return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = bike.Id }, bike)); + } + + /// + /// Update bike + /// + [HttpPut("{id}")] + public override Task> Update(int id, [FromBody] BikeCreateUpdateDto request) + { + var bike = _bikeService.UpdateBike(id, request); + return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); + } + + /// + /// Delete bike + /// + [HttpDelete("{id}")] + public override Task Delete(int id) + { + var result = _bikeService.DeleteBike(id); + return Task.FromResult(result ? NoContent() : NotFound()); + } +} \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/CrudControllerBase.cs b/Bikes.Api.Host/Controllers/CrudControllerBase.cs new file mode 100644 index 000000000..3be9bc250 --- /dev/null +++ b/Bikes.Api.Host/Controllers/CrudControllerBase.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Base controller for CRUD operations +/// +/// DTO type +/// Create/Update DTO type +[ApiController] +[Route("api/[controller]")] +public abstract class CrudControllerBase : ControllerBase + where TDto : class + where TCreateUpdateDto : class +{ + /// + /// Get all entities + /// + [HttpGet] + public abstract Task>> GetAll(); + + /// + /// Get entity by id + /// + [HttpGet("{id}")] + public abstract Task> GetById(int id); + + /// + /// Create new entity + /// + [HttpPost] + public abstract Task> Create([FromBody] TCreateUpdateDto request); + + /// + /// Update entity + /// + [HttpPut("{id}")] + public abstract Task> Update(int id, [FromBody] TCreateUpdateDto request); + + /// + /// Delete entity + /// + [HttpDelete("{id}")] + public abstract Task Delete(int id); +} \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes.Api.Host/Controllers/RentersController.cs new file mode 100644 index 000000000..1d6c58cc7 --- /dev/null +++ b/Bikes.Api.Host/Controllers/RentersController.cs @@ -0,0 +1,65 @@ +using Bikes.Application.Contracts.Renters; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Controller for renter management +/// +[ApiController] +[Route("api/[controller]")] +public class RentersController(IRenterService renterService) : CrudControllerBase +{ + private readonly IRenterService _renterService = renterService; + + /// + /// Get all renters + /// + [HttpGet] + public override Task>> GetAll() + { + var renters = _renterService.GetAllRenters(); + return Task.FromResult>>(Ok(renters)); + } + + /// + /// Get renter by id + /// + [HttpGet("{id}")] + public override Task> GetById(int id) + { + var renter = _renterService.GetRenterById(id); + return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); + } + + /// + /// Create new renter + /// + [HttpPost] + public override Task> Create([FromBody] RenterCreateUpdateDto request) + { + var renter = _renterService.CreateRenter(request); + return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = renter.Id }, renter)); + } + + /// + /// Update renter + /// + [HttpPut("{id}")] + public override Task> Update(int id, [FromBody] RenterCreateUpdateDto request) + { + var renter = _renterService.UpdateRenter(id, request); + return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); + } + + /// + /// Delete renter + /// + [HttpDelete("{id}")] + public override Task Delete(int id) + { + var result = _renterService.DeleteRenter(id); + return Task.FromResult(result ? NoContent() : NotFound()); + } +} \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes.Api.Host/Controllers/RentsController.cs new file mode 100644 index 000000000..485e179a9 --- /dev/null +++ b/Bikes.Api.Host/Controllers/RentsController.cs @@ -0,0 +1,65 @@ +using Bikes.Application.Contracts.Rents; +using Bikes.Application.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bikes.Api.Host.Controllers; + +/// +/// Controller for rent management +/// +[ApiController] +[Route("api/[controller]")] +public class RentsController(IRentService rentService) : CrudControllerBase +{ + private readonly IRentService _rentService = rentService; + + /// + /// Get all rents + /// + [HttpGet] + public override Task>> GetAll() + { + var rents = _rentService.GetAllRents(); + return Task.FromResult>>(Ok(rents)); + } + + /// + /// Get rent by id + /// + [HttpGet("{id}")] + public override Task> GetById(int id) + { + var rent = _rentService.GetRentById(id); + return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); + } + + /// + /// Create new rent + /// + [HttpPost] + public override Task> Create([FromBody] RentCreateUpdateDto request) + { + var rent = _rentService.CreateRent(request); + return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = rent.Id }, rent)); + } + + /// + /// Update rent + /// + [HttpPut("{id}")] + public override Task> Update(int id, [FromBody] RentCreateUpdateDto request) + { + var rent = _rentService.UpdateRent(id, request); + return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); + } + + /// + /// Delete rent + /// + [HttpDelete("{id}")] + public override Task Delete(int id) + { + var result = _rentService.DeleteRent(id); + return Task.FromResult(result ? NoContent() : NotFound()); + } +} \ No newline at end of file diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs new file mode 100644 index 000000000..ef48ffe8a --- /dev/null +++ b/Bikes.Api.Host/Program.cs @@ -0,0 +1,36 @@ +using Bikes.Application.Services; +using Bikes.Application.Contracts.Analytics; +using Bikes.Application.Contracts.Bikes; +using Bikes.Application.Contracts.Models; +using Bikes.Application.Contracts.Renters; +using Bikes.Application.Contracts.Rents; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Register services with interfaces +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +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.Api.Host/Properties/launchSettings.json b/Bikes.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..6b8f22f6e --- /dev/null +++ b/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:64619", + "sslPort": 44386 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5145", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7278;http://localhost:5145", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Bikes.Api.Host/appsettings.Development.json b/Bikes.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Bikes.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Bikes.Api.Host/appsettings.json b/Bikes.Api.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Bikes.Api.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 55e065bd9ec518ff8180a089b719ebb88f515498 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 30 Oct 2025 06:35:01 +0400 Subject: [PATCH 10/19] Fixed some files, added services for renters --- Bikes.Application/Services/IBikeRepository.cs | 48 +++----- .../Services/InMemoryBikeRepository.cs | 107 +++++++++++++----- Bikes.Application/Services/RenterService.cs | 22 ++-- 3 files changed, 109 insertions(+), 68 deletions(-) diff --git a/Bikes.Application/Services/IBikeRepository.cs b/Bikes.Application/Services/IBikeRepository.cs index 92dfaf020..f0d29166d 100644 --- a/Bikes.Application/Services/IBikeRepository.cs +++ b/Bikes.Application/Services/IBikeRepository.cs @@ -7,43 +7,31 @@ namespace Bikes.Application.Services; /// public interface IBikeRepository { - /// - /// Get all bikes - /// + // Bike methods public List GetAllBikes(); - - /// - /// Get bike by identifier - /// public Bike? GetBikeById(int id); - - /// - /// Add new bike - /// public void AddBike(Bike bike); - - /// - /// Update bike - /// public void UpdateBike(Bike bike); - - /// - /// Delete bike by identifier - /// public void DeleteBike(int id); - /// - /// Get all bike models - /// + // BikeModel methods public List GetAllModels(); + public BikeModel? GetModelById(int id); + public void AddModel(BikeModel model); + public void UpdateModel(BikeModel model); + public void DeleteModel(int id); - /// - /// Get all rental records - /// - public List GetAllRents(); - - /// - /// Get all renters - /// + // Renter methods public List GetAllRenters(); + public Renter? GetRenterById(int id); + public void AddRenter(Renter renter); + public void UpdateRenter(Renter renter); + public void DeleteRenter(int id); + + // Rent methods + public List GetAllRents(); + public Rent? GetRentById(int id); + public void AddRent(Rent rent); + public void UpdateRent(Rent rent); + public void DeleteRent(int id); } \ No newline at end of file diff --git a/Bikes.Application/Services/InMemoryBikeRepository.cs b/Bikes.Application/Services/InMemoryBikeRepository.cs index b9cc52604..1f057577b 100644 --- a/Bikes.Application/Services/InMemoryBikeRepository.cs +++ b/Bikes.Application/Services/InMemoryBikeRepository.cs @@ -8,26 +8,16 @@ namespace Bikes.Application.Services; /// public class InMemoryBikeRepository() : IBikeRepository { - private readonly BikesFixture _fixture = new(); - private readonly List _bikes = [.. new BikesFixture().Bikes]; private readonly List _models = [.. new BikesFixture().Models]; private readonly List _rents = [.. new BikesFixture().Rents]; private readonly List _renters = [.. new BikesFixture().Renters]; - /// - /// Get all bikes - /// + // Bike methods public List GetAllBikes() => [.. _bikes]; - /// - /// Get bike by identifier - /// public Bike? GetBikeById(int id) => _bikes.FirstOrDefault(b => b.Id == id); - /// - /// Add new bike - /// public void AddBike(Bike bike) { if (bike == null) @@ -36,9 +26,6 @@ public void AddBike(Bike bike) _bikes.Add(bike); } - /// - /// Update bike - /// public void UpdateBike(Bike bike) { if (bike == null) @@ -52,23 +39,89 @@ public void UpdateBike(Bike bike) } } - /// - /// Delete bike by identifier - /// public void DeleteBike(int id) => _bikes.RemoveAll(b => b.Id == id); - /// - /// Get all bike models - /// + // BikeModel methods public List GetAllModels() => [.. _models]; - /// - /// Get all rental records - /// - public List GetAllRents() => [.. _rents]; + public BikeModel? GetModelById(int id) => _models.FirstOrDefault(m => m.Id == id); + + public void AddModel(BikeModel model) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + _models.Add(model); + } + + public void UpdateModel(BikeModel model) + { + if (model == null) + throw new ArgumentNullException(nameof(model)); + + var existingModel = _models.FirstOrDefault(m => m.Id == model.Id); + if (existingModel != null) + { + _models.Remove(existingModel); + _models.Add(model); + } + } + + public void DeleteModel(int id) => _models.RemoveAll(m => m.Id == id); - /// - /// Get all renters - /// + // Renter methods public List GetAllRenters() => [.. _renters]; + + public Renter? GetRenterById(int id) => _renters.FirstOrDefault(r => r.Id == id); + + public void AddRenter(Renter renter) + { + if (renter == null) + throw new ArgumentNullException(nameof(renter)); + + _renters.Add(renter); + } + + public void UpdateRenter(Renter renter) + { + if (renter == null) + throw new ArgumentNullException(nameof(renter)); + + var existingRenter = _renters.FirstOrDefault(r => r.Id == renter.Id); + if (existingRenter != null) + { + _renters.Remove(existingRenter); + _renters.Add(renter); + } + } + + public void DeleteRenter(int id) => _renters.RemoveAll(r => r.Id == id); + + // Rent methods + public List GetAllRents() => [.. _rents]; + + public Rent? GetRentById(int id) => _rents.FirstOrDefault(r => r.Id == id); + + public void AddRent(Rent rent) + { + if (rent == null) + throw new ArgumentNullException(nameof(rent)); + + _rents.Add(rent); + } + + public void UpdateRent(Rent rent) + { + if (rent == null) + throw new ArgumentNullException(nameof(rent)); + + var existingRent = _rents.FirstOrDefault(r => r.Id == rent.Id); + if (existingRent != null) + { + _rents.Remove(existingRent); + _rents.Add(rent); + } + } + + public void DeleteRent(int id) => _rents.RemoveAll(r => r.Id == id); } \ No newline at end of file diff --git a/Bikes.Application/Services/RenterService.cs b/Bikes.Application/Services/RenterService.cs index 514621d17..66fbcc2f7 100644 --- a/Bikes.Application/Services/RenterService.cs +++ b/Bikes.Application/Services/RenterService.cs @@ -28,7 +28,7 @@ public List GetAllRenters() /// public RenterDto? GetRenterById(int id) { - var renter = _repository.GetAllRenters().FirstOrDefault(r => r.Id == id); + var renter = _repository.GetRenterById(id); return renter == null ? null : new RenterDto { Id = renter.Id, @@ -45,13 +45,12 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) var renters = _repository.GetAllRenters(); var newRenter = new Renter { - Id = renters.Max(r => r.Id) + 1, + Id = renters.Count > 0 ? renters.Max(r => r.Id) + 1 : 1, FullName = request.FullName, Phone = request.Phone }; - // Note: In real application, we would add to repository - // _repository.AddRenter(newRenter); + _repository.AddRenter(newRenter); return new RenterDto { @@ -66,13 +65,14 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) /// public RenterDto? UpdateRenter(int id, RenterCreateUpdateDto request) { - var renters = _repository.GetAllRenters(); - var renter = renters.FirstOrDefault(r => r.Id == id); + var renter = _repository.GetRenterById(id); if (renter == null) return null; renter.FullName = request.FullName; renter.Phone = request.Phone; + _repository.UpdateRenter(renter); + return new RenterDto { Id = renter.Id, @@ -86,10 +86,10 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) /// public bool DeleteRenter(int id) { - var renters = _repository.GetAllRenters(); - var renter = renters.FirstOrDefault(r => r.Id == id); - return renter != null; - // Note: In real application, we would remove from repository - // return _repository.DeleteRenter(id); + var renter = _repository.GetRenterById(id); + if (renter == null) return false; + + _repository.DeleteRenter(id); + return true; } } \ No newline at end of file From ae91a6cbadadeffa9c1b11c50a1f7965092dc7d4 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Wed, 5 Nov 2025 00:13:52 +0400 Subject: [PATCH 11/19] I hope all comments have been corrected --- .github/workflows/dotnet_tests.yml | 4 +- Bikes.Api.Host/Bikes.Api.Host.csproj | 26 ++--- .../Controllers/AnalyticsController.cs | 68 ++++++++--- .../Controllers/BikeModelsController.cs | 49 ++++---- Bikes.Api.Host/Controllers/BikesController.cs | 49 ++++---- .../Controllers/CrudControllerBase.cs | 58 +++++++++- .../Controllers/RentersController.cs | 49 ++++---- Bikes.Api.Host/Controllers/RentsController.cs | 49 ++++---- Bikes.Api.Host/Program.cs | 14 ++- .../Analytics/IAnalyticsService.cs | 5 +- .../Bikes.Application.Contracts.csproj | 12 +- .../Bikes/IBikeService.cs | 30 +---- .../IApplicationService.cs | 32 +++++- .../Models/IBikeModelService.cs | 30 +---- .../Renters/IRenterService.cs | 30 +---- .../Rents/IRentService.cs | 30 +---- Bikes.Application/Bikes.Application.csproj | 3 +- .../Services/AnalyticsService.cs | 34 +++--- .../Services/BikeModelService.cs | 40 ++++--- Bikes.Application/Services/BikeService.cs | 39 ++++--- Bikes.Application/Services/RentService.cs | 52 +++++---- Bikes.Application/Services/RenterService.cs | 35 +++--- Bikes.Domain/Bikes.Domain.csproj | 4 + Bikes.Domain/Bikes.Domain.sln | 33 ------ .../Repositories}/IBikeRepository.cs | 2 +- .../Bikes.Infrastructure.InMemory.csproj | 14 +++ .../Repositories}/InMemoryBikeRepository.cs | 35 ++---- Bikes.sln | 107 ++++++++++++++++++ lab1.sln | 48 -------- 29 files changed, 516 insertions(+), 465 deletions(-) delete mode 100644 Bikes.Domain/Bikes.Domain.sln rename {Bikes.Application/Services => Bikes.Domain/Repositories}/IBikeRepository.cs (96%) create mode 100644 Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj rename {Bikes.Application/Services => Bikes.Infrastructure.InMemory/Repositories}/InMemoryBikeRepository.cs (79%) create mode 100644 Bikes.sln delete mode 100644 lab1.sln diff --git a/.github/workflows/dotnet_tests.yml b/.github/workflows/dotnet_tests.yml index 9397a3511..8a6b671ce 100644 --- a/.github/workflows/dotnet_tests.yml +++ b/.github/workflows/dotnet_tests.yml @@ -22,10 +22,10 @@ jobs: dotnet-version: '8.x' - name: Restore dependencies - run: dotnet restore lab1.sln + run: dotnet restore Bikes.sln - name: Build - run: dotnet build lab1.sln --no-restore --configuration Release + run: dotnet build Bikes.sln --no-restore --configuration Release - name: Run tests run: dotnet test Bikes.Tests/Bikes.Tests.csproj --no-build --configuration Release --verbosity normal \ No newline at end of file diff --git a/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes.Api.Host/Bikes.Api.Host.csproj index 614b5d408..3d0d1b000 100644 --- a/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -1,18 +1,18 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - + + + - - - - + + + + - + \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes.Api.Host/Controllers/AnalyticsController.cs index f1af8d61e..011535fab 100644 --- a/Bikes.Api.Host/Controllers/AnalyticsController.cs +++ b/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -11,16 +11,21 @@ namespace Bikes.Api.Host.Controllers; [Route("api/[controller]")] public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase { - private readonly IAnalyticsService _analyticsService = analyticsService; - /// /// Get all sport bikes /// [HttpGet("sport-bikes")] public ActionResult> GetSportBikes() { - var sportBikes = _analyticsService.GetSportBikes(); - return Ok(sportBikes); + try + { + var sportBikes = analyticsService.GetSportBikes(); + return Ok(sportBikes); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } /// @@ -29,8 +34,15 @@ public ActionResult> GetSportBikes() [HttpGet("top-models-by-profit")] public ActionResult> GetTopModelsByProfit() { - var topModels = _analyticsService.GetTop5ModelsByProfit(); - return Ok(topModels); + try + { + var topModels = analyticsService.GetTop5ModelsByProfit(); + return Ok(topModels); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } /// @@ -39,8 +51,15 @@ public ActionResult> GetTopModelsByProfit() [HttpGet("top-models-by-duration")] public ActionResult> GetTopModelsByDuration() { - var topModels = _analyticsService.GetTop5ModelsByRentalDuration(); - return Ok(topModels); + try + { + var topModels = analyticsService.GetTop5ModelsByRentalDuration(); + return Ok(topModels); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } /// @@ -49,8 +68,15 @@ public ActionResult> GetTopModelsByDuration() [HttpGet("rental-statistics")] public ActionResult GetRentalStatistics() { - var statistics = _analyticsService.GetRentalStatistics(); - return Ok(statistics); + try + { + var statistics = analyticsService.GetRentalStatistics(); + return Ok(statistics); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } /// @@ -59,8 +85,15 @@ public ActionResult GetRentalStatistics() [HttpGet("rental-time-by-type")] public ActionResult> GetRentalTimeByType() { - var rentalTime = _analyticsService.GetTotalRentalTimeByBikeType(); - return Ok(rentalTime); + try + { + var rentalTime = analyticsService.GetTotalRentalTimeByBikeType(); + return Ok(rentalTime); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } /// @@ -69,7 +102,14 @@ public ActionResult> GetRentalTimeByType() [HttpGet("top-renters")] public ActionResult> GetTopRenters() { - var topRenters = _analyticsService.GetTopRentersByRentalCount(); - return Ok(topRenters); + try + { + var topRenters = analyticsService.GetTopRentersByRentalCount(); + return Ok(topRenters); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } } } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes.Api.Host/Controllers/BikeModelsController.cs index 3a97ec460..3a3432830 100644 --- a/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -1,5 +1,4 @@ using Bikes.Application.Contracts.Models; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -11,16 +10,21 @@ namespace Bikes.Api.Host.Controllers; [Route("api/[controller]")] public class BikeModelsController(IBikeModelService bikeModelService) : CrudControllerBase { - private readonly IBikeModelService _bikeModelService = bikeModelService; - /// /// Get all bike models /// [HttpGet] public override Task>> GetAll() { - var models = _bikeModelService.GetAllModels(); - return Task.FromResult>>(Ok(models)); + try + { + var models = bikeModelService.GetAll(); + return Task.FromResult>>(Ok(models)); + } + catch (Exception ex) + { + return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } /// @@ -29,37 +33,32 @@ public override Task>> GetAll() [HttpGet("{id}")] public override Task> GetById(int id) { - var model = _bikeModelService.GetModelById(id); - return Task.FromResult>(model == null ? NotFound() : Ok(model)); + try + { + var model = bikeModelService.GetById(id); + return Task.FromResult>(model == null ? NotFound() : Ok(model)); + } + catch (Exception ex) + { + return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } - /// - /// Create new bike model - /// - [HttpPost] - public override Task> Create([FromBody] BikeModelCreateUpdateDto request) + protected override Task> CreateInternal(BikeModelCreateUpdateDto request) { - var model = _bikeModelService.CreateModel(request); + var model = bikeModelService.Create(request); return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = model.Id }, model)); } - /// - /// Update bike model - /// - [HttpPut("{id}")] - public override Task> Update(int id, [FromBody] BikeModelCreateUpdateDto request) + protected override Task> UpdateInternal(int id, BikeModelCreateUpdateDto request) { - var model = _bikeModelService.UpdateModel(id, request); + var model = bikeModelService.Update(id, request); return Task.FromResult>(model == null ? NotFound() : Ok(model)); } - /// - /// Delete bike model - /// - [HttpDelete("{id}")] - public override Task Delete(int id) + protected override Task DeleteInternal(int id) { - var result = _bikeModelService.DeleteModel(id); + var result = bikeModelService.Delete(id); return Task.FromResult(result ? NoContent() : NotFound()); } } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes.Api.Host/Controllers/BikesController.cs index baf236434..8908fa7a1 100644 --- a/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes.Api.Host/Controllers/BikesController.cs @@ -1,5 +1,4 @@ using Bikes.Application.Contracts.Bikes; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -11,16 +10,21 @@ namespace Bikes.Api.Host.Controllers; [Route("api/[controller]")] public class BikesController(IBikeService bikeService) : CrudControllerBase { - private readonly IBikeService _bikeService = bikeService; - /// /// Get all bikes /// [HttpGet] public override Task>> GetAll() { - var bikes = _bikeService.GetAllBikes(); - return Task.FromResult>>(Ok(bikes)); + try + { + var bikes = bikeService.GetAll(); + return Task.FromResult>>(Ok(bikes)); + } + catch (Exception ex) + { + return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } /// @@ -29,37 +33,32 @@ public override Task>> GetAll() [HttpGet("{id}")] public override Task> GetById(int id) { - var bike = _bikeService.GetBikeById(id); - return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); + try + { + var bike = bikeService.GetById(id); + return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); + } + catch (Exception ex) + { + return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } - /// - /// Create new bike - /// - [HttpPost] - public override Task> Create([FromBody] BikeCreateUpdateDto request) + protected override Task> CreateInternal(BikeCreateUpdateDto request) { - var bike = _bikeService.CreateBike(request); + var bike = bikeService.Create(request); return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = bike.Id }, bike)); } - /// - /// Update bike - /// - [HttpPut("{id}")] - public override Task> Update(int id, [FromBody] BikeCreateUpdateDto request) + protected override Task> UpdateInternal(int id, BikeCreateUpdateDto request) { - var bike = _bikeService.UpdateBike(id, request); + var bike = bikeService.Update(id, request); return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); } - /// - /// Delete bike - /// - [HttpDelete("{id}")] - public override Task Delete(int id) + protected override Task DeleteInternal(int id) { - var result = _bikeService.DeleteBike(id); + var result = bikeService.Delete(id); return Task.FromResult(result ? NoContent() : NotFound()); } } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/CrudControllerBase.cs b/Bikes.Api.Host/Controllers/CrudControllerBase.cs index 3be9bc250..c56db80ac 100644 --- a/Bikes.Api.Host/Controllers/CrudControllerBase.cs +++ b/Bikes.Api.Host/Controllers/CrudControllerBase.cs @@ -29,17 +29,69 @@ public abstract class CrudControllerBase : ControllerBas /// Create new entity /// [HttpPost] - public abstract Task> Create([FromBody] TCreateUpdateDto request); + public virtual async Task> Create([FromBody] TCreateUpdateDto request) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return await CreateInternal(request); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } /// /// Update entity /// [HttpPut("{id}")] - public abstract Task> Update(int id, [FromBody] TCreateUpdateDto request); + public virtual async Task> Update(int id, [FromBody] TCreateUpdateDto request) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return await UpdateInternal(id, request); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } /// /// Delete entity /// [HttpDelete("{id}")] - public abstract Task Delete(int id); + public virtual async Task Delete(int id) + { + try + { + return await DeleteInternal(id); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } + + protected abstract Task> CreateInternal(TCreateUpdateDto request); + protected abstract Task> UpdateInternal(int id, TCreateUpdateDto request); + protected abstract Task DeleteInternal(int id); } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes.Api.Host/Controllers/RentersController.cs index 1d6c58cc7..3f1b4173b 100644 --- a/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes.Api.Host/Controllers/RentersController.cs @@ -1,5 +1,4 @@ using Bikes.Application.Contracts.Renters; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -11,16 +10,21 @@ namespace Bikes.Api.Host.Controllers; [Route("api/[controller]")] public class RentersController(IRenterService renterService) : CrudControllerBase { - private readonly IRenterService _renterService = renterService; - /// /// Get all renters /// [HttpGet] public override Task>> GetAll() { - var renters = _renterService.GetAllRenters(); - return Task.FromResult>>(Ok(renters)); + try + { + var renters = renterService.GetAll(); + return Task.FromResult>>(Ok(renters)); + } + catch (Exception ex) + { + return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } /// @@ -29,37 +33,32 @@ public override Task>> GetAll() [HttpGet("{id}")] public override Task> GetById(int id) { - var renter = _renterService.GetRenterById(id); - return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); + try + { + var renter = renterService.GetById(id); + return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); + } + catch (Exception ex) + { + return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } - /// - /// Create new renter - /// - [HttpPost] - public override Task> Create([FromBody] RenterCreateUpdateDto request) + protected override Task> CreateInternal(RenterCreateUpdateDto request) { - var renter = _renterService.CreateRenter(request); + var renter = renterService.Create(request); return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = renter.Id }, renter)); } - /// - /// Update renter - /// - [HttpPut("{id}")] - public override Task> Update(int id, [FromBody] RenterCreateUpdateDto request) + protected override Task> UpdateInternal(int id, RenterCreateUpdateDto request) { - var renter = _renterService.UpdateRenter(id, request); + var renter = renterService.Update(id, request); return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); } - /// - /// Delete renter - /// - [HttpDelete("{id}")] - public override Task Delete(int id) + protected override Task DeleteInternal(int id) { - var result = _renterService.DeleteRenter(id); + var result = renterService.Delete(id); return Task.FromResult(result ? NoContent() : NotFound()); } } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes.Api.Host/Controllers/RentsController.cs index 485e179a9..8f102bbbe 100644 --- a/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes.Api.Host/Controllers/RentsController.cs @@ -1,5 +1,4 @@ using Bikes.Application.Contracts.Rents; -using Bikes.Application.Services; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -11,16 +10,21 @@ namespace Bikes.Api.Host.Controllers; [Route("api/[controller]")] public class RentsController(IRentService rentService) : CrudControllerBase { - private readonly IRentService _rentService = rentService; - /// /// Get all rents /// [HttpGet] public override Task>> GetAll() { - var rents = _rentService.GetAllRents(); - return Task.FromResult>>(Ok(rents)); + try + { + var rents = rentService.GetAll(); + return Task.FromResult>>(Ok(rents)); + } + catch (Exception ex) + { + return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } /// @@ -29,37 +33,32 @@ public override Task>> GetAll() [HttpGet("{id}")] public override Task> GetById(int id) { - var rent = _rentService.GetRentById(id); - return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); + try + { + var rent = rentService.GetById(id); + return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); + } + catch (Exception ex) + { + return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); + } } - /// - /// Create new rent - /// - [HttpPost] - public override Task> Create([FromBody] RentCreateUpdateDto request) + protected override Task> CreateInternal(RentCreateUpdateDto request) { - var rent = _rentService.CreateRent(request); + var rent = rentService.Create(request); return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = rent.Id }, rent)); } - /// - /// Update rent - /// - [HttpPut("{id}")] - public override Task> Update(int id, [FromBody] RentCreateUpdateDto request) + protected override Task> UpdateInternal(int id, RentCreateUpdateDto request) { - var rent = _rentService.UpdateRent(id, request); + var rent = rentService.Update(id, request); return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); } - /// - /// Delete rent - /// - [HttpDelete("{id}")] - public override Task Delete(int id) + protected override Task DeleteInternal(int id) { - var result = _rentService.DeleteRent(id); + var result = rentService.Delete(id); return Task.FromResult(result ? NoContent() : NotFound()); } } \ No newline at end of file diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs index ef48ffe8a..4f9bd3c23 100644 --- a/Bikes.Api.Host/Program.cs +++ b/Bikes.Api.Host/Program.cs @@ -4,6 +4,8 @@ using Bikes.Application.Contracts.Models; using Bikes.Application.Contracts.Renters; using Bikes.Application.Contracts.Rents; +using Bikes.Domain.Repositories; +using Bikes.Infrastructure.InMemory.Repositories; var builder = WebApplication.CreateBuilder(args); @@ -13,12 +15,12 @@ builder.Services.AddSwaggerGen(); // Register services with interfaces -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs b/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs index 1c20ad0dc..a18f7b382 100644 --- a/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs +++ b/Bikes.Application.Contracts/Analytics/IAnalyticsService.cs @@ -1,12 +1,11 @@ -using Bikes.Application.Contracts; -using Bikes.Application.Contracts.Bikes; +using Bikes.Application.Contracts.Bikes; namespace Bikes.Application.Contracts.Analytics; /// /// Service for bike rental analytics /// -public interface IAnalyticsService : IApplicationService +public interface IAnalyticsService { /// /// Get all sport bikes diff --git a/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj b/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj index fa71b7ae6..d44b7bba6 100644 --- a/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj +++ b/Bikes.Application.Contracts/Bikes.Application.Contracts.csproj @@ -1,9 +1,9 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - + \ No newline at end of file diff --git a/Bikes.Application.Contracts/Bikes/IBikeService.cs b/Bikes.Application.Contracts/Bikes/IBikeService.cs index 4510e276c..d880287c9 100644 --- a/Bikes.Application.Contracts/Bikes/IBikeService.cs +++ b/Bikes.Application.Contracts/Bikes/IBikeService.cs @@ -1,34 +1,8 @@ -using Bikes.Application.Contracts; - -namespace Bikes.Application.Contracts.Bikes; +namespace Bikes.Application.Contracts.Bikes; /// /// Service for bike management /// -public interface IBikeService : IApplicationService +public interface IBikeService : IApplicationService { - /// - /// Get all bikes - /// - public List GetAllBikes(); - - /// - /// Get bike by identifier - /// - public BikeDto? GetBikeById(int id); - - /// - /// Create new bike - /// - public BikeDto CreateBike(BikeCreateUpdateDto request); - - /// - /// Update bike - /// - public BikeDto? UpdateBike(int id, BikeCreateUpdateDto request); - - /// - /// Delete bike - /// - public bool DeleteBike(int id); } \ No newline at end of file diff --git a/Bikes.Application.Contracts/IApplicationService.cs b/Bikes.Application.Contracts/IApplicationService.cs index 9a9107bca..350e0aba6 100644 --- a/Bikes.Application.Contracts/IApplicationService.cs +++ b/Bikes.Application.Contracts/IApplicationService.cs @@ -1,8 +1,36 @@ namespace Bikes.Application.Contracts; /// -/// Base interface for all application services +/// Base interface for all application services with CRUD operations /// -public interface IApplicationService +/// DTO type for reading +/// DTO type for creating and updating +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class { + /// + /// Get all entities + /// + public List GetAll(); + + /// + /// Get entity by identifier + /// + public TDto? GetById(int id); + + /// + /// Create new entity + /// + public TDto Create(TCreateUpdateDto request); + + /// + /// Update entity + /// + public TDto? Update(int id, TCreateUpdateDto request); + + /// + /// Delete entity + /// + public bool Delete(int id); } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Models/IBikeModelService.cs b/Bikes.Application.Contracts/Models/IBikeModelService.cs index cbd3d0dfa..e47678268 100644 --- a/Bikes.Application.Contracts/Models/IBikeModelService.cs +++ b/Bikes.Application.Contracts/Models/IBikeModelService.cs @@ -1,34 +1,8 @@ -using Bikes.Application.Contracts; - -namespace Bikes.Application.Contracts.Models; +namespace Bikes.Application.Contracts.Models; /// /// Service for bike model management /// -public interface IBikeModelService : IApplicationService +public interface IBikeModelService : IApplicationService { - /// - /// Get all bike models - /// - public List GetAllModels(); - - /// - /// Get bike model by identifier - /// - public BikeModelDto? GetModelById(int id); - - /// - /// Create new bike model - /// - public BikeModelDto CreateModel(BikeModelCreateUpdateDto request); - - /// - /// Update bike model - /// - public BikeModelDto? UpdateModel(int id, BikeModelCreateUpdateDto request); - - /// - /// Delete bike model - /// - public bool DeleteModel(int id); } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Renters/IRenterService.cs b/Bikes.Application.Contracts/Renters/IRenterService.cs index 9734afe65..ecd43212a 100644 --- a/Bikes.Application.Contracts/Renters/IRenterService.cs +++ b/Bikes.Application.Contracts/Renters/IRenterService.cs @@ -1,34 +1,8 @@ -using Bikes.Application.Contracts; - -namespace Bikes.Application.Contracts.Renters; +namespace Bikes.Application.Contracts.Renters; /// /// Service for renter management /// -public interface IRenterService : IApplicationService +public interface IRenterService : IApplicationService { - /// - /// Get all renters - /// - public List GetAllRenters(); - - /// - /// Get renter by identifier - /// - public RenterDto? GetRenterById(int id); - - /// - /// Create new renter - /// - public RenterDto CreateRenter(RenterCreateUpdateDto request); - - /// - /// Update renter - /// - public RenterDto? UpdateRenter(int id, RenterCreateUpdateDto request); - - /// - /// Delete renter - /// - public bool DeleteRenter(int id); } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/IRentService.cs b/Bikes.Application.Contracts/Rents/IRentService.cs index fc2d5e3bb..2bae57458 100644 --- a/Bikes.Application.Contracts/Rents/IRentService.cs +++ b/Bikes.Application.Contracts/Rents/IRentService.cs @@ -1,34 +1,8 @@ -using Bikes.Application.Contracts; - -namespace Bikes.Application.Contracts.Rents; +namespace Bikes.Application.Contracts.Rents; /// /// Service for rent management /// -public interface IRentService : IApplicationService +public interface IRentService : IApplicationService { - /// - /// Get all rents - /// - public List GetAllRents(); - - /// - /// Get rent by identifier - /// - public RentDto? GetRentById(int id); - - /// - /// Create new rent - /// - public RentDto CreateRent(RentCreateUpdateDto request); - - /// - /// Update rent - /// - public RentDto? UpdateRent(int id, RentCreateUpdateDto request); - - /// - /// Delete rent - /// - public bool DeleteRent(int id); } \ No newline at end of file diff --git a/Bikes.Application/Bikes.Application.csproj b/Bikes.Application/Bikes.Application.csproj index 15b06c758..00730a53a 100644 --- a/Bikes.Application/Bikes.Application.csproj +++ b/Bikes.Application/Bikes.Application.csproj @@ -9,7 +9,6 @@ - - + \ No newline at end of file diff --git a/Bikes.Application/Services/AnalyticsService.cs b/Bikes.Application/Services/AnalyticsService.cs index 6f28f6601..b4dddf11c 100644 --- a/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes.Application/Services/AnalyticsService.cs @@ -1,6 +1,7 @@ using Bikes.Application.Contracts.Analytics; using Bikes.Application.Contracts.Bikes; using Bikes.Domain.Models; +using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -9,14 +10,12 @@ namespace Bikes.Application.Services; /// public class AnalyticsService(IBikeRepository repository) : IAnalyticsService { - private readonly IBikeRepository _repository = repository; - /// /// Get all sport bikes /// public List GetSportBikes() { - return _repository.GetAllBikes() + return [.. repository.GetAllBikes() .Where(b => b.Model.Type == BikeType.Sport) .Select(b => new BikeDto { @@ -25,8 +24,7 @@ public List GetSportBikes() ModelId = b.Model.Id, Color = b.Color, IsAvailable = b.IsAvailable - }) - .ToList(); + })]; } /// @@ -34,9 +32,9 @@ public List GetSportBikes() /// public List GetTop5ModelsByProfit() { - var rents = _repository.GetAllRents(); + var rents = repository.GetAllRents(); - return rents + return [.. rents .GroupBy(r => r.Bike.Model) .Select(g => new BikeModelAnalyticsDto { @@ -48,8 +46,7 @@ public List GetTop5ModelsByProfit() TotalDuration = g.Sum(r => r.DurationHours) }) .OrderByDescending(x => x.TotalProfit) - .Take(5) - .ToList(); + .Take(5)]; } /// @@ -57,9 +54,9 @@ public List GetTop5ModelsByProfit() /// public List GetTop5ModelsByRentalDuration() { - var rents = _repository.GetAllRents(); + var rents = repository.GetAllRents(); - return rents + return [.. rents .GroupBy(r => r.Bike.Model) .Select(g => new BikeModelAnalyticsDto { @@ -71,8 +68,7 @@ public List GetTop5ModelsByRentalDuration() TotalDuration = g.Sum(r => r.DurationHours) }) .OrderByDescending(x => x.TotalDuration) - .Take(5) - .ToList(); + .Take(5)]; } /// @@ -80,9 +76,8 @@ public List GetTop5ModelsByRentalDuration() /// public RentalStatistics GetRentalStatistics() { - var durations = _repository.GetAllRents() - .Select(r => r.DurationHours) - .ToList(); + var durations = repository.GetAllRents() + .Select(r => r.DurationHours); return new RentalStatistics( MinDuration: durations.Min(), @@ -96,7 +91,7 @@ public RentalStatistics GetRentalStatistics() /// public Dictionary GetTotalRentalTimeByBikeType() { - return _repository.GetAllRents() + return repository.GetAllRents() .GroupBy(r => r.Bike.Model.Type) .ToDictionary( g => g.Key.ToString(), @@ -109,7 +104,7 @@ public Dictionary GetTotalRentalTimeByBikeType() /// public List GetTopRentersByRentalCount() { - return _repository.GetAllRents() + return [.. repository.GetAllRents() .GroupBy(r => r.Renter) .Select(g => new RenterAnalyticsDto { @@ -117,7 +112,6 @@ public List GetTopRentersByRentalCount() RentalCount = g.Count() }) .OrderByDescending(x => x.RentalCount) - .Take(5) - .ToList(); + .Take(5)]; } } \ No newline at end of file diff --git a/Bikes.Application/Services/BikeModelService.cs b/Bikes.Application/Services/BikeModelService.cs index 728e1abaf..fb58e7921 100644 --- a/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes.Application/Services/BikeModelService.cs @@ -1,5 +1,6 @@ using Bikes.Application.Contracts.Models; using Bikes.Domain.Models; +using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -8,14 +9,12 @@ namespace Bikes.Application.Services; /// public class BikeModelService(IBikeRepository repository) : IBikeModelService { - private readonly IBikeRepository _repository = repository; - /// /// Get all bike models /// - public List GetAllModels() + public List GetAll() { - return _repository.GetAllModels().Select(m => new BikeModelDto + return [.. repository.GetAllModels().Select(m => new BikeModelDto { Id = m.Id, Name = m.Name, @@ -26,15 +25,15 @@ public List GetAllModels() BrakeType = m.BrakeType, ModelYear = m.ModelYear, PricePerHour = m.PricePerHour - }).ToList(); + })]; } /// /// Get bike model by identifier /// - public BikeModelDto? GetModelById(int id) + public BikeModelDto? GetById(int id) { - var model = _repository.GetAllModels().FirstOrDefault(m => m.Id == id); + var model = repository.GetModelById(id); return model == null ? null : new BikeModelDto { Id = model.Id, @@ -52,12 +51,14 @@ public List GetAllModels() /// /// Create new bike model /// - public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) + public BikeModelDto Create(BikeModelCreateUpdateDto request) { + ArgumentNullException.ThrowIfNull(request); + if (!Enum.TryParse(request.Type, out var bikeType)) throw new InvalidOperationException($"Invalid bike type: {request.Type}"); - var models = _repository.GetAllModels(); + var models = repository.GetAllModels(); var newModel = new BikeModel { Id = models.Max(m => m.Id) + 1, @@ -71,6 +72,8 @@ public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) PricePerHour = request.PricePerHour }; + repository.AddModel(newModel); + return new BikeModelDto { Id = newModel.Id, @@ -88,13 +91,14 @@ public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) /// /// Update bike model /// - public BikeModelDto? UpdateModel(int id, BikeModelCreateUpdateDto request) + public BikeModelDto? Update(int id, BikeModelCreateUpdateDto request) { + ArgumentNullException.ThrowIfNull(request); + if (!Enum.TryParse(request.Type, out var bikeType)) throw new InvalidOperationException($"Invalid bike type: {request.Type}"); - var models = _repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == id); + var model = repository.GetModelById(id); if (model == null) return null; model.Name = request.Name; @@ -106,6 +110,8 @@ public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) model.ModelYear = request.ModelYear; model.PricePerHour = request.PricePerHour; + repository.UpdateModel(model); + return new BikeModelDto { Id = model.Id, @@ -123,10 +129,12 @@ public BikeModelDto CreateModel(BikeModelCreateUpdateDto request) /// /// Delete bike model /// - public bool DeleteModel(int id) + public bool Delete(int id) { - var models = _repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == id); - return model != null; + var model = repository.GetModelById(id); + if (model == null) return false; + + repository.DeleteModel(id); + return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/BikeService.cs b/Bikes.Application/Services/BikeService.cs index f28d0964a..736c55f5c 100644 --- a/Bikes.Application/Services/BikeService.cs +++ b/Bikes.Application/Services/BikeService.cs @@ -1,5 +1,6 @@ using Bikes.Application.Contracts.Bikes; using Bikes.Domain.Models; +using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -8,29 +9,27 @@ namespace Bikes.Application.Services; /// public class BikeService(IBikeRepository repository) : IBikeService { - private readonly IBikeRepository _repository = repository; - /// /// Get all bikes /// - public List GetAllBikes() + public List GetAll() { - return _repository.GetAllBikes().Select(b => new BikeDto + return [.. repository.GetAllBikes().Select(b => new BikeDto { Id = b.Id, SerialNumber = b.SerialNumber, ModelId = b.Model.Id, Color = b.Color, IsAvailable = b.IsAvailable - }).ToList(); + })]; } /// /// Get bike by identifier /// - public BikeDto? GetBikeById(int id) + public BikeDto? GetById(int id) { - var bike = _repository.GetBikeById(id); + var bike = repository.GetBikeById(id); return bike == null ? null : new BikeDto { Id = bike.Id, @@ -44,23 +43,25 @@ public List GetAllBikes() /// /// Create new bike /// - public BikeDto CreateBike(BikeCreateUpdateDto request) + public BikeDto Create(BikeCreateUpdateDto request) { - var models = _repository.GetAllModels(); + ArgumentNullException.ThrowIfNull(request); + + var models = repository.GetAllModels(); var model = models.FirstOrDefault(m => m.Id == request.ModelId); if (model == null) throw new InvalidOperationException("Model not found"); var newBike = new Bike { - Id = _repository.GetAllBikes().Max(b => b.Id) + 1, + Id = repository.GetAllBikes().Max(b => b.Id) + 1, SerialNumber = request.SerialNumber, Model = model, Color = request.Color, IsAvailable = true }; - _repository.AddBike(newBike); + repository.AddBike(newBike); return new BikeDto { @@ -75,12 +76,14 @@ public BikeDto CreateBike(BikeCreateUpdateDto request) /// /// Update bike /// - public BikeDto? UpdateBike(int id, BikeCreateUpdateDto request) + public BikeDto? Update(int id, BikeCreateUpdateDto request) { - var bike = _repository.GetBikeById(id); + ArgumentNullException.ThrowIfNull(request); + + var bike = repository.GetBikeById(id); if (bike == null) return null; - var models = _repository.GetAllModels(); + var models = repository.GetAllModels(); var model = models.FirstOrDefault(m => m.Id == request.ModelId); if (model == null) throw new InvalidOperationException("Model not found"); @@ -89,7 +92,7 @@ public BikeDto CreateBike(BikeCreateUpdateDto request) bike.Model = model; bike.Color = request.Color; - _repository.UpdateBike(bike); + repository.UpdateBike(bike); return new BikeDto { @@ -104,12 +107,12 @@ public BikeDto CreateBike(BikeCreateUpdateDto request) /// /// Delete bike /// - public bool DeleteBike(int id) + public bool Delete(int id) { - var bike = _repository.GetBikeById(id); + var bike = repository.GetBikeById(id); if (bike == null) return false; - _repository.DeleteBike(id); + repository.DeleteBike(id); return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/RentService.cs b/Bikes.Application/Services/RentService.cs index a85953fd1..5a2642ec9 100644 --- a/Bikes.Application/Services/RentService.cs +++ b/Bikes.Application/Services/RentService.cs @@ -1,5 +1,6 @@ using Bikes.Application.Contracts.Rents; using Bikes.Domain.Models; +using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -8,14 +9,12 @@ namespace Bikes.Application.Services; /// public class RentService(IBikeRepository repository) : IRentService { - private readonly IBikeRepository _repository = repository; - /// /// Get all rents /// - public List GetAllRents() + public List GetAll() { - return _repository.GetAllRents().Select(r => new RentDto + return [.. repository.GetAllRents().Select(r => new RentDto { Id = r.Id, BikeId = r.Bike.Id, @@ -23,15 +22,15 @@ public List GetAllRents() StartTime = r.StartTime, DurationHours = r.DurationHours, TotalCost = r.TotalCost - }).ToList(); + })]; } /// /// Get rent by identifier /// - public RentDto? GetRentById(int id) + public RentDto? GetById(int id) { - var rent = _repository.GetAllRents().FirstOrDefault(r => r.Id == id); + var rent = repository.GetRentById(id); return rent == null ? null : new RentDto { Id = rent.Id, @@ -46,14 +45,13 @@ public List GetAllRents() /// /// Create new rent /// - public RentDto CreateRent(RentCreateUpdateDto request) + public RentDto Create(RentCreateUpdateDto request) { - var bikes = _repository.GetAllBikes(); - var renters = _repository.GetAllRenters(); - var rents = _repository.GetAllRents(); + ArgumentNullException.ThrowIfNull(request); - var bike = bikes.FirstOrDefault(b => b.Id == request.BikeId); - var renter = renters.FirstOrDefault(r => r.Id == request.RenterId); + var bike = repository.GetBikeById(request.BikeId); + var renter = repository.GetRenterById(request.RenterId); + var rents = repository.GetAllRents(); if (bike == null) throw new InvalidOperationException("Bike not found"); @@ -69,6 +67,8 @@ public RentDto CreateRent(RentCreateUpdateDto request) DurationHours = request.DurationHours }; + repository.AddRent(newRent); + return new RentDto { Id = newRent.Id, @@ -83,17 +83,15 @@ public RentDto CreateRent(RentCreateUpdateDto request) /// /// Update rent /// - public RentDto? UpdateRent(int id, RentCreateUpdateDto request) + public RentDto? Update(int id, RentCreateUpdateDto request) { - var rents = _repository.GetAllRents(); - var rent = rents.FirstOrDefault(r => r.Id == id); - if (rent == null) return null; + ArgumentNullException.ThrowIfNull(request); - var bikes = _repository.GetAllBikes(); - var renters = _repository.GetAllRenters(); + var rent = repository.GetRentById(id); + if (rent == null) return null; - var bike = bikes.FirstOrDefault(b => b.Id == request.BikeId); - var renter = renters.FirstOrDefault(r => r.Id == request.RenterId); + var bike = repository.GetBikeById(request.BikeId); + var renter = repository.GetRenterById(request.RenterId); if (bike == null) throw new InvalidOperationException("Bike not found"); @@ -105,6 +103,8 @@ public RentDto CreateRent(RentCreateUpdateDto request) rent.StartTime = request.StartTime; rent.DurationHours = request.DurationHours; + repository.UpdateRent(rent); + return new RentDto { Id = rent.Id, @@ -119,10 +119,12 @@ public RentDto CreateRent(RentCreateUpdateDto request) /// /// Delete rent /// - public bool DeleteRent(int id) + public bool Delete(int id) { - var rents = _repository.GetAllRents(); - var rent = rents.FirstOrDefault(r => r.Id == id); - return rent != null; + var rent = repository.GetRentById(id); + if (rent == null) return false; + + repository.DeleteRent(id); + return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/RenterService.cs b/Bikes.Application/Services/RenterService.cs index 66fbcc2f7..afcd1c90f 100644 --- a/Bikes.Application/Services/RenterService.cs +++ b/Bikes.Application/Services/RenterService.cs @@ -1,5 +1,6 @@ using Bikes.Application.Contracts.Renters; using Bikes.Domain.Models; +using Bikes.Domain.Repositories; namespace Bikes.Application.Services; @@ -8,27 +9,25 @@ namespace Bikes.Application.Services; /// public class RenterService(IBikeRepository repository) : IRenterService { - private readonly IBikeRepository _repository = repository; - /// /// Get all renters /// - public List GetAllRenters() + public List GetAll() { - return _repository.GetAllRenters().Select(r => new RenterDto + return [.. repository.GetAllRenters().Select(r => new RenterDto { Id = r.Id, FullName = r.FullName, Phone = r.Phone - }).ToList(); + })]; } /// /// Get renter by identifier /// - public RenterDto? GetRenterById(int id) + public RenterDto? GetById(int id) { - var renter = _repository.GetRenterById(id); + var renter = repository.GetRenterById(id); return renter == null ? null : new RenterDto { Id = renter.Id, @@ -40,9 +39,11 @@ public List GetAllRenters() /// /// Create new renter /// - public RenterDto CreateRenter(RenterCreateUpdateDto request) + public RenterDto Create(RenterCreateUpdateDto request) { - var renters = _repository.GetAllRenters(); + ArgumentNullException.ThrowIfNull(request); + + var renters = repository.GetAllRenters(); var newRenter = new Renter { Id = renters.Count > 0 ? renters.Max(r => r.Id) + 1 : 1, @@ -50,7 +51,7 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) Phone = request.Phone }; - _repository.AddRenter(newRenter); + repository.AddRenter(newRenter); return new RenterDto { @@ -63,15 +64,17 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) /// /// Update renter /// - public RenterDto? UpdateRenter(int id, RenterCreateUpdateDto request) + public RenterDto? Update(int id, RenterCreateUpdateDto request) { - var renter = _repository.GetRenterById(id); + ArgumentNullException.ThrowIfNull(request); + + var renter = repository.GetRenterById(id); if (renter == null) return null; renter.FullName = request.FullName; renter.Phone = request.Phone; - _repository.UpdateRenter(renter); + repository.UpdateRenter(renter); return new RenterDto { @@ -84,12 +87,12 @@ public RenterDto CreateRenter(RenterCreateUpdateDto request) /// /// Delete renter /// - public bool DeleteRenter(int id) + public bool Delete(int id) { - var renter = _repository.GetRenterById(id); + var renter = repository.GetRenterById(id); if (renter == null) return false; - _repository.DeleteRenter(id); + repository.DeleteRenter(id); return true; } } \ No newline at end of file diff --git a/Bikes.Domain/Bikes.Domain.csproj b/Bikes.Domain/Bikes.Domain.csproj index fa71b7ae6..bbf0ab3d6 100644 --- a/Bikes.Domain/Bikes.Domain.csproj +++ b/Bikes.Domain/Bikes.Domain.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Bikes.Domain/Bikes.Domain.sln b/Bikes.Domain/Bikes.Domain.sln deleted file mode 100644 index 8addb5d2a..000000000 --- a/Bikes.Domain/Bikes.Domain.sln +++ /dev/null @@ -1,33 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36603.0 d17.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Domain", "Bikes.Domain.csproj", "{05557513-7E27-4EDC-A5C1-6A421582D538}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Tests", "..\Bikes.Tests\Bikes.Tests.csproj", "{BE6F3941-FC43-4217-9120-94E2C759AA9F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {05557513-7E27-4EDC-A5C1-6A421582D538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05557513-7E27-4EDC-A5C1-6A421582D538}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05557513-7E27-4EDC-A5C1-6A421582D538}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05557513-7E27-4EDC-A5C1-6A421582D538}.Release|Any CPU.Build.0 = Release|Any CPU - {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE6F3941-FC43-4217-9120-94E2C759AA9F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {4BCE112F-1E42-40D3-89D9-1006B2FE29CE} - EndGlobalSection -EndGlobal diff --git a/Bikes.Application/Services/IBikeRepository.cs b/Bikes.Domain/Repositories/IBikeRepository.cs similarity index 96% rename from Bikes.Application/Services/IBikeRepository.cs rename to Bikes.Domain/Repositories/IBikeRepository.cs index f0d29166d..d02595070 100644 --- a/Bikes.Application/Services/IBikeRepository.cs +++ b/Bikes.Domain/Repositories/IBikeRepository.cs @@ -1,6 +1,6 @@ using Bikes.Domain.Models; -namespace Bikes.Application.Services; +namespace Bikes.Domain.Repositories; /// /// Repository for bike data access diff --git a/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj b/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj new file mode 100644 index 000000000..f02574c1f --- /dev/null +++ b/Bikes.Infrastructure.InMemory/Bikes.Infrastructure.InMemory.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + \ No newline at end of file diff --git a/Bikes.Application/Services/InMemoryBikeRepository.cs b/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs similarity index 79% rename from Bikes.Application/Services/InMemoryBikeRepository.cs rename to Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs index 1f057577b..5a191334c 100644 --- a/Bikes.Application/Services/InMemoryBikeRepository.cs +++ b/Bikes.Infrastructure.InMemory/Repositories/InMemoryBikeRepository.cs @@ -1,7 +1,8 @@ using Bikes.Domain.Models; +using Bikes.Domain.Repositories; using Bikes.Tests; -namespace Bikes.Application.Services; +namespace Bikes.Infrastructure.InMemory.Repositories; /// /// In-memory implementation of bike repository @@ -20,17 +21,13 @@ public class InMemoryBikeRepository() : IBikeRepository public void AddBike(Bike bike) { - if (bike == null) - throw new ArgumentNullException(nameof(bike)); - + ArgumentNullException.ThrowIfNull(bike); _bikes.Add(bike); } public void UpdateBike(Bike bike) { - if (bike == null) - throw new ArgumentNullException(nameof(bike)); - + ArgumentNullException.ThrowIfNull(bike); var existingBike = _bikes.FirstOrDefault(b => b.Id == bike.Id); if (existingBike != null) { @@ -48,17 +45,13 @@ public void UpdateBike(Bike bike) public void AddModel(BikeModel model) { - if (model == null) - throw new ArgumentNullException(nameof(model)); - + ArgumentNullException.ThrowIfNull(model); _models.Add(model); } public void UpdateModel(BikeModel model) { - if (model == null) - throw new ArgumentNullException(nameof(model)); - + ArgumentNullException.ThrowIfNull(model); var existingModel = _models.FirstOrDefault(m => m.Id == model.Id); if (existingModel != null) { @@ -76,17 +69,13 @@ public void UpdateModel(BikeModel model) public void AddRenter(Renter renter) { - if (renter == null) - throw new ArgumentNullException(nameof(renter)); - + ArgumentNullException.ThrowIfNull(renter); _renters.Add(renter); } public void UpdateRenter(Renter renter) { - if (renter == null) - throw new ArgumentNullException(nameof(renter)); - + ArgumentNullException.ThrowIfNull(renter); var existingRenter = _renters.FirstOrDefault(r => r.Id == renter.Id); if (existingRenter != null) { @@ -104,17 +93,13 @@ public void UpdateRenter(Renter renter) public void AddRent(Rent rent) { - if (rent == null) - throw new ArgumentNullException(nameof(rent)); - + ArgumentNullException.ThrowIfNull(rent); _rents.Add(rent); } public void UpdateRent(Rent rent) { - if (rent == null) - throw new ArgumentNullException(nameof(rent)); - + ArgumentNullException.ThrowIfNull(rent); var existingRent = _rents.FirstOrDefault(r => r.Id == rent.Id); if (existingRent != null) { diff --git a/Bikes.sln b/Bikes.sln new file mode 100644 index 000000000..390a4da89 --- /dev/null +++ b/Bikes.sln @@ -0,0 +1,107 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Domain", "Bikes.Domain\Bikes.Domain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application.Contracts", "Bikes.Application.Contracts\Bikes.Application.Contracts.csproj", "{375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes.Application\Bikes.Application.csproj", "{221D85D1-A79D-4C32-BA01-E781961721A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api.Host\Bikes.Api.Host.csproj", "{26BE026D-95E7-4C62-A832-7A7A9B6A7D48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.InMemory", "Bikes.Infrastructure.InMemory\Bikes.Infrastructure.InMemory.csproj", "{AF5C88C9-4078-48B6-814A-F64C690D97FF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|x64.Build.0 = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|x86.Build.0 = Debug|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.Build.0 = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|x64.ActiveCfg = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|x64.Build.0 = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|x86.ActiveCfg = Release|Any CPU + {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|x86.Build.0 = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|x64.Build.0 = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|x86.Build.0 = Debug|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.Build.0 = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|x64.ActiveCfg = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|x64.Build.0 = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|x86.ActiveCfg = Release|Any CPU + {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|x86.Build.0 = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|x64.ActiveCfg = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|x64.Build.0 = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|x86.ActiveCfg = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|x86.Build.0 = Debug|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.Build.0 = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|x64.ActiveCfg = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|x64.Build.0 = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|x86.ActiveCfg = Release|Any CPU + {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|x86.Build.0 = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|x64.Build.0 = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Debug|x86.Build.0 = Debug|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|Any CPU.Build.0 = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x64.ActiveCfg = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x64.Build.0 = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x86.ActiveCfg = Release|Any CPU + {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5ABC640F-2193-41DE-939D-657478D0E14B} + EndGlobalSection +EndGlobal diff --git a/lab1.sln b/lab1.sln deleted file mode 100644 index b2b4a9bd9..000000000 --- a/lab1.sln +++ /dev/null @@ -1,48 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Domain", "Bikes.Domain\Bikes.Domain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bikes.Tests", "Bikes.Tests\Bikes.Tests.csproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application.Contracts", "Bikes.Application.Contracts\Bikes.Application.Contracts.csproj", "{375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Application", "Bikes.Application\Bikes.Application.csproj", "{221D85D1-A79D-4C32-BA01-E781961721A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api.Host\Bikes.Api.Host.csproj", "{26BE026D-95E7-4C62-A832-7A7A9B6A7D48}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU - {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {375C2BDE-6F0B-40BD-B6EC-E99F74ACB3C7}.Release|Any CPU.Build.0 = Release|Any CPU - {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {221D85D1-A79D-4C32-BA01-E781961721A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {221D85D1-A79D-4C32-BA01-E781961721A3}.Release|Any CPU.Build.0 = Release|Any CPU - {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26BE026D-95E7-4C62-A832-7A7A9B6A7D48}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {5ABC640F-2193-41DE-939D-657478D0E14B} - EndGlobalSection -EndGlobal From 5033331eb6a413aa9b3975ed8330145d51f873b2 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 6 Nov 2025 13:00:51 +0400 Subject: [PATCH 12/19] refactor: simplify null checks with ?? operator --- Bikes.Application/Services/BikeService.cs | 10 ++++------ Bikes.Application/Services/RentService.cs | 11 ++++------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Bikes.Application/Services/BikeService.cs b/Bikes.Application/Services/BikeService.cs index 736c55f5c..a7fa0df7a 100644 --- a/Bikes.Application/Services/BikeService.cs +++ b/Bikes.Application/Services/BikeService.cs @@ -48,9 +48,8 @@ public BikeDto Create(BikeCreateUpdateDto request) ArgumentNullException.ThrowIfNull(request); var models = repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == request.ModelId); - if (model == null) - throw new InvalidOperationException("Model not found"); + var model = models.FirstOrDefault(m => m.Id == request.ModelId) + ?? throw new InvalidOperationException("Model not found"); var newBike = new Bike { @@ -84,9 +83,8 @@ public BikeDto Create(BikeCreateUpdateDto request) if (bike == null) return null; var models = repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == request.ModelId); - if (model == null) - throw new InvalidOperationException("Model not found"); + var model = models.FirstOrDefault(m => m.Id == request.ModelId) + ?? throw new InvalidOperationException("Model not found"); bike.SerialNumber = request.SerialNumber; bike.Model = model; diff --git a/Bikes.Application/Services/RentService.cs b/Bikes.Application/Services/RentService.cs index 5a2642ec9..591af83b0 100644 --- a/Bikes.Application/Services/RentService.cs +++ b/Bikes.Application/Services/RentService.cs @@ -90,13 +90,10 @@ public RentDto Create(RentCreateUpdateDto request) var rent = repository.GetRentById(id); if (rent == null) return null; - var bike = repository.GetBikeById(request.BikeId); - var renter = repository.GetRenterById(request.RenterId); - - if (bike == null) - throw new InvalidOperationException("Bike not found"); - if (renter == null) - throw new InvalidOperationException("Renter not found"); + var bike = repository.GetBikeById(request.BikeId) + ?? throw new InvalidOperationException("Bike not found"); + var renter = repository.GetRenterById(request.RenterId) + ?? throw new InvalidOperationException("Renter not found"); rent.Bike = bike; rent.Renter = renter; From 87324a1be115777d818914f67ccb11a39e9ba7aa Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Thu, 13 Nov 2025 23:10:10 +0400 Subject: [PATCH 13/19] fix: refactor CRUD controllers and add DTO validation --- Bikes.Api.Host/Bikes.Api.Host.csproj | 1 + .../Controllers/BikeModelsController.cs | 58 ++----------------- Bikes.Api.Host/Controllers/BikesController.cs | 58 ++----------------- .../Controllers/CrudControllerBase.cs | 57 ++++++++++++++---- .../Controllers/RentersController.cs | 58 ++----------------- Bikes.Api.Host/Controllers/RentsController.cs | 58 ++----------------- .../Bikes/BikeCreateUpdateDto.cs | 10 +++- .../Models/BikeModelCreateUpdateDto.cs | 20 ++++++- .../Renters/RenterCreateUpdateDto.cs | 8 ++- .../Rents/RentCreateUpdateDto.cs | 11 +++- 10 files changed, 111 insertions(+), 228 deletions(-) diff --git a/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes.Api.Host/Bikes.Api.Host.csproj index 3d0d1b000..ceb832f22 100644 --- a/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -12,6 +12,7 @@ + diff --git a/Bikes.Api.Host/Controllers/BikeModelsController.cs b/Bikes.Api.Host/Controllers/BikeModelsController.cs index 3a3432830..167b7604d 100644 --- a/Bikes.Api.Host/Controllers/BikeModelsController.cs +++ b/Bikes.Api.Host/Controllers/BikeModelsController.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Contracts.Models; +using Bikes.Application.Contracts; +using Bikes.Application.Contracts.Models; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -8,57 +9,8 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] -public class BikeModelsController(IBikeModelService bikeModelService) : CrudControllerBase +public class BikeModelsController(IBikeModelService bikeModelService) + : CrudControllerBase { - /// - /// Get all bike models - /// - [HttpGet] - public override Task>> GetAll() - { - try - { - var models = bikeModelService.GetAll(); - return Task.FromResult>>(Ok(models)); - } - catch (Exception ex) - { - return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - /// - /// Get bike model by id - /// - [HttpGet("{id}")] - public override Task> GetById(int id) - { - try - { - var model = bikeModelService.GetById(id); - return Task.FromResult>(model == null ? NotFound() : Ok(model)); - } - catch (Exception ex) - { - return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - protected override Task> CreateInternal(BikeModelCreateUpdateDto request) - { - var model = bikeModelService.Create(request); - return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = model.Id }, model)); - } - - protected override Task> UpdateInternal(int id, BikeModelCreateUpdateDto request) - { - var model = bikeModelService.Update(id, request); - return Task.FromResult>(model == null ? NotFound() : Ok(model)); - } - - protected override Task DeleteInternal(int id) - { - var result = bikeModelService.Delete(id); - return Task.FromResult(result ? NoContent() : NotFound()); - } + protected override IApplicationService Service => bikeModelService; } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/BikesController.cs b/Bikes.Api.Host/Controllers/BikesController.cs index 8908fa7a1..c2f349706 100644 --- a/Bikes.Api.Host/Controllers/BikesController.cs +++ b/Bikes.Api.Host/Controllers/BikesController.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Contracts.Bikes; +using Bikes.Application.Contracts; +using Bikes.Application.Contracts.Bikes; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -8,57 +9,8 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] -public class BikesController(IBikeService bikeService) : CrudControllerBase +public class BikesController(IBikeService bikeService) + : CrudControllerBase { - /// - /// Get all bikes - /// - [HttpGet] - public override Task>> GetAll() - { - try - { - var bikes = bikeService.GetAll(); - return Task.FromResult>>(Ok(bikes)); - } - catch (Exception ex) - { - return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - /// - /// Get bike by id - /// - [HttpGet("{id}")] - public override Task> GetById(int id) - { - try - { - var bike = bikeService.GetById(id); - return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); - } - catch (Exception ex) - { - return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - protected override Task> CreateInternal(BikeCreateUpdateDto request) - { - var bike = bikeService.Create(request); - return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = bike.Id }, bike)); - } - - protected override Task> UpdateInternal(int id, BikeCreateUpdateDto request) - { - var bike = bikeService.Update(id, request); - return Task.FromResult>(bike == null ? NotFound() : Ok(bike)); - } - - protected override Task DeleteInternal(int id) - { - var result = bikeService.Delete(id); - return Task.FromResult(result ? NoContent() : NotFound()); - } + protected override IApplicationService Service => bikeService; } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/CrudControllerBase.cs b/Bikes.Api.Host/Controllers/CrudControllerBase.cs index c56db80ac..99ee80608 100644 --- a/Bikes.Api.Host/Controllers/CrudControllerBase.cs +++ b/Bikes.Api.Host/Controllers/CrudControllerBase.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Bikes.Application.Contracts; +using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -13,23 +14,47 @@ public abstract class CrudControllerBase : ControllerBas where TDto : class where TCreateUpdateDto : class { + protected abstract IApplicationService Service { get; } + /// /// Get all entities /// [HttpGet] - public abstract Task>> GetAll(); + public virtual ActionResult> GetAll() + { + try + { + var entities = Service.GetAll(); + return Ok(entities); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } /// /// Get entity by id /// [HttpGet("{id}")] - public abstract Task> GetById(int id); + public virtual ActionResult GetById(int id) + { + try + { + var entity = Service.GetById(id); + return entity == null ? NotFound() : Ok(entity); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } /// /// Create new entity /// [HttpPost] - public virtual async Task> Create([FromBody] TCreateUpdateDto request) + public virtual ActionResult Create([FromBody] TCreateUpdateDto request) { try { @@ -38,7 +63,8 @@ public virtual async Task> Create([FromBody] TCreateUpdateDto return BadRequest(ModelState); } - return await CreateInternal(request); + var entity = Service.Create(request); + return CreatedAtAction(nameof(GetById), new { id = GetEntityId(entity) }, entity); } catch (InvalidOperationException ex) { @@ -54,7 +80,7 @@ public virtual async Task> Create([FromBody] TCreateUpdateDto /// Update entity /// [HttpPut("{id}")] - public virtual async Task> Update(int id, [FromBody] TCreateUpdateDto request) + public virtual ActionResult Update(int id, [FromBody] TCreateUpdateDto request) { try { @@ -63,7 +89,8 @@ public virtual async Task> Update(int id, [FromBody] TCreateU return BadRequest(ModelState); } - return await UpdateInternal(id, request); + var entity = Service.Update(id, request); + return entity == null ? NotFound() : Ok(entity); } catch (InvalidOperationException ex) { @@ -79,11 +106,12 @@ public virtual async Task> Update(int id, [FromBody] TCreateU /// Delete entity /// [HttpDelete("{id}")] - public virtual async Task Delete(int id) + public virtual ActionResult Delete(int id) { try { - return await DeleteInternal(id); + var result = Service.Delete(id); + return result ? NoContent() : NotFound(); } catch (Exception ex) { @@ -91,7 +119,12 @@ public virtual async Task Delete(int id) } } - protected abstract Task> CreateInternal(TCreateUpdateDto request); - protected abstract Task> UpdateInternal(int id, TCreateUpdateDto request); - protected abstract Task DeleteInternal(int id); + /// + /// Extract entity ID for CreatedAtAction + /// + private static int GetEntityId(TDto entity) + { + var idProperty = typeof(TDto).GetProperty("Id"); + return idProperty != null ? (int)idProperty.GetValue(entity)! : 0; + } } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentersController.cs b/Bikes.Api.Host/Controllers/RentersController.cs index 3f1b4173b..4a12de855 100644 --- a/Bikes.Api.Host/Controllers/RentersController.cs +++ b/Bikes.Api.Host/Controllers/RentersController.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Contracts.Renters; +using Bikes.Application.Contracts; +using Bikes.Application.Contracts.Renters; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -8,57 +9,8 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] -public class RentersController(IRenterService renterService) : CrudControllerBase +public class RentersController(IRenterService renterService) + : CrudControllerBase { - /// - /// Get all renters - /// - [HttpGet] - public override Task>> GetAll() - { - try - { - var renters = renterService.GetAll(); - return Task.FromResult>>(Ok(renters)); - } - catch (Exception ex) - { - return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - /// - /// Get renter by id - /// - [HttpGet("{id}")] - public override Task> GetById(int id) - { - try - { - var renter = renterService.GetById(id); - return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); - } - catch (Exception ex) - { - return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - protected override Task> CreateInternal(RenterCreateUpdateDto request) - { - var renter = renterService.Create(request); - return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = renter.Id }, renter)); - } - - protected override Task> UpdateInternal(int id, RenterCreateUpdateDto request) - { - var renter = renterService.Update(id, request); - return Task.FromResult>(renter == null ? NotFound() : Ok(renter)); - } - - protected override Task DeleteInternal(int id) - { - var result = renterService.Delete(id); - return Task.FromResult(result ? NoContent() : NotFound()); - } + protected override IApplicationService Service => renterService; } \ No newline at end of file diff --git a/Bikes.Api.Host/Controllers/RentsController.cs b/Bikes.Api.Host/Controllers/RentsController.cs index 8f102bbbe..8ed05ef61 100644 --- a/Bikes.Api.Host/Controllers/RentsController.cs +++ b/Bikes.Api.Host/Controllers/RentsController.cs @@ -1,4 +1,5 @@ -using Bikes.Application.Contracts.Rents; +using Bikes.Application.Contracts; +using Bikes.Application.Contracts.Rents; using Microsoft.AspNetCore.Mvc; namespace Bikes.Api.Host.Controllers; @@ -8,57 +9,8 @@ namespace Bikes.Api.Host.Controllers; /// [ApiController] [Route("api/[controller]")] -public class RentsController(IRentService rentService) : CrudControllerBase +public class RentsController(IRentService rentService) + : CrudControllerBase { - /// - /// Get all rents - /// - [HttpGet] - public override Task>> GetAll() - { - try - { - var rents = rentService.GetAll(); - return Task.FromResult>>(Ok(rents)); - } - catch (Exception ex) - { - return Task.FromResult>>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - /// - /// Get rent by id - /// - [HttpGet("{id}")] - public override Task> GetById(int id) - { - try - { - var rent = rentService.GetById(id); - return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); - } - catch (Exception ex) - { - return Task.FromResult>(StatusCode(500, $"Internal server error: {ex.Message}")); - } - } - - protected override Task> CreateInternal(RentCreateUpdateDto request) - { - var rent = rentService.Create(request); - return Task.FromResult>(CreatedAtAction(nameof(GetById), new { id = rent.Id }, rent)); - } - - protected override Task> UpdateInternal(int id, RentCreateUpdateDto request) - { - var rent = rentService.Update(id, request); - return Task.FromResult>(rent == null ? NotFound() : Ok(rent)); - } - - protected override Task DeleteInternal(int id) - { - var result = rentService.Delete(id); - return Task.FromResult(result ? NoContent() : NotFound()); - } + protected override IApplicationService Service => rentService; } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs b/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs index 2a82cd49a..b7618bae2 100644 --- a/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs +++ b/Bikes.Application.Contracts/Bikes/BikeCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Application.Contracts.Bikes; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Application.Contracts.Bikes; /// /// DTO for creating and updating bikes @@ -8,15 +10,21 @@ public class BikeCreateUpdateDto /// /// Bike serial number /// + [Required(ErrorMessage = "Serial number is required")] + [StringLength(50, MinimumLength = 3, ErrorMessage = "Serial number must be between 3 and 50 characters")] public required string SerialNumber { get; set; } /// /// Bike model identifier /// + [Required(ErrorMessage = "Model ID is required")] + [Range(1, int.MaxValue, ErrorMessage = "Model ID must be a positive number")] public required int ModelId { get; set; } /// /// Bike color /// + [Required(ErrorMessage = "Color is required")] + [StringLength(30, MinimumLength = 2, ErrorMessage = "Color must be between 2 and 30 characters")] public required string Color { get; set; } } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs b/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs index 3caebfc4c..93173580e 100644 --- a/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs +++ b/Bikes.Application.Contracts/Models/BikeModelCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Application.Contracts.Models; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Application.Contracts.Models; /// /// DTO for creating and updating bike models @@ -8,40 +10,56 @@ public class BikeModelCreateUpdateDto /// /// Model name /// + [Required(ErrorMessage = "Model name is required")] + [StringLength(100, MinimumLength = 2, ErrorMessage = "Model name must be between 2 and 100 characters")] public required string Name { get; set; } /// /// Bike type /// + [Required(ErrorMessage = "Bike type is required")] + [RegularExpression("^(Mountain|Road|Hybrid|City|Sport)$", ErrorMessage = "Bike type must be Mountain, Road, Hybrid, City, or Sport")] public required string Type { get; set; } /// /// Wheel size in inches /// + [Required(ErrorMessage = "Wheel size is required")] + [Range(10, 30, ErrorMessage = "Wheel size must be between 10 and 30 inches")] public required decimal WheelSize { get; set; } /// /// Maximum weight capacity in kg /// + [Required(ErrorMessage = "Maximum weight capacity is required")] + [Range(50, 300, ErrorMessage = "Maximum weight capacity must be between 50 and 300 kg")] public required decimal MaxWeight { get; set; } /// /// Bike weight in kg /// + [Required(ErrorMessage = "Bike weight is required")] + [Range(5, 50, ErrorMessage = "Bike weight must be between 5 and 50 kg")] public required decimal Weight { get; set; } /// /// Brake type /// + [Required(ErrorMessage = "Brake type is required")] + [RegularExpression("^(Mechanical|Hydraulic|Rim)$", ErrorMessage = "Brake type must be Mechanical, Hydraulic, or Rim")] public required string BrakeType { get; set; } /// /// Model year /// + [Required(ErrorMessage = "Model year is required")] + [Range(2000, 2030, ErrorMessage = "Model year must be between 2000 and 2030")] public required int ModelYear { get; set; } /// /// Price per rental hour /// + [Required(ErrorMessage = "Price per hour is required")] + [Range(0.01, 1000, ErrorMessage = "Price per hour must be between 0.01 and 1000")] public required decimal PricePerHour { get; set; } } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs b/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs index ac3dc7b53..8e9c26154 100644 --- a/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs +++ b/Bikes.Application.Contracts/Renters/RenterCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Application.Contracts.Renters; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Application.Contracts.Renters; /// /// DTO for creating and updating renters @@ -8,10 +10,14 @@ public class RenterCreateUpdateDto /// /// Renter full name /// + [Required(ErrorMessage = "Full name is required")] + [StringLength(100, MinimumLength = 2, ErrorMessage = "Full name must be between 2 and 100 characters")] public required string FullName { get; set; } /// /// Contact phone number /// + [Required(ErrorMessage = "Phone number is required")] + [RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Phone number must be in valid international format")] public required string Phone { get; set; } } \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs b/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs index 0eebcd51e..4c5c3cf34 100644 --- a/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs +++ b/Bikes.Application.Contracts/Rents/RentCreateUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Bikes.Application.Contracts.Rents; +using System.ComponentModel.DataAnnotations; + +namespace Bikes.Application.Contracts.Rents; /// /// DTO for creating and updating rents @@ -8,20 +10,27 @@ public class RentCreateUpdateDto /// /// Bike identifier /// + [Required(ErrorMessage = "Bike ID is required")] + [Range(1, int.MaxValue, ErrorMessage = "Bike ID must be a positive number")] public required int BikeId { get; set; } /// /// Renter identifier /// + [Required(ErrorMessage = "Renter ID is required")] + [Range(1, int.MaxValue, ErrorMessage = "Renter ID must be a positive number")] public required int RenterId { get; set; } /// /// Rental start time /// + [Required(ErrorMessage = "Start time is required")] public required DateTime StartTime { get; set; } /// /// Rental duration in hours /// + [Required(ErrorMessage = "Duration is required")] + [Range(1, 24 * 7, ErrorMessage = "Duration must be between 1 and 168 hours (1 week)")] public required int DurationHours { get; set; } } \ No newline at end of file From 6e512f53af71dcd4ebdb42e46afe64cc0b24354d Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Mon, 24 Nov 2025 23:43:24 +0400 Subject: [PATCH 14/19] feat: migrate to PostgreSQL with EF Core - Replace InMemory storage with PostgreSQL database - Add EF Core migrations and DbContext configuration - Implement DataSeeder for test data - Fix DateTime UTC compatibility issues - Update DI container for PostgreSQL integration --- Bikes.Api.Host/Bikes.Api.Host.csproj | 10 +- Bikes.Api.Host/Program.cs | 27 ++- Bikes.Api.Host/appsettings.json | 5 +- Bikes.Domain/Models/Bike.cs | 28 ++- Bikes.Domain/Models/BikeModel.cs | 29 ++- Bikes.Domain/Models/Rent.cs | 31 ++- Bikes.Domain/Models/Renter.cs | 17 +- .../Bikes.Infrastructure.EfCore.csproj | 26 ++ Bikes.Infrastructure.EfCore/BikesDbContext.cs | 91 +++++++ Bikes.Infrastructure.EfCore/DataSeeder.cs | 160 +++++++++++++ .../EfCoreBikeRepository.cs | 225 +++++++++++++++++ .../20251117201300_Initial.Designer.cs | 226 ++++++++++++++++++ .../Migrations/20251117201300_Initial.cs | 131 ++++++++++ .../Migrations/BikesDbContextModelSnapshot.cs | 223 +++++++++++++++++ .../Properties/launchSettings.json | 12 + Bikes.sln | 14 ++ Infrastructure.EfCore/BikesDbContext.cs | 6 + .../Infrastructure.EfCore.csproj | 23 ++ 18 files changed, 1263 insertions(+), 21 deletions(-) create mode 100644 Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj create mode 100644 Bikes.Infrastructure.EfCore/BikesDbContext.cs create mode 100644 Bikes.Infrastructure.EfCore/DataSeeder.cs create mode 100644 Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs create mode 100644 Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.Designer.cs create mode 100644 Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs create mode 100644 Bikes.Infrastructure.EfCore/Migrations/BikesDbContextModelSnapshot.cs create mode 100644 Bikes.Infrastructure.EfCore/Properties/launchSettings.json create mode 100644 Infrastructure.EfCore/BikesDbContext.cs create mode 100644 Infrastructure.EfCore/Infrastructure.EfCore.csproj diff --git a/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes.Api.Host/Bikes.Api.Host.csproj index ceb832f22..b25b74a84 100644 --- a/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -1,19 +1,19 @@  - net8.0 enable enable - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - + - \ No newline at end of file diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs index 4f9bd3c23..1aaf115b7 100644 --- a/Bikes.Api.Host/Program.cs +++ b/Bikes.Api.Host/Program.cs @@ -5,7 +5,8 @@ using Bikes.Application.Contracts.Renters; using Bikes.Application.Contracts.Rents; using Bikes.Domain.Repositories; -using Bikes.Infrastructure.InMemory.Repositories; +using Bikes.Infrastructure.EfCore; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -14,13 +15,17 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// Register services with interfaces -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +// Register DbContext with PostgreSQL +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Register services - Scoped +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -35,4 +40,10 @@ app.UseAuthorization(); app.MapControllers(); +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + context.Seed(); +} + app.Run(); \ No newline at end of file diff --git a/Bikes.Api.Host/appsettings.json b/Bikes.Api.Host/appsettings.json index 10f68b8c8..232f36baf 100644 --- a/Bikes.Api.Host/appsettings.json +++ b/Bikes.Api.Host/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=bikes_db;Username=postgres;Password=12345" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -6,4 +9,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/Bikes.Domain/Models/Bike.cs b/Bikes.Domain/Models/Bike.cs index 56fd7757b..020182a32 100644 --- a/Bikes.Domain/Models/Bike.cs +++ b/Bikes.Domain/Models/Bike.cs @@ -1,32 +1,56 @@ -namespace Bikes.Domain.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Bikes.Domain.Models; /// /// Bicycle entity /// +[Table("bikes")] public class Bike { /// /// Unique identifier /// + [Key] + [Column("id")] public int Id { get; set; } /// /// Serial number of the bicycle /// + [Required] + [Column("serial_number")] public required string SerialNumber { get; set; } + /// + /// Foreign key for bike model + /// + [Required] + [Column("model_id")] + public int ModelId { get; set; } + /// /// Reference to bike model /// - public required BikeModel Model { get; set; } + public virtual BikeModel Model { get; set; } = null!; /// /// Color of the bicycle /// + [Required] + [Column("color")] public required string Color { get; set; } /// /// Availability status for rental /// + [Required] + [Column("is_available")] public bool IsAvailable { get; set; } = true; + + /// + /// Navigation property for rents + /// + public virtual List Rents { get; set; } = new(); } \ No newline at end of file diff --git a/Bikes.Domain/Models/BikeModel.cs b/Bikes.Domain/Models/BikeModel.cs index 6e6045aa5..0cb654ab1 100644 --- a/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes.Domain/Models/BikeModel.cs @@ -1,52 +1,79 @@ -namespace Bikes.Domain.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Bikes.Domain.Models; /// /// Bicycle model information /// +[Table("bike_models")] public class BikeModel { /// /// Unique identifier /// + [Key] + [Column("id")] public int Id { get; set; } /// /// Model name /// + [Required] + [Column("name")] public required string Name { get; set; } /// /// Type of bicycle /// + [Required] + [Column("type")] public required BikeType Type { get; set; } /// /// Wheel size in inches /// + [Required] + [Column("wheel_size")] public required decimal WheelSize { get; set; } /// /// Maximum permissible passenger weight in kg /// + [Required] + [Column("max_weight")] public required decimal MaxWeight { get; set; } /// /// Bicycle weight in kg /// + [Required] + [Column("weight")] public required decimal Weight { get; set; } /// /// Type of brakes /// + [Required] + [Column("brake_type")] public required string BrakeType { get; set; } /// /// Model year /// + [Required] + [Column("model_year")] public required int ModelYear { get; set; } /// /// Price per hour of rental /// + [Required] + [Column("price_per_hour")] public required decimal PricePerHour { get; set; } + + /// + /// Navigation property for bikes + /// + public virtual List Bikes { get; set; } = new(); } \ No newline at end of file diff --git a/Bikes.Domain/Models/Rent.cs b/Bikes.Domain/Models/Rent.cs index 4b48bed26..60fc6ddeb 100644 --- a/Bikes.Domain/Models/Rent.cs +++ b/Bikes.Domain/Models/Rent.cs @@ -1,37 +1,62 @@ -namespace Bikes.Domain.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Bikes.Domain.Models; /// /// Bicycle rental record /// +[Table("rents")] public class Rent { /// /// Unique identifier /// + [Key] + [Column("id")] public int Id { get; set; } + /// + /// Foreign key for bike + /// + [Required] + [Column("bike_id")] + public int BikeId { get; set; } + /// /// Reference to rented bicycle /// - public required Bike Bike { get; set; } + public virtual Bike Bike { get; set; } = null!; + + /// + /// Foreign key for renter + /// + [Required] + [Column("renter_id")] + public int RenterId { get; set; } /// /// Reference to renter /// - public required Renter Renter { get; set; } + public virtual Renter Renter { get; set; } = null!; /// /// Rental start time /// + [Required] + [Column("start_time")] public required DateTime StartTime { get; set; } /// /// Rental duration in hours /// + [Required] + [Column("duration_hours")] public required int DurationHours { get; set; } /// /// Total rental cost /// + [Column("total_cost")] public decimal TotalCost => Bike.Model.PricePerHour * DurationHours; } \ No newline at end of file diff --git a/Bikes.Domain/Models/Renter.cs b/Bikes.Domain/Models/Renter.cs index c6925dca4..e680d1abb 100644 --- a/Bikes.Domain/Models/Renter.cs +++ b/Bikes.Domain/Models/Renter.cs @@ -1,22 +1,37 @@ -namespace Bikes.Domain.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Bikes.Domain.Models; /// /// Bicycle renter information /// +[Table("renters")] public class Renter { /// /// Unique identifier /// + [Key] + [Column("id")] public int Id { get; set; } /// /// Full name of the renter /// + [Required] + [Column("full_name")] public required string FullName { get; set; } /// /// Contact phone number /// + [Required] + [Column("phone")] public required string Phone { get; set; } + + /// + /// Navigation property for rents + /// + public virtual List Rents { get; set; } = new(); } \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj b/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj new file mode 100644 index 000000000..b6b7afa98 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/BikesDbContext.cs b/Bikes.Infrastructure.EfCore/BikesDbContext.cs new file mode 100644 index 000000000..37897241e --- /dev/null +++ b/Bikes.Infrastructure.EfCore/BikesDbContext.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore; +using Bikes.Domain.Models; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// Database context for bikes rental system +/// +public class BikesDbContext : DbContext +{ + public BikesDbContext(DbContextOptions options) : base(options) + { + } + + /// + /// Bike models table + /// + public DbSet BikeModels { get; set; } = null!; + + /// + /// Bikes table + /// + public DbSet Bikes { get; set; } = null!; + + /// + /// Renters table + /// + public DbSet Renters { get; set; } = null!; + + /// + /// Rents table + /// + public DbSet Rents { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // BikeModel configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); + entity.Property(e => e.Type).IsRequired().HasConversion(); + entity.Property(e => e.WheelSize).HasPrecision(5, 2); + entity.Property(e => e.MaxWeight).HasPrecision(7, 2); + entity.Property(e => e.Weight).HasPrecision(7, 2); + entity.Property(e => e.BrakeType).IsRequired().HasMaxLength(50); + entity.Property(e => e.PricePerHour).HasPrecision(10, 2); + }); + + // Bike configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.SerialNumber).IsRequired().HasMaxLength(50); + entity.Property(e => e.Color).IsRequired().HasMaxLength(30); + + entity.HasOne(e => e.Model) + .WithMany(m => m.Bikes) + .HasForeignKey(e => e.ModelId) + .OnDelete(DeleteBehavior.Restrict); + }); + + // Renter configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.FullName).IsRequired().HasMaxLength(100); + entity.Property(e => e.Phone).IsRequired().HasMaxLength(20); + }); + + // Rent configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.StartTime).IsRequired(); + entity.Property(e => e.DurationHours).IsRequired(); + + entity.HasOne(e => e.Bike) + .WithMany(b => b.Rents) + .HasForeignKey(e => e.BikeId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.Renter) + .WithMany(r => r.Rents) + .HasForeignKey(e => e.RenterId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/DataSeeder.cs b/Bikes.Infrastructure.EfCore/DataSeeder.cs new file mode 100644 index 000000000..86d509ad9 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/DataSeeder.cs @@ -0,0 +1,160 @@ +using Bikes.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// Seeds initial data to database +/// +public static class DataSeeder +{ + /// + /// Seed initial data + /// + public static void Seed(this BikesDbContext context) + { + if (context.BikeModels.Any()) return; + + var models = InitializeModels(); + var bikes = InitializeBikes(models); + var renters = InitializeRenters(); + var rents = InitializeRents(bikes, renters); + + context.BikeModels.AddRange(models); + context.Bikes.AddRange(bikes); + context.Renters.AddRange(renters); + context.Rents.AddRange(rents); + + context.SaveChanges(); + } + + private static List InitializeModels() + { + return + [ + new() { Id = 1, Name = "Sport Pro 1000", Type = BikeType.Sport, WheelSize = 28, MaxWeight = 120, Weight = 10, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 15 }, + new() { Id = 2, Name = "Mountain Extreme", Type = BikeType.Mountain, WheelSize = 29, MaxWeight = 130, Weight = 12, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 12 }, + new() { Id = 3, Name = "Road Racer", Type = BikeType.Road, WheelSize = 26, MaxWeight = 110, Weight = 8, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 10 }, + new() { Id = 4, Name = "Sport Elite", Type = BikeType.Sport, WheelSize = 27.5m, MaxWeight = 125, Weight = 11, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 14 }, + new() { Id = 5, Name = "Hybrid Comfort", Type = BikeType.Hybrid, WheelSize = 28, MaxWeight = 135, Weight = 13, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 8 }, + new() { Id = 6, Name = "Electric City", Type = BikeType.Electric, WheelSize = 26, MaxWeight = 140, Weight = 20, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 20 }, + new() { Id = 7, Name = "Sport Lightning", Type = BikeType.Sport, WheelSize = 29, MaxWeight = 115, Weight = 9.5m, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 16 }, + new() { Id = 8, Name = "Mountain King", Type = BikeType.Mountain, WheelSize = 27.5m, MaxWeight = 128, Weight = 11.5m, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 13 }, + new() { Id = 9, Name = "Road Speed", Type = BikeType.Road, WheelSize = 28, MaxWeight = 105, Weight = 7.5m, BrakeType = "Rim", ModelYear = 2023, PricePerHour = 11 }, + new() { Id = 10, Name = "Sport Thunder", Type = BikeType.Sport, WheelSize = 26, MaxWeight = 122, Weight = 10.5m, BrakeType = "Disc", ModelYear = 2023, PricePerHour = 15.5m }, + new() { Id = 11, Name = "Electric Mountain", Type = BikeType.Electric, WheelSize = 29, MaxWeight = 145, Weight = 22, BrakeType = "Hydraulic", ModelYear = 2023, PricePerHour = 25 } + ]; + } + + private static List InitializeBikes(List models) + { + var bikes = new List(); + var colors = new[] { "Red", "Blue", "Green", "Black", "White", "Yellow" }; + + var bikeConfigurations = new[] + { + new { ModelIndex = 0, ColorIndex = 0 }, + new { ModelIndex = 1, ColorIndex = 1 }, + new { ModelIndex = 2, ColorIndex = 2 }, + new { ModelIndex = 3, ColorIndex = 3 }, + new { ModelIndex = 4, ColorIndex = 4 }, + new { ModelIndex = 5, ColorIndex = 5 }, + new { ModelIndex = 6, ColorIndex = 0 }, + new { ModelIndex = 7, ColorIndex = 1 }, + new { ModelIndex = 8, ColorIndex = 2 }, + new { ModelIndex = 9, ColorIndex = 3 }, + new { ModelIndex = 10, ColorIndex = 4 }, + new { ModelIndex = 0, ColorIndex = 5 }, + new { ModelIndex = 1, ColorIndex = 0 }, + new { ModelIndex = 2, ColorIndex = 1 }, + new { ModelIndex = 3, ColorIndex = 2 } + }; + + for (var i = 0; i < bikeConfigurations.Length; i++) + { + var config = bikeConfigurations[i]; + var model = models[config.ModelIndex]; + var color = colors[config.ColorIndex]; + + bikes.Add(new Bike + { + Id = i + 1, + SerialNumber = $"SN{(i + 1):D6}", + ModelId = model.Id, + Model = model, + Color = color, + IsAvailable = i % 2 == 0 + }); + } + + return bikes; + } + + private static List InitializeRenters() + { + return + [ + new() { Id = 1, FullName = "Ivanov Ivan", Phone = "+79111111111" }, + new() { Id = 2, FullName = "Petrov Petr", Phone = "+79112222222" }, + new() { Id = 3, FullName = "Sidorov Alexey", Phone = "+79113333333" }, + new() { Id = 4, FullName = "Kuznetsova Maria", Phone = "+79114444444" }, + new() { Id = 5, FullName = "Smirnov Dmitry", Phone = "+79115555555" }, + new() { Id = 6, FullName = "Vasilyeva Ekaterina", Phone = "+79116666666" }, + new() { Id = 7, FullName = "Popov Artem", Phone = "+79117777777" }, + new() { Id = 8, FullName = "Lebedeva Olga", Phone = "+79118888888" }, + new() { Id = 9, FullName = "Novikov Sergey", Phone = "+79119999999" }, + new() { Id = 10, FullName = "Morozova Anna", Phone = "+79110000000" }, + new() { Id = 11, FullName = "Volkov Pavel", Phone = "+79121111111" }, + new() { Id = 12, FullName = "Sokolova Irina", Phone = "+79122222222" } + ]; + } + + private static List InitializeRents(List bikes, List renters) + { + var rents = new List(); + var rentId = 1; + + var rentalData = new[] + { + new { BikeIndex = 0, RenterIndex = 0, Duration = 5, DaysAgo = 2 }, + new { BikeIndex = 1, RenterIndex = 1, Duration = 3, DaysAgo = 5 }, + new { BikeIndex = 2, RenterIndex = 2, Duration = 8, DaysAgo = 1 }, + new { BikeIndex = 0, RenterIndex = 3, Duration = 2, DaysAgo = 7 }, + new { BikeIndex = 3, RenterIndex = 4, Duration = 12, DaysAgo = 3 }, + new { BikeIndex = 1, RenterIndex = 5, Duration = 6, DaysAgo = 4 }, + new { BikeIndex = 4, RenterIndex = 6, Duration = 4, DaysAgo = 6 }, + new { BikeIndex = 2, RenterIndex = 7, Duration = 10, DaysAgo = 2 }, + new { BikeIndex = 5, RenterIndex = 8, Duration = 7, DaysAgo = 1 }, + new { BikeIndex = 3, RenterIndex = 9, Duration = 3, DaysAgo = 8 }, + new { BikeIndex = 6, RenterIndex = 10, Duration = 15, DaysAgo = 2 }, + new { BikeIndex = 4, RenterIndex = 11, Duration = 9, DaysAgo = 5 }, + new { BikeIndex = 7, RenterIndex = 0, Duration = 2, DaysAgo = 3 }, + new { BikeIndex = 5, RenterIndex = 1, Duration = 6, DaysAgo = 4 }, + new { BikeIndex = 8, RenterIndex = 2, Duration = 11, DaysAgo = 1 }, + new { BikeIndex = 6, RenterIndex = 3, Duration = 4, DaysAgo = 7 }, + new { BikeIndex = 9, RenterIndex = 4, Duration = 8, DaysAgo = 2 }, + new { BikeIndex = 7, RenterIndex = 5, Duration = 5, DaysAgo = 5 }, + new { BikeIndex = 10, RenterIndex = 6, Duration = 13, DaysAgo = 3 }, + new { BikeIndex = 8, RenterIndex = 7, Duration = 7, DaysAgo = 4 } + }; + + foreach (var data in rentalData) + { + var bike = bikes[data.BikeIndex]; + var renter = renters[data.RenterIndex]; + + rents.Add(new Rent + { + Id = rentId++, + BikeId = bike.Id, + Bike = bike, + RenterId = renter.Id, + Renter = renter, + StartTime = DateTime.UtcNow.AddDays(-data.DaysAgo), + DurationHours = data.Duration + }); + } + + return rents; + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs new file mode 100644 index 000000000..49d6f1900 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs @@ -0,0 +1,225 @@ +using Microsoft.EntityFrameworkCore; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// EF Core implementation of bike repository +/// +public class EfCoreBikeRepository(BikesDbContext context) : IBikeRepository +{ + /// + /// Get all bikes + /// + public List GetAllBikes() + { + return [.. context.Bikes + .Include(b => b.Model) + .AsNoTracking()]; + } + + /// + /// Get bike by identifier + /// + public Bike? GetBikeById(int id) + { + return context.Bikes + .Include(b => b.Model) + .AsNoTracking() + .FirstOrDefault(b => b.Id == id); + } + + /// + /// Add new bike + /// + public void AddBike(Bike bike) + { + ArgumentNullException.ThrowIfNull(bike); + context.Bikes.Add(bike); + context.SaveChanges(); + } + + /// + /// Update bike + /// + public void UpdateBike(Bike bike) + { + ArgumentNullException.ThrowIfNull(bike); + context.Bikes.Update(bike); + context.SaveChanges(); + } + + /// + /// Delete bike + /// + public void DeleteBike(int id) + { + var bike = context.Bikes.Find(id); + if (bike != null) + { + context.Bikes.Remove(bike); + context.SaveChanges(); + } + } + + /// + /// Get all bike models + /// + public List GetAllModels() + { + return [.. context.BikeModels.AsNoTracking()]; + } + + /// + /// Get bike model by identifier + /// + public BikeModel? GetModelById(int id) + { + return context.BikeModels + .AsNoTracking() + .FirstOrDefault(m => m.Id == id); + } + + /// + /// Add new bike model + /// + public void AddModel(BikeModel model) + { + ArgumentNullException.ThrowIfNull(model); + context.BikeModels.Add(model); + context.SaveChanges(); + } + + /// + /// Update bike model + /// + public void UpdateModel(BikeModel model) + { + ArgumentNullException.ThrowIfNull(model); + context.BikeModels.Update(model); + context.SaveChanges(); + } + + /// + /// Delete bike model + /// + public void DeleteModel(int id) + { + var model = context.BikeModels.Find(id); + if (model != null) + { + context.BikeModels.Remove(model); + context.SaveChanges(); + } + } + + /// + /// Get all renters + /// + public List GetAllRenters() + { + return [.. context.Renters.AsNoTracking()]; + } + + /// + /// Get renter by identifier + /// + public Renter? GetRenterById(int id) + { + return context.Renters + .AsNoTracking() + .FirstOrDefault(r => r.Id == id); + } + + /// + /// Add new renter + /// + public void AddRenter(Renter renter) + { + ArgumentNullException.ThrowIfNull(renter); + context.Renters.Add(renter); + context.SaveChanges(); + } + + /// + /// Update renter + /// + public void UpdateRenter(Renter renter) + { + ArgumentNullException.ThrowIfNull(renter); + context.Renters.Update(renter); + context.SaveChanges(); + } + + /// + /// Delete renter + /// + public void DeleteRenter(int id) + { + var renter = context.Renters.Find(id); + if (renter != null) + { + context.Renters.Remove(renter); + context.SaveChanges(); + } + } + + /// + /// Get all rents + /// + public List GetAllRents() + { + return [.. context.Rents + .Include(r => r.Bike) + .ThenInclude(b => b.Model) + .Include(r => r.Renter) + .AsNoTracking()]; + } + + /// + /// Get rent by identifier + /// + public Rent? GetRentById(int id) + { + return context.Rents + .Include(r => r.Bike) + .ThenInclude(b => b.Model) + .Include(r => r.Renter) + .AsNoTracking() + .FirstOrDefault(r => r.Id == id); + } + + /// + /// Add new rent + /// + public void AddRent(Rent rent) + { + ArgumentNullException.ThrowIfNull(rent); + context.Rents.Add(rent); + context.SaveChanges(); + } + + /// + /// Update rent + /// + public void UpdateRent(Rent rent) + { + ArgumentNullException.ThrowIfNull(rent); + context.Rents.Update(rent); + context.SaveChanges(); + } + + /// + /// Delete rent + /// + public void DeleteRent(int id) + { + var rent = context.Rents.Find(id); + if (rent != null) + { + context.Rents.Remove(rent); + context.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.Designer.cs b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.Designer.cs new file mode 100644 index 000000000..99e06b90f --- /dev/null +++ b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.Designer.cs @@ -0,0 +1,226 @@ +// +using System; +using Bikes.Infrastructure.EfCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bikes.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(BikesDbContext))] + [Migration("20251117201300_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("color"); + + b.Property("IsAvailable") + .HasColumnType("boolean") + .HasColumnName("is_available"); + + b.Property("ModelId") + .HasColumnType("integer") + .HasColumnName("model_id"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("serial_number"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("bikes"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.BikeModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BrakeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("brake_type"); + + b.Property("MaxWeight") + .HasPrecision(7, 2) + .HasColumnType("numeric(7,2)") + .HasColumnName("max_weight"); + + b.Property("ModelYear") + .HasColumnType("integer") + .HasColumnName("model_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PricePerHour") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("price_per_hour"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("Weight") + .HasPrecision(7, 2) + .HasColumnType("numeric(7,2)") + .HasColumnName("weight"); + + b.Property("WheelSize") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)") + .HasColumnName("wheel_size"); + + b.HasKey("Id"); + + b.ToTable("bike_models"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Rent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BikeId") + .HasColumnType("integer") + .HasColumnName("bike_id"); + + b.Property("DurationHours") + .HasColumnType("integer") + .HasColumnName("duration_hours"); + + b.Property("RenterId") + .HasColumnType("integer") + .HasColumnName("renter_id"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("Id"); + + b.HasIndex("BikeId"); + + b.HasIndex("RenterId"); + + b.ToTable("rents"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Renter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("full_name"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("renters"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.HasOne("Bikes.Domain.Models.BikeModel", "Model") + .WithMany("Bikes") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Rent", b => + { + b.HasOne("Bikes.Domain.Models.Bike", "Bike") + .WithMany("Rents") + .HasForeignKey("BikeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Bikes.Domain.Models.Renter", "Renter") + .WithMany("Rents") + .HasForeignKey("RenterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Bike"); + + b.Navigation("Renter"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.Navigation("Rents"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.BikeModel", b => + { + b.Navigation("Bikes"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Renter", b => + { + b.Navigation("Rents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs new file mode 100644 index 000000000..37225ea0a --- /dev/null +++ b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bikes.Infrastructure.EfCore.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "bike_models", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + type = table.Column(type: "text", nullable: false), + wheel_size = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + max_weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), + weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), + brake_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + model_year = table.Column(type: "integer", nullable: false), + price_per_hour = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_bike_models", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "renters", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + full_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_renters", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "bikes", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + serial_number = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + model_id = table.Column(type: "integer", nullable: false), + color = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + is_available = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_bikes", x => x.id); + table.ForeignKey( + name: "FK_bikes_bike_models_model_id", + column: x => x.model_id, + principalTable: "bike_models", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "rents", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + bike_id = table.Column(type: "integer", nullable: false), + renter_id = table.Column(type: "integer", nullable: false), + start_time = table.Column(type: "timestamp with time zone", nullable: false), + duration_hours = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_rents", x => x.id); + table.ForeignKey( + name: "FK_rents_bikes_bike_id", + column: x => x.bike_id, + principalTable: "bikes", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_rents_renters_renter_id", + column: x => x.renter_id, + principalTable: "renters", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_bikes_model_id", + table: "bikes", + column: "model_id"); + + migrationBuilder.CreateIndex( + name: "IX_rents_bike_id", + table: "rents", + column: "bike_id"); + + migrationBuilder.CreateIndex( + name: "IX_rents_renter_id", + table: "rents", + column: "renter_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "rents"); + + migrationBuilder.DropTable( + name: "bikes"); + + migrationBuilder.DropTable( + name: "renters"); + + migrationBuilder.DropTable( + name: "bike_models"); + } + } +} diff --git a/Bikes.Infrastructure.EfCore/Migrations/BikesDbContextModelSnapshot.cs b/Bikes.Infrastructure.EfCore/Migrations/BikesDbContextModelSnapshot.cs new file mode 100644 index 000000000..071076da6 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/Migrations/BikesDbContextModelSnapshot.cs @@ -0,0 +1,223 @@ +// +using System; +using Bikes.Infrastructure.EfCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bikes.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(BikesDbContext))] + partial class BikesDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("color"); + + b.Property("IsAvailable") + .HasColumnType("boolean") + .HasColumnName("is_available"); + + b.Property("ModelId") + .HasColumnType("integer") + .HasColumnName("model_id"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("serial_number"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("bikes"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.BikeModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BrakeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("brake_type"); + + b.Property("MaxWeight") + .HasPrecision(7, 2) + .HasColumnType("numeric(7,2)") + .HasColumnName("max_weight"); + + b.Property("ModelYear") + .HasColumnType("integer") + .HasColumnName("model_year"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PricePerHour") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("price_per_hour"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("Weight") + .HasPrecision(7, 2) + .HasColumnType("numeric(7,2)") + .HasColumnName("weight"); + + b.Property("WheelSize") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)") + .HasColumnName("wheel_size"); + + b.HasKey("Id"); + + b.ToTable("bike_models"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Rent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BikeId") + .HasColumnType("integer") + .HasColumnName("bike_id"); + + b.Property("DurationHours") + .HasColumnType("integer") + .HasColumnName("duration_hours"); + + b.Property("RenterId") + .HasColumnType("integer") + .HasColumnName("renter_id"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("Id"); + + b.HasIndex("BikeId"); + + b.HasIndex("RenterId"); + + b.ToTable("rents"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Renter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("full_name"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.HasKey("Id"); + + b.ToTable("renters"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.HasOne("Bikes.Domain.Models.BikeModel", "Model") + .WithMany("Bikes") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Rent", b => + { + b.HasOne("Bikes.Domain.Models.Bike", "Bike") + .WithMany("Rents") + .HasForeignKey("BikeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Bikes.Domain.Models.Renter", "Renter") + .WithMany("Rents") + .HasForeignKey("RenterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Bike"); + + b.Navigation("Renter"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Bike", b => + { + b.Navigation("Rents"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.BikeModel", b => + { + b.Navigation("Bikes"); + }); + + modelBuilder.Entity("Bikes.Domain.Models.Renter", b => + { + b.Navigation("Rents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Bikes.Infrastructure.EfCore/Properties/launchSettings.json b/Bikes.Infrastructure.EfCore/Properties/launchSettings.json new file mode 100644 index 000000000..1cac07450 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Bikes.Infrastructure.EfCore": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50514;http://localhost:50515" + } + } +} \ No newline at end of file diff --git a/Bikes.sln b/Bikes.sln index 390a4da89..417e5b6e3 100644 --- a/Bikes.sln +++ b/Bikes.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Api.Host", "Bikes.Api EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.InMemory", "Bikes.Infrastructure.InMemory\Bikes.Infrastructure.InMemory.csproj", "{AF5C88C9-4078-48B6-814A-F64C690D97FF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.EfCore", "Bikes.Infrastructure.EfCore\Bikes.Infrastructure.EfCore.csproj", "{AB546D05-D5C3-4C68-8682-4DDA037568C1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,6 +99,18 @@ Global {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x64.Build.0 = Release|Any CPU {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x86.ActiveCfg = Release|Any CPU {AF5C88C9-4078-48B6-814A-F64C690D97FF}.Release|x86.Build.0 = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|x64.Build.0 = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Debug|x86.Build.0 = Debug|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|Any CPU.Build.0 = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x64.ActiveCfg = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x64.Build.0 = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x86.ActiveCfg = Release|Any CPU + {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Infrastructure.EfCore/BikesDbContext.cs b/Infrastructure.EfCore/BikesDbContext.cs new file mode 100644 index 000000000..beeead2a1 --- /dev/null +++ b/Infrastructure.EfCore/BikesDbContext.cs @@ -0,0 +1,6 @@ +namespace Infrastructure.EfCore; + +public class Class1 +{ + +} diff --git a/Infrastructure.EfCore/Infrastructure.EfCore.csproj b/Infrastructure.EfCore/Infrastructure.EfCore.csproj new file mode 100644 index 000000000..cefe28690 --- /dev/null +++ b/Infrastructure.EfCore/Infrastructure.EfCore.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + \ No newline at end of file From 33ad1a752817ab6b0f86187bd9adb3a834e8d939 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Mon, 15 Dec 2025 23:36:21 +0400 Subject: [PATCH 15/19] Aspire orchestration has been added and configured --- Bikes.Api.Host/Bikes.Api.Host.csproj | 9 +- Bikes.Api.Host/Program.cs | 14 +- Bikes.Api.Host/Properties/launchSettings.json | 25 +--- Bikes.Api.Host/appsettings.json | 2 +- Bikes.AppHost/AppHost.cs | 12 ++ Bikes.AppHost/Bikes.AppHost.csproj | 23 ++++ Bikes.AppHost/Properties/launchSettings.json | 29 +++++ .../Bikes.Infrastructure.EfCore.csproj | 6 +- Bikes.Infrastructure.EfCore/DataSeeder.cs | 1 - .../Bikes.ServiceDefaults.csproj | 19 +++ .../ServiceDefaultsExtensions.cs | 123 ++++++++++++++++++ Bikes.sln | 28 ++++ Infrastructure.EfCore/BikesDbContext.cs | 6 - .../Infrastructure.EfCore.csproj | 23 ---- 14 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 Bikes.AppHost/AppHost.cs create mode 100644 Bikes.AppHost/Bikes.AppHost.csproj create mode 100644 Bikes.AppHost/Properties/launchSettings.json create mode 100644 Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj create mode 100644 Bikes.ServiceDefaults/ServiceDefaultsExtensions.cs delete mode 100644 Infrastructure.EfCore/BikesDbContext.cs delete mode 100644 Infrastructure.EfCore/Infrastructure.EfCore.csproj diff --git a/Bikes.Api.Host/Bikes.Api.Host.csproj b/Bikes.Api.Host/Bikes.Api.Host.csproj index b25b74a84..332a054c9 100644 --- a/Bikes.Api.Host/Bikes.Api.Host.csproj +++ b/Bikes.Api.Host/Bikes.Api.Host.csproj @@ -4,16 +4,19 @@ enable enable + - runtime; build; native; contentfiles; analyzers; buildtransitive - all + runtime; build; native; contentfiles; analyzers; buildtransitive + all - + + + \ No newline at end of file diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs index 1aaf115b7..6d4537cc2 100644 --- a/Bikes.Api.Host/Program.cs +++ b/Bikes.Api.Host/Program.cs @@ -1,25 +1,26 @@ -using Bikes.Application.Services; using Bikes.Application.Contracts.Analytics; using Bikes.Application.Contracts.Bikes; using Bikes.Application.Contracts.Models; using Bikes.Application.Contracts.Renters; using Bikes.Application.Contracts.Rents; +using Bikes.Application.Services; using Bikes.Domain.Repositories; using Bikes.Infrastructure.EfCore; +using Bikes.ServiceDefaults; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); -// Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// Register DbContext with PostgreSQL builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("bikes-db")); +}); -// Register services - Scoped builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -29,7 +30,6 @@ var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -38,11 +38,13 @@ app.UseHttpsRedirection(); app.UseAuthorization(); +app.MapDefaultEndpoints(); app.MapControllers(); using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); context.Seed(); } diff --git a/Bikes.Api.Host/Properties/launchSettings.json b/Bikes.Api.Host/Properties/launchSettings.json index 6b8f22f6e..d339d863d 100644 --- a/Bikes.Api.Host/Properties/launchSettings.json +++ b/Bikes.Api.Host/Properties/launchSettings.json @@ -1,20 +1,11 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:64619", - "sslPort": 44386 - } - }, +{ "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5145", + "applicationUrl": "http://localhost:5186", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -24,18 +15,10 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7278;http://localhost:5145", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", + "applicationUrl": "https://localhost:7186;http://localhost:5186", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/Bikes.Api.Host/appsettings.json b/Bikes.Api.Host/appsettings.json index 232f36baf..bf32e9174 100644 --- a/Bikes.Api.Host/appsettings.json +++ b/Bikes.Api.Host/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=bikes_db;Username=postgres;Password=12345" + "DefaultConnection": "Host=localhost;Port=5432;Database=bikes_db;Username=postgres;Password=postgres123" }, "Logging": { "LogLevel": { diff --git a/Bikes.AppHost/AppHost.cs b/Bikes.AppHost/AppHost.cs new file mode 100644 index 000000000..bde066cdf --- /dev/null +++ b/Bikes.AppHost/AppHost.cs @@ -0,0 +1,12 @@ +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres"); +var bikesDb = postgres.AddDatabase("bikes-db"); + +builder.AddProject("bikes-api") + .WithReference(bikesDb) + .WaitFor(bikesDb); + +builder.Build().Run(); \ No newline at end of file diff --git a/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes.AppHost/Bikes.AppHost.csproj new file mode 100644 index 000000000..4e3acc588 --- /dev/null +++ b/Bikes.AppHost/Bikes.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + Exe + net8.0 + enable + enable + true + aspire-host-id + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bikes.AppHost/Properties/launchSettings.json b/Bikes.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..eac85cbcb --- /dev/null +++ b/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:17150;http://localhost:15096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21165", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22192" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19260", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20243" + } + } + } +} diff --git a/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj b/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj index b6b7afa98..9833c6855 100644 --- a/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj +++ b/Bikes.Infrastructure.EfCore/Bikes.Infrastructure.EfCore.csproj @@ -1,5 +1,4 @@  - net8.0 enable @@ -14,13 +13,12 @@ - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/DataSeeder.cs b/Bikes.Infrastructure.EfCore/DataSeeder.cs index 86d509ad9..8ba5dc2f8 100644 --- a/Bikes.Infrastructure.EfCore/DataSeeder.cs +++ b/Bikes.Infrastructure.EfCore/DataSeeder.cs @@ -1,5 +1,4 @@ using Bikes.Domain.Models; -using Microsoft.EntityFrameworkCore; namespace Bikes.Infrastructure.EfCore; diff --git a/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj b/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj new file mode 100644 index 000000000..2ce061c99 --- /dev/null +++ b/Bikes.ServiceDefaults/Bikes.ServiceDefaults.csproj @@ -0,0 +1,19 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bikes.ServiceDefaults/ServiceDefaultsExtensions.cs b/Bikes.ServiceDefaults/ServiceDefaultsExtensions.cs new file mode 100644 index 000000000..267a490fd --- /dev/null +++ b/Bikes.ServiceDefaults/ServiceDefaultsExtensions.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Bikes.ServiceDefaults; + +/// +/// Extensions for configuring default .NET Aspire services. +/// +public static class ServiceDefaultsExtensions +{ + /// + /// Adds default .NET Aspire services: OpenTelemetry, health checks, service discovery, and HTTP client resilience. + /// + /// The host application builder type. + /// The host builder. + /// The host builder for chaining. + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// Configures OpenTelemetry for metrics, tracing, and logging collection. + /// + /// The host application builder type. + /// The host builder. + /// The host builder for chaining. + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + /// + /// Adds OpenTelemetry exporters if OTLP endpoint is configured. + /// + /// The host application builder type. + /// The host builder. + /// The host builder for chaining. + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + /// + /// Adds default health checks for the application. + /// + /// The host application builder type. + /// The host builder. + /// The host builder for chaining. + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Maps default health check endpoints for the web application. + /// + /// The web application. + /// The web application for chaining. + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + app.MapHealthChecks("/health"); + } + + return app; + } +} \ No newline at end of file diff --git a/Bikes.sln b/Bikes.sln index 417e5b6e3..e55a36201 100644 --- a/Bikes.sln +++ b/Bikes.sln @@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.InMemo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.EfCore", "Bikes.Infrastructure.EfCore\Bikes.Infrastructure.EfCore.csproj", "{AB546D05-D5C3-4C68-8682-4DDA037568C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.AppHost", "Bikes.AppHost\Bikes.AppHost.csproj", "{D5DCAAC2-A630-4C25-8A30-A9F465633F0E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.ServiceDefaults", "Bikes.ServiceDefaults\Bikes.ServiceDefaults.csproj", "{2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +115,30 @@ Global {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x64.Build.0 = Release|Any CPU {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x86.ActiveCfg = Release|Any CPU {AB546D05-D5C3-4C68-8682-4DDA037568C1}.Release|x86.Build.0 = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|x64.Build.0 = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Debug|x86.Build.0 = Debug|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|Any CPU.Build.0 = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|x64.ActiveCfg = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|x64.Build.0 = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|x86.ActiveCfg = Release|Any CPU + {D5DCAAC2-A630-4C25-8A30-A9F465633F0E}.Release|x86.Build.0 = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|x64.Build.0 = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Debug|x86.Build.0 = Debug|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x64.ActiveCfg = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x64.Build.0 = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x86.ActiveCfg = Release|Any CPU + {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Infrastructure.EfCore/BikesDbContext.cs b/Infrastructure.EfCore/BikesDbContext.cs deleted file mode 100644 index beeead2a1..000000000 --- a/Infrastructure.EfCore/BikesDbContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Infrastructure.EfCore; - -public class Class1 -{ - -} diff --git a/Infrastructure.EfCore/Infrastructure.EfCore.csproj b/Infrastructure.EfCore/Infrastructure.EfCore.csproj deleted file mode 100644 index cefe28690..000000000 --- a/Infrastructure.EfCore/Infrastructure.EfCore.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - \ No newline at end of file From 59c65a250ddf533d5782a1a913f637d2f09eaab3 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Wed, 17 Dec 2025 19:10:37 +0400 Subject: [PATCH 16/19] finalized and fixed 500 --- .../Controllers/AnalyticsController.cs | 12 + .../Controllers/CrudControllerBase.cs | 15 ++ Bikes.Api.Host/Program.cs | 6 + Bikes.Application.Contracts/Rents/RentDto.cs | 10 +- .../Services/AnalyticsService.cs | 23 +- .../Services/BikeModelService.cs | 22 +- Bikes.Application/Services/BikeService.cs | 39 +-- Bikes.Application/Services/RentService.cs | 41 ++-- Bikes.Application/Services/RenterService.cs | 22 +- Bikes.Domain/Models/Bike.cs | 7 +- Bikes.Domain/Models/BikeModel.cs | 18 +- Bikes.Domain/Models/Rent.cs | 4 +- Bikes.Domain/Models/Renter.cs | 6 +- Bikes.Domain/Repositories/IBikeRepository.cs | 22 +- Bikes.Infrastructure.EfCore/BikesDbContext.cs | 6 +- Bikes.Infrastructure.EfCore/DataSeeder.cs | 16 ++ .../EfCoreBikeRepository.cs | 18 ++ .../Migrations/20251117201300_Initial.cs | 224 +++++++++--------- 18 files changed, 300 insertions(+), 211 deletions(-) diff --git a/Bikes.Api.Host/Controllers/AnalyticsController.cs b/Bikes.Api.Host/Controllers/AnalyticsController.cs index 011535fab..aabff1312 100644 --- a/Bikes.Api.Host/Controllers/AnalyticsController.cs +++ b/Bikes.Api.Host/Controllers/AnalyticsController.cs @@ -15,6 +15,8 @@ public class AnalyticsController(IAnalyticsService analyticsService) : Controlle /// Get all sport bikes /// [HttpGet("sport-bikes")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult> GetSportBikes() { try @@ -32,6 +34,8 @@ public ActionResult> GetSportBikes() /// Get top 5 models by profit /// [HttpGet("top-models-by-profit")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult> GetTopModelsByProfit() { try @@ -49,6 +53,8 @@ public ActionResult> GetTopModelsByProfit() /// Get top 5 models by rental duration /// [HttpGet("top-models-by-duration")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult> GetTopModelsByDuration() { try @@ -66,6 +72,8 @@ public ActionResult> GetTopModelsByDuration() /// Get rental statistics /// [HttpGet("rental-statistics")] + [ProducesResponseType(typeof(RentalStatistics), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult GetRentalStatistics() { try @@ -83,6 +91,8 @@ public ActionResult GetRentalStatistics() /// Get total rental time by bike type /// [HttpGet("rental-time-by-type")] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult> GetRentalTimeByType() { try @@ -100,6 +110,8 @@ public ActionResult> GetRentalTimeByType() /// Get top renters by rental count /// [HttpGet("top-renters")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public ActionResult> GetTopRenters() { try diff --git a/Bikes.Api.Host/Controllers/CrudControllerBase.cs b/Bikes.Api.Host/Controllers/CrudControllerBase.cs index 99ee80608..28a402df4 100644 --- a/Bikes.Api.Host/Controllers/CrudControllerBase.cs +++ b/Bikes.Api.Host/Controllers/CrudControllerBase.cs @@ -20,6 +20,8 @@ public abstract class CrudControllerBase : ControllerBas /// Get all entities /// [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public virtual ActionResult> GetAll() { try @@ -37,6 +39,9 @@ public virtual ActionResult> GetAll() /// Get entity by id /// [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public virtual ActionResult GetById(int id) { try @@ -54,6 +59,9 @@ public virtual ActionResult GetById(int id) /// Create new entity /// [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public virtual ActionResult Create([FromBody] TCreateUpdateDto request) { try @@ -80,6 +88,10 @@ public virtual ActionResult Create([FromBody] TCreateUpdateDto request) /// Update entity /// [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public virtual ActionResult Update(int id, [FromBody] TCreateUpdateDto request) { try @@ -106,6 +118,9 @@ public virtual ActionResult Update(int id, [FromBody] TCreateUpdateDto req /// Delete entity /// [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public virtual ActionResult Delete(int id) { try diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs index 6d4537cc2..cd1ac1d4f 100644 --- a/Bikes.Api.Host/Program.cs +++ b/Bikes.Api.Host/Program.cs @@ -19,9 +19,15 @@ builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("bikes-db")); + options.EnableSensitiveDataLogging(); + options.LogTo(Console.WriteLine, LogLevel.Information); }); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Bikes.Application.Contracts/Rents/RentDto.cs b/Bikes.Application.Contracts/Rents/RentDto.cs index 529b0df6f..c54826ce3 100644 --- a/Bikes.Application.Contracts/Rents/RentDto.cs +++ b/Bikes.Application.Contracts/Rents/RentDto.cs @@ -13,25 +13,25 @@ public class RentDto /// /// Rented bike identifier /// - public required int BikeId { get; set; } + public int BikeId { get; set; } /// /// Renter identifier /// - public required int RenterId { get; set; } + public int RenterId { get; set; } /// /// Rental start time /// - public required DateTime StartTime { get; set; } + public DateTime StartTime { get; set; } /// /// Rental duration in hours /// - public required int DurationHours { get; set; } + public int DurationHours { get; set; } /// /// Total rental cost /// - public required decimal TotalCost { get; set; } + public decimal TotalCost { get; set; } } \ No newline at end of file diff --git a/Bikes.Application/Services/AnalyticsService.cs b/Bikes.Application/Services/AnalyticsService.cs index b4dddf11c..7b7ca0204 100644 --- a/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes.Application/Services/AnalyticsService.cs @@ -8,14 +8,17 @@ namespace Bikes.Application.Services; /// /// Implementation of analytics service /// -public class AnalyticsService(IBikeRepository repository) : IAnalyticsService +public class AnalyticsService( + IBikeRepository bikeRepository, + IRentRepository rentRepository, + IBikeModelRepository bikeModelRepository) : IAnalyticsService { /// /// Get all sport bikes - /// + /// public List GetSportBikes() { - return [.. repository.GetAllBikes() + return [.. bikeRepository.GetAllBikes() .Where(b => b.Model.Type == BikeType.Sport) .Select(b => new BikeDto { @@ -32,7 +35,7 @@ public List GetSportBikes() /// public List GetTop5ModelsByProfit() { - var rents = repository.GetAllRents(); + var rents = rentRepository.GetAllRents(); return [.. rents .GroupBy(r => r.Bike.Model) @@ -42,7 +45,7 @@ public List GetTop5ModelsByProfit() Name = g.Key.Name, Type = g.Key.Type.ToString(), PricePerHour = g.Key.PricePerHour, - TotalProfit = g.Sum(r => r.TotalCost), + TotalProfit = g.Sum(r => r.Bike.Model.PricePerHour * r.DurationHours), TotalDuration = g.Sum(r => r.DurationHours) }) .OrderByDescending(x => x.TotalProfit) @@ -54,7 +57,7 @@ public List GetTop5ModelsByProfit() /// public List GetTop5ModelsByRentalDuration() { - var rents = repository.GetAllRents(); + var rents = rentRepository.GetAllRents(); return [.. rents .GroupBy(r => r.Bike.Model) @@ -64,7 +67,7 @@ public List GetTop5ModelsByRentalDuration() Name = g.Key.Name, Type = g.Key.Type.ToString(), PricePerHour = g.Key.PricePerHour, - TotalProfit = g.Sum(r => r.TotalCost), + TotalProfit = g.Sum(r => r.Bike.Model.PricePerHour * r.DurationHours), TotalDuration = g.Sum(r => r.DurationHours) }) .OrderByDescending(x => x.TotalDuration) @@ -76,7 +79,7 @@ public List GetTop5ModelsByRentalDuration() /// public RentalStatistics GetRentalStatistics() { - var durations = repository.GetAllRents() + var durations = rentRepository.GetAllRents() .Select(r => r.DurationHours); return new RentalStatistics( @@ -91,7 +94,7 @@ public RentalStatistics GetRentalStatistics() /// public Dictionary GetTotalRentalTimeByBikeType() { - return repository.GetAllRents() + return rentRepository.GetAllRents() .GroupBy(r => r.Bike.Model.Type) .ToDictionary( g => g.Key.ToString(), @@ -104,7 +107,7 @@ public Dictionary GetTotalRentalTimeByBikeType() /// public List GetTopRentersByRentalCount() { - return [.. repository.GetAllRents() + return [.. rentRepository.GetAllRents() .GroupBy(r => r.Renter) .Select(g => new RenterAnalyticsDto { diff --git a/Bikes.Application/Services/BikeModelService.cs b/Bikes.Application/Services/BikeModelService.cs index fb58e7921..0d6dd892a 100644 --- a/Bikes.Application/Services/BikeModelService.cs +++ b/Bikes.Application/Services/BikeModelService.cs @@ -7,14 +7,14 @@ namespace Bikes.Application.Services; /// /// Implementation of bike model service /// -public class BikeModelService(IBikeRepository repository) : IBikeModelService +public class BikeModelService(IBikeModelRepository bikeModelRepository) : IBikeModelService { /// /// Get all bike models /// public List GetAll() { - return [.. repository.GetAllModels().Select(m => new BikeModelDto + return [.. bikeModelRepository.GetAllModels().Select(m => new BikeModelDto { Id = m.Id, Name = m.Name, @@ -33,7 +33,7 @@ public List GetAll() /// public BikeModelDto? GetById(int id) { - var model = repository.GetModelById(id); + var model = bikeModelRepository.GetModelById(id); return model == null ? null : new BikeModelDto { Id = model.Id, @@ -58,10 +58,12 @@ public BikeModelDto Create(BikeModelCreateUpdateDto request) if (!Enum.TryParse(request.Type, out var bikeType)) throw new InvalidOperationException($"Invalid bike type: {request.Type}"); - var models = repository.GetAllModels(); + var models = bikeModelRepository.GetAllModels(); + var maxId = models.Count == 0 ? 0 : models.Max(m => m.Id); + var newModel = new BikeModel { - Id = models.Max(m => m.Id) + 1, + Id = maxId + 1, Name = request.Name, Type = bikeType, WheelSize = request.WheelSize, @@ -72,7 +74,7 @@ public BikeModelDto Create(BikeModelCreateUpdateDto request) PricePerHour = request.PricePerHour }; - repository.AddModel(newModel); + bikeModelRepository.AddModel(newModel); return new BikeModelDto { @@ -98,7 +100,7 @@ public BikeModelDto Create(BikeModelCreateUpdateDto request) if (!Enum.TryParse(request.Type, out var bikeType)) throw new InvalidOperationException($"Invalid bike type: {request.Type}"); - var model = repository.GetModelById(id); + var model = bikeModelRepository.GetModelById(id); if (model == null) return null; model.Name = request.Name; @@ -110,7 +112,7 @@ public BikeModelDto Create(BikeModelCreateUpdateDto request) model.ModelYear = request.ModelYear; model.PricePerHour = request.PricePerHour; - repository.UpdateModel(model); + bikeModelRepository.UpdateModel(model); return new BikeModelDto { @@ -131,10 +133,10 @@ public BikeModelDto Create(BikeModelCreateUpdateDto request) /// public bool Delete(int id) { - var model = repository.GetModelById(id); + var model = bikeModelRepository.GetModelById(id); if (model == null) return false; - repository.DeleteModel(id); + bikeModelRepository.DeleteModel(id); return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/BikeService.cs b/Bikes.Application/Services/BikeService.cs index a7fa0df7a..c2b304060 100644 --- a/Bikes.Application/Services/BikeService.cs +++ b/Bikes.Application/Services/BikeService.cs @@ -7,14 +7,16 @@ namespace Bikes.Application.Services; /// /// Implementation of bike service /// -public class BikeService(IBikeRepository repository) : IBikeService +public class BikeService( + IBikeRepository bikeRepository, + IBikeModelRepository bikeModelRepository) : IBikeService { /// /// Get all bikes /// public List GetAll() { - return [.. repository.GetAllBikes().Select(b => new BikeDto + return [.. bikeRepository.GetAllBikes().Select(b => new BikeDto { Id = b.Id, SerialNumber = b.SerialNumber, @@ -29,7 +31,7 @@ public List GetAll() /// public BikeDto? GetById(int id) { - var bike = repository.GetBikeById(id); + var bike = bikeRepository.GetBikeById(id); return bike == null ? null : new BikeDto { Id = bike.Id, @@ -47,26 +49,25 @@ public BikeDto Create(BikeCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var models = repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == request.ModelId) - ?? throw new InvalidOperationException("Model not found"); + var modelExists = bikeModelRepository.GetModelById(request.ModelId) != null; + if (!modelExists) + throw new InvalidOperationException("Model not found"); var newBike = new Bike { - Id = repository.GetAllBikes().Max(b => b.Id) + 1, SerialNumber = request.SerialNumber, - Model = model, + ModelId = request.ModelId, Color = request.Color, IsAvailable = true }; - repository.AddBike(newBike); + bikeRepository.AddBike(newBike); return new BikeDto { Id = newBike.Id, SerialNumber = newBike.SerialNumber, - ModelId = newBike.Model.Id, + ModelId = newBike.ModelId, Color = newBike.Color, IsAvailable = newBike.IsAvailable }; @@ -79,24 +80,24 @@ public BikeDto Create(BikeCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var bike = repository.GetBikeById(id); + var bike = bikeRepository.GetBikeById(id); if (bike == null) return null; - var models = repository.GetAllModels(); - var model = models.FirstOrDefault(m => m.Id == request.ModelId) - ?? throw new InvalidOperationException("Model not found"); + var modelExists = bikeModelRepository.GetModelById(request.ModelId) != null; + if (!modelExists) + throw new InvalidOperationException("Model not found"); bike.SerialNumber = request.SerialNumber; - bike.Model = model; + bike.ModelId = request.ModelId; bike.Color = request.Color; - repository.UpdateBike(bike); + bikeRepository.UpdateBike(bike); return new BikeDto { Id = bike.Id, SerialNumber = bike.SerialNumber, - ModelId = bike.Model.Id, + ModelId = bike.ModelId, Color = bike.Color, IsAvailable = bike.IsAvailable }; @@ -107,10 +108,10 @@ public BikeDto Create(BikeCreateUpdateDto request) /// public bool Delete(int id) { - var bike = repository.GetBikeById(id); + var bike = bikeRepository.GetBikeById(id); if (bike == null) return false; - repository.DeleteBike(id); + bikeRepository.DeleteBike(id); return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/RentService.cs b/Bikes.Application/Services/RentService.cs index 591af83b0..c726020da 100644 --- a/Bikes.Application/Services/RentService.cs +++ b/Bikes.Application/Services/RentService.cs @@ -7,21 +7,24 @@ namespace Bikes.Application.Services; /// /// Implementation of rent service /// -public class RentService(IBikeRepository repository) : IRentService +public class RentService( + IRentRepository rentRepository, + IBikeRepository bikeRepository, + IRenterRepository renterRepository) : IRentService { /// /// Get all rents /// public List GetAll() { - return [.. repository.GetAllRents().Select(r => new RentDto + return [.. rentRepository.GetAllRents().Select(r => new RentDto { Id = r.Id, BikeId = r.Bike.Id, RenterId = r.Renter.Id, StartTime = r.StartTime, DurationHours = r.DurationHours, - TotalCost = r.TotalCost + TotalCost = r.Bike.Model.PricePerHour * r.DurationHours })]; } @@ -30,7 +33,7 @@ public List GetAll() /// public RentDto? GetById(int id) { - var rent = repository.GetRentById(id); + var rent = rentRepository.GetRentById(id); return rent == null ? null : new RentDto { Id = rent.Id, @@ -38,7 +41,7 @@ public List GetAll() RenterId = rent.Renter.Id, StartTime = rent.StartTime, DurationHours = rent.DurationHours, - TotalCost = rent.TotalCost + TotalCost = rent.Bike.Model.PricePerHour * rent.DurationHours }; } @@ -49,25 +52,27 @@ public RentDto Create(RentCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var bike = repository.GetBikeById(request.BikeId); - var renter = repository.GetRenterById(request.RenterId); - var rents = repository.GetAllRents(); + var bike = bikeRepository.GetBikeById(request.BikeId); + var renter = renterRepository.GetRenterById(request.RenterId); + var rents = rentRepository.GetAllRents(); if (bike == null) throw new InvalidOperationException("Bike not found"); if (renter == null) throw new InvalidOperationException("Renter not found"); + var maxId = rents.Count == 0 ? 0 : rents.Max(r => r.Id); + var newRent = new Rent { - Id = rents.Max(r => r.Id) + 1, + Id = maxId + 1, Bike = bike, Renter = renter, StartTime = request.StartTime, DurationHours = request.DurationHours }; - repository.AddRent(newRent); + rentRepository.AddRent(newRent); return new RentDto { @@ -76,7 +81,7 @@ public RentDto Create(RentCreateUpdateDto request) RenterId = newRent.Renter.Id, StartTime = newRent.StartTime, DurationHours = newRent.DurationHours, - TotalCost = newRent.TotalCost + TotalCost = newRent.Bike.Model.PricePerHour * newRent.DurationHours }; } @@ -87,12 +92,12 @@ public RentDto Create(RentCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var rent = repository.GetRentById(id); + var rent = rentRepository.GetRentById(id); if (rent == null) return null; - var bike = repository.GetBikeById(request.BikeId) + var bike = bikeRepository.GetBikeById(request.BikeId) ?? throw new InvalidOperationException("Bike not found"); - var renter = repository.GetRenterById(request.RenterId) + var renter = renterRepository.GetRenterById(request.RenterId) ?? throw new InvalidOperationException("Renter not found"); rent.Bike = bike; @@ -100,7 +105,7 @@ public RentDto Create(RentCreateUpdateDto request) rent.StartTime = request.StartTime; rent.DurationHours = request.DurationHours; - repository.UpdateRent(rent); + rentRepository.UpdateRent(rent); return new RentDto { @@ -109,7 +114,7 @@ public RentDto Create(RentCreateUpdateDto request) RenterId = rent.Renter.Id, StartTime = rent.StartTime, DurationHours = rent.DurationHours, - TotalCost = rent.TotalCost + TotalCost = rent.Bike.Model.PricePerHour * rent.DurationHours }; } @@ -118,10 +123,10 @@ public RentDto Create(RentCreateUpdateDto request) /// public bool Delete(int id) { - var rent = repository.GetRentById(id); + var rent = rentRepository.GetRentById(id); if (rent == null) return false; - repository.DeleteRent(id); + rentRepository.DeleteRent(id); return true; } } \ No newline at end of file diff --git a/Bikes.Application/Services/RenterService.cs b/Bikes.Application/Services/RenterService.cs index afcd1c90f..bcc0fe14c 100644 --- a/Bikes.Application/Services/RenterService.cs +++ b/Bikes.Application/Services/RenterService.cs @@ -7,14 +7,14 @@ namespace Bikes.Application.Services; /// /// Implementation of renter service /// -public class RenterService(IBikeRepository repository) : IRenterService +public class RenterService(IRenterRepository renterRepository) : IRenterService { /// /// Get all renters /// public List GetAll() { - return [.. repository.GetAllRenters().Select(r => new RenterDto + return [.. renterRepository.GetAllRenters().Select(r => new RenterDto { Id = r.Id, FullName = r.FullName, @@ -27,7 +27,7 @@ public List GetAll() /// public RenterDto? GetById(int id) { - var renter = repository.GetRenterById(id); + var renter = renterRepository.GetRenterById(id); return renter == null ? null : new RenterDto { Id = renter.Id, @@ -43,15 +43,17 @@ public RenterDto Create(RenterCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var renters = repository.GetAllRenters(); + var renters = renterRepository.GetAllRenters(); + var maxId = renters.Count == 0 ? 0 : renters.Max(r => r.Id); + var newRenter = new Renter { - Id = renters.Count > 0 ? renters.Max(r => r.Id) + 1 : 1, + Id = maxId + 1, FullName = request.FullName, Phone = request.Phone }; - repository.AddRenter(newRenter); + renterRepository.AddRenter(newRenter); return new RenterDto { @@ -68,13 +70,13 @@ public RenterDto Create(RenterCreateUpdateDto request) { ArgumentNullException.ThrowIfNull(request); - var renter = repository.GetRenterById(id); + var renter = renterRepository.GetRenterById(id); if (renter == null) return null; renter.FullName = request.FullName; renter.Phone = request.Phone; - repository.UpdateRenter(renter); + renterRepository.UpdateRenter(renter); return new RenterDto { @@ -89,10 +91,10 @@ public RenterDto Create(RenterCreateUpdateDto request) /// public bool Delete(int id) { - var renter = repository.GetRenterById(id); + var renter = renterRepository.GetRenterById(id); if (renter == null) return false; - repository.DeleteRenter(id); + renterRepository.DeleteRenter(id); return true; } } \ No newline at end of file diff --git a/Bikes.Domain/Models/Bike.cs b/Bikes.Domain/Models/Bike.cs index 020182a32..75be88566 100644 --- a/Bikes.Domain/Models/Bike.cs +++ b/Bikes.Domain/Models/Bike.cs @@ -14,6 +14,7 @@ public class Bike /// [Key] [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } /// @@ -21,7 +22,7 @@ public class Bike /// [Required] [Column("serial_number")] - public required string SerialNumber { get; set; } + public string SerialNumber { get; set; } = null!; /// /// Foreign key for bike model @@ -40,7 +41,7 @@ public class Bike /// [Required] [Column("color")] - public required string Color { get; set; } + public string Color { get; set; } = null!; /// /// Availability status for rental @@ -52,5 +53,5 @@ public class Bike /// /// Navigation property for rents /// - public virtual List Rents { get; set; } = new(); + public virtual List Rents { get; set; } = []; } \ No newline at end of file diff --git a/Bikes.Domain/Models/BikeModel.cs b/Bikes.Domain/Models/BikeModel.cs index 0cb654ab1..dd573ef64 100644 --- a/Bikes.Domain/Models/BikeModel.cs +++ b/Bikes.Domain/Models/BikeModel.cs @@ -21,59 +21,59 @@ public class BikeModel /// [Required] [Column("name")] - public required string Name { get; set; } + public string Name { get; set; } = null!; /// /// Type of bicycle /// [Required] [Column("type")] - public required BikeType Type { get; set; } + public BikeType Type { get; set; } /// /// Wheel size in inches /// [Required] [Column("wheel_size")] - public required decimal WheelSize { get; set; } + public decimal WheelSize { get; set; } /// /// Maximum permissible passenger weight in kg /// [Required] [Column("max_weight")] - public required decimal MaxWeight { get; set; } + public decimal MaxWeight { get; set; } /// /// Bicycle weight in kg /// [Required] [Column("weight")] - public required decimal Weight { get; set; } + public decimal Weight { get; set; } /// /// Type of brakes /// [Required] [Column("brake_type")] - public required string BrakeType { get; set; } + public string BrakeType { get; set; } = null!; /// /// Model year /// [Required] [Column("model_year")] - public required int ModelYear { get; set; } + public int ModelYear { get; set; } /// /// Price per hour of rental /// [Required] [Column("price_per_hour")] - public required decimal PricePerHour { get; set; } + public decimal PricePerHour { get; set; } /// /// Navigation property for bikes /// - public virtual List Bikes { get; set; } = new(); + public virtual List Bikes { get; set; } = []; } \ No newline at end of file diff --git a/Bikes.Domain/Models/Rent.cs b/Bikes.Domain/Models/Rent.cs index 60fc6ddeb..6345ca998 100644 --- a/Bikes.Domain/Models/Rent.cs +++ b/Bikes.Domain/Models/Rent.cs @@ -45,14 +45,14 @@ public class Rent /// [Required] [Column("start_time")] - public required DateTime StartTime { get; set; } + public DateTime StartTime { get; set; } /// /// Rental duration in hours /// [Required] [Column("duration_hours")] - public required int DurationHours { get; set; } + public int DurationHours { get; set; } /// /// Total rental cost diff --git a/Bikes.Domain/Models/Renter.cs b/Bikes.Domain/Models/Renter.cs index e680d1abb..608025bc8 100644 --- a/Bikes.Domain/Models/Renter.cs +++ b/Bikes.Domain/Models/Renter.cs @@ -21,17 +21,17 @@ public class Renter /// [Required] [Column("full_name")] - public required string FullName { get; set; } + public string FullName { get; set; } = null!; /// /// Contact phone number /// [Required] [Column("phone")] - public required string Phone { get; set; } + public string Phone { get; set; } = null!; /// /// Navigation property for rents /// - public virtual List Rents { get; set; } = new(); + public virtual List Rents { get; set; } = []; } \ No newline at end of file diff --git a/Bikes.Domain/Repositories/IBikeRepository.cs b/Bikes.Domain/Repositories/IBikeRepository.cs index d02595070..9dada527c 100644 --- a/Bikes.Domain/Repositories/IBikeRepository.cs +++ b/Bikes.Domain/Repositories/IBikeRepository.cs @@ -7,28 +7,42 @@ namespace Bikes.Domain.Repositories; /// public interface IBikeRepository { - // Bike methods public List GetAllBikes(); public Bike? GetBikeById(int id); public void AddBike(Bike bike); public void UpdateBike(Bike bike); public void DeleteBike(int id); +} - // BikeModel methods +/// +/// Repository for bike model data access +/// +public interface IBikeModelRepository +{ public List GetAllModels(); public BikeModel? GetModelById(int id); public void AddModel(BikeModel model); public void UpdateModel(BikeModel model); public void DeleteModel(int id); +} - // Renter methods +/// +/// Repository for renter data access +/// +public interface IRenterRepository +{ public List GetAllRenters(); public Renter? GetRenterById(int id); public void AddRenter(Renter renter); public void UpdateRenter(Renter renter); public void DeleteRenter(int id); +} - // Rent methods +/// +/// Repository for rent data access +/// +public interface IRentRepository +{ public List GetAllRents(); public Rent? GetRentById(int id); public void AddRent(Rent rent); diff --git a/Bikes.Infrastructure.EfCore/BikesDbContext.cs b/Bikes.Infrastructure.EfCore/BikesDbContext.cs index 37897241e..2266ede0e 100644 --- a/Bikes.Infrastructure.EfCore/BikesDbContext.cs +++ b/Bikes.Infrastructure.EfCore/BikesDbContext.cs @@ -6,12 +6,8 @@ namespace Bikes.Infrastructure.EfCore; /// /// Database context for bikes rental system /// -public class BikesDbContext : DbContext +public class BikesDbContext(DbContextOptions options) : DbContext(options) { - public BikesDbContext(DbContextOptions options) : base(options) - { - } - /// /// Bike models table /// diff --git a/Bikes.Infrastructure.EfCore/DataSeeder.cs b/Bikes.Infrastructure.EfCore/DataSeeder.cs index 8ba5dc2f8..f8e92d17d 100644 --- a/Bikes.Infrastructure.EfCore/DataSeeder.cs +++ b/Bikes.Infrastructure.EfCore/DataSeeder.cs @@ -1,4 +1,5 @@ using Bikes.Domain.Models; +using Microsoft.EntityFrameworkCore; namespace Bikes.Infrastructure.EfCore; @@ -25,6 +26,21 @@ public static void Seed(this BikesDbContext context) context.Rents.AddRange(rents); context.SaveChanges(); + + FixSequences(context); + } + + private static void FixSequences(BikesDbContext context) + { + var tables = new[] { "bike_models", "bikes", "renters", "rents" }; + + foreach (var table in tables) + { + context.Database.ExecuteSqlRaw( + $@"SELECT setval(pg_get_serial_sequence('{table}', 'id'), + COALESCE((SELECT MAX(id) FROM {table}), 1), true);" + ); + } } private static List InitializeModels() diff --git a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs index 49d6f1900..47645ddbb 100644 --- a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs +++ b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs @@ -62,7 +62,13 @@ public void DeleteBike(int id) context.SaveChanges(); } } +} +/// +/// EF Core implementation of bike model repository +/// +public class EfCoreBikeModelRepository(BikesDbContext context) : IBikeModelRepository +{ /// /// Get all bike models /// @@ -113,7 +119,13 @@ public void DeleteModel(int id) context.SaveChanges(); } } +} +/// +/// EF Core implementation of renter repository +/// +public class EfCoreRenterRepository(BikesDbContext context) : IRenterRepository +{ /// /// Get all renters /// @@ -164,7 +176,13 @@ public void DeleteRenter(int id) context.SaveChanges(); } } +} +/// +/// EF Core implementation of rent repository +/// +public class EfCoreRentRepository(BikesDbContext context) : IRentRepository +{ /// /// Get all rents /// diff --git a/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs index 37225ea0a..fbb96b66c 100644 --- a/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs +++ b/Bikes.Infrastructure.EfCore/Migrations/20251117201300_Initial.cs @@ -1,131 +1,129 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace Bikes.Infrastructure.EfCore.Migrations +namespace Bikes.Infrastructure.EfCore.Migrations; + +/// +public partial class Initial : Migration { /// - public partial class Initial : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "bike_models", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - type = table.Column(type: "text", nullable: false), - wheel_size = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), - max_weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), - weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), - brake_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - model_year = table.Column(type: "integer", nullable: false), - price_per_hour = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_bike_models", x => x.id); - }); + migrationBuilder.CreateTable( + name: "bike_models", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + type = table.Column(type: "text", nullable: false), + wheel_size = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + max_weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), + weight = table.Column(type: "numeric(7,2)", precision: 7, scale: 2, nullable: false), + brake_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + model_year = table.Column(type: "integer", nullable: false), + price_per_hour = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_bike_models", x => x.id); + }); - migrationBuilder.CreateTable( - name: "renters", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - full_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_renters", x => x.id); - }); + migrationBuilder.CreateTable( + name: "renters", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + full_name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_renters", x => x.id); + }); - migrationBuilder.CreateTable( - name: "bikes", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - serial_number = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - model_id = table.Column(type: "integer", nullable: false), - color = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), - is_available = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_bikes", x => x.id); - table.ForeignKey( - name: "FK_bikes_bike_models_model_id", - column: x => x.model_id, - principalTable: "bike_models", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - }); + migrationBuilder.CreateTable( + name: "bikes", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + serial_number = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + model_id = table.Column(type: "integer", nullable: false), + color = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + is_available = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_bikes", x => x.id); + table.ForeignKey( + name: "FK_bikes_bike_models_model_id", + column: x => x.model_id, + principalTable: "bike_models", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); - migrationBuilder.CreateTable( - name: "rents", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - bike_id = table.Column(type: "integer", nullable: false), - renter_id = table.Column(type: "integer", nullable: false), - start_time = table.Column(type: "timestamp with time zone", nullable: false), - duration_hours = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_rents", x => x.id); - table.ForeignKey( - name: "FK_rents_bikes_bike_id", - column: x => x.bike_id, - principalTable: "bikes", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_rents_renters_renter_id", - column: x => x.renter_id, - principalTable: "renters", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - }); + migrationBuilder.CreateTable( + name: "rents", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + bike_id = table.Column(type: "integer", nullable: false), + renter_id = table.Column(type: "integer", nullable: false), + start_time = table.Column(type: "timestamp with time zone", nullable: false), + duration_hours = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_rents", x => x.id); + table.ForeignKey( + name: "FK_rents_bikes_bike_id", + column: x => x.bike_id, + principalTable: "bikes", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_rents_renters_renter_id", + column: x => x.renter_id, + principalTable: "renters", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); - migrationBuilder.CreateIndex( - name: "IX_bikes_model_id", - table: "bikes", - column: "model_id"); + migrationBuilder.CreateIndex( + name: "IX_bikes_model_id", + table: "bikes", + column: "model_id"); - migrationBuilder.CreateIndex( - name: "IX_rents_bike_id", - table: "rents", - column: "bike_id"); + migrationBuilder.CreateIndex( + name: "IX_rents_bike_id", + table: "rents", + column: "bike_id"); - migrationBuilder.CreateIndex( - name: "IX_rents_renter_id", - table: "rents", - column: "renter_id"); - } + migrationBuilder.CreateIndex( + name: "IX_rents_renter_id", + table: "rents", + column: "renter_id"); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "rents"); + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "rents"); - migrationBuilder.DropTable( - name: "bikes"); + migrationBuilder.DropTable( + name: "bikes"); - migrationBuilder.DropTable( - name: "renters"); + migrationBuilder.DropTable( + name: "renters"); - migrationBuilder.DropTable( - name: "bike_models"); - } + migrationBuilder.DropTable( + name: "bike_models"); } -} +} \ No newline at end of file From aee4a1c8135dacb78c326be579dac8216e52f0ff Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 19 Dec 2025 01:55:08 +0400 Subject: [PATCH 17/19] =?UTF-8?q?=D0=A1corrected=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bikes.AppHost/AppHost.cs | 2 - .../Services/AnalyticsService.cs | 3 +- Bikes.Domain/Bikes.Domain.csproj | 4 - .../Repositories/IBikeModelRepository.cs | 15 ++ Bikes.Domain/Repositories/IBikeRepository.cs | 36 ---- Bikes.Domain/Repositories/IRentRepository.cs | 15 ++ .../Repositories/IRenterRepository.cs | 15 ++ Bikes.Infrastructure.EfCore/DataSeeder.cs | 12 +- .../EfCoreBikeModelRepository.cs | 62 ++++++ .../EfCoreBikeRepository.cs | 179 +----------------- .../EfCoreRentRepository.cs | 69 +++++++ .../EfCoreRenterRepository.cs | 62 ++++++ 12 files changed, 248 insertions(+), 226 deletions(-) create mode 100644 Bikes.Domain/Repositories/IBikeModelRepository.cs create mode 100644 Bikes.Domain/Repositories/IRentRepository.cs create mode 100644 Bikes.Domain/Repositories/IRenterRepository.cs create mode 100644 Bikes.Infrastructure.EfCore/EfCoreBikeModelRepository.cs create mode 100644 Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs create mode 100644 Bikes.Infrastructure.EfCore/EfCoreRenterRepository.cs diff --git a/Bikes.AppHost/AppHost.cs b/Bikes.AppHost/AppHost.cs index bde066cdf..5a86ccb42 100644 --- a/Bikes.AppHost/AppHost.cs +++ b/Bikes.AppHost/AppHost.cs @@ -1,5 +1,3 @@ -using Aspire.Hosting; - var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres"); diff --git a/Bikes.Application/Services/AnalyticsService.cs b/Bikes.Application/Services/AnalyticsService.cs index 7b7ca0204..8be828e02 100644 --- a/Bikes.Application/Services/AnalyticsService.cs +++ b/Bikes.Application/Services/AnalyticsService.cs @@ -10,8 +10,7 @@ namespace Bikes.Application.Services; /// public class AnalyticsService( IBikeRepository bikeRepository, - IRentRepository rentRepository, - IBikeModelRepository bikeModelRepository) : IAnalyticsService + IRentRepository rentRepository) : IAnalyticsService { /// /// Get all sport bikes diff --git a/Bikes.Domain/Bikes.Domain.csproj b/Bikes.Domain/Bikes.Domain.csproj index bbf0ab3d6..fa71b7ae6 100644 --- a/Bikes.Domain/Bikes.Domain.csproj +++ b/Bikes.Domain/Bikes.Domain.csproj @@ -6,8 +6,4 @@ enable - - - - diff --git a/Bikes.Domain/Repositories/IBikeModelRepository.cs b/Bikes.Domain/Repositories/IBikeModelRepository.cs new file mode 100644 index 000000000..ca3281fd5 --- /dev/null +++ b/Bikes.Domain/Repositories/IBikeModelRepository.cs @@ -0,0 +1,15 @@ +using Bikes.Domain.Models; + +namespace Bikes.Domain.Repositories; + +/// +/// Repository for bike model data access +/// +public interface IBikeModelRepository +{ + public List GetAllModels(); + public BikeModel? GetModelById(int id); + public void AddModel(BikeModel model); + public void UpdateModel(BikeModel model); + public void DeleteModel(int id); +} \ No newline at end of file diff --git a/Bikes.Domain/Repositories/IBikeRepository.cs b/Bikes.Domain/Repositories/IBikeRepository.cs index 9dada527c..b63b496be 100644 --- a/Bikes.Domain/Repositories/IBikeRepository.cs +++ b/Bikes.Domain/Repositories/IBikeRepository.cs @@ -12,40 +12,4 @@ public interface IBikeRepository public void AddBike(Bike bike); public void UpdateBike(Bike bike); public void DeleteBike(int id); -} - -/// -/// Repository for bike model data access -/// -public interface IBikeModelRepository -{ - public List GetAllModels(); - public BikeModel? GetModelById(int id); - public void AddModel(BikeModel model); - public void UpdateModel(BikeModel model); - public void DeleteModel(int id); -} - -/// -/// Repository for renter data access -/// -public interface IRenterRepository -{ - public List GetAllRenters(); - public Renter? GetRenterById(int id); - public void AddRenter(Renter renter); - public void UpdateRenter(Renter renter); - public void DeleteRenter(int id); -} - -/// -/// Repository for rent data access -/// -public interface IRentRepository -{ - public List GetAllRents(); - public Rent? GetRentById(int id); - public void AddRent(Rent rent); - public void UpdateRent(Rent rent); - public void DeleteRent(int id); } \ No newline at end of file diff --git a/Bikes.Domain/Repositories/IRentRepository.cs b/Bikes.Domain/Repositories/IRentRepository.cs new file mode 100644 index 000000000..ca1fc55d3 --- /dev/null +++ b/Bikes.Domain/Repositories/IRentRepository.cs @@ -0,0 +1,15 @@ +using Bikes.Domain.Models; + +namespace Bikes.Domain.Repositories; + +/// +/// Repository for rent data access +/// +public interface IRentRepository +{ + public List GetAllRents(); + public Rent? GetRentById(int id); + public void AddRent(Rent rent); + public void UpdateRent(Rent rent); + public void DeleteRent(int id); +} \ No newline at end of file diff --git a/Bikes.Domain/Repositories/IRenterRepository.cs b/Bikes.Domain/Repositories/IRenterRepository.cs new file mode 100644 index 000000000..2443e2560 --- /dev/null +++ b/Bikes.Domain/Repositories/IRenterRepository.cs @@ -0,0 +1,15 @@ +using Bikes.Domain.Models; + +namespace Bikes.Domain.Repositories; + +/// +/// Repository for renter data access +/// +public interface IRenterRepository +{ + public List GetAllRenters(); + public Renter? GetRenterById(int id); + public void AddRenter(Renter renter); + public void UpdateRenter(Renter renter); + public void DeleteRenter(int id); +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/DataSeeder.cs b/Bikes.Infrastructure.EfCore/DataSeeder.cs index f8e92d17d..90ddf024f 100644 --- a/Bikes.Infrastructure.EfCore/DataSeeder.cs +++ b/Bikes.Infrastructure.EfCore/DataSeeder.cs @@ -36,10 +36,14 @@ private static void FixSequences(BikesDbContext context) foreach (var table in tables) { - context.Database.ExecuteSqlRaw( - $@"SELECT setval(pg_get_serial_sequence('{table}', 'id'), - COALESCE((SELECT MAX(id) FROM {table}), 1), true);" - ); + var sql = $@" + SELECT setval( + pg_get_serial_sequence('{table}', 'id'), + COALESCE((SELECT MAX(id) FROM {table}), 1), + true + )"; + + context.Database.ExecuteSqlRaw(sql); } } diff --git a/Bikes.Infrastructure.EfCore/EfCoreBikeModelRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreBikeModelRepository.cs new file mode 100644 index 000000000..27c195b68 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/EfCoreBikeModelRepository.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// EF Core implementation of bike model repository +/// +public class EfCoreBikeModelRepository(BikesDbContext context) : IBikeModelRepository +{ + /// + /// Get all bike models + /// + public List GetAllModels() + { + return [.. context.BikeModels.AsNoTracking()]; + } + + /// + /// Get bike model by identifier + /// + public BikeModel? GetModelById(int id) + { + return context.BikeModels + .AsNoTracking() + .FirstOrDefault(m => m.Id == id); + } + + /// + /// Add new bike model + /// + public void AddModel(BikeModel model) + { + ArgumentNullException.ThrowIfNull(model); + context.BikeModels.Add(model); + context.SaveChanges(); + } + + /// + /// Update bike model + /// + public void UpdateModel(BikeModel model) + { + ArgumentNullException.ThrowIfNull(model); + context.BikeModels.Update(model); + context.SaveChanges(); + } + + /// + /// Delete bike model + /// + public void DeleteModel(int id) + { + var model = context.BikeModels.Find(id); + if (model != null) + { + context.BikeModels.Remove(model); + context.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs index 47645ddbb..77b245045 100644 --- a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs +++ b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs @@ -63,181 +63,4 @@ public void DeleteBike(int id) } } } - -/// -/// EF Core implementation of bike model repository -/// -public class EfCoreBikeModelRepository(BikesDbContext context) : IBikeModelRepository -{ - /// - /// Get all bike models - /// - public List GetAllModels() - { - return [.. context.BikeModels.AsNoTracking()]; - } - - /// - /// Get bike model by identifier - /// - public BikeModel? GetModelById(int id) - { - return context.BikeModels - .AsNoTracking() - .FirstOrDefault(m => m.Id == id); - } - - /// - /// Add new bike model - /// - public void AddModel(BikeModel model) - { - ArgumentNullException.ThrowIfNull(model); - context.BikeModels.Add(model); - context.SaveChanges(); - } - - /// - /// Update bike model - /// - public void UpdateModel(BikeModel model) - { - ArgumentNullException.ThrowIfNull(model); - context.BikeModels.Update(model); - context.SaveChanges(); - } - - /// - /// Delete bike model - /// - public void DeleteModel(int id) - { - var model = context.BikeModels.Find(id); - if (model != null) - { - context.BikeModels.Remove(model); - context.SaveChanges(); - } - } -} - -/// -/// EF Core implementation of renter repository -/// -public class EfCoreRenterRepository(BikesDbContext context) : IRenterRepository -{ - /// - /// Get all renters - /// - public List GetAllRenters() - { - return [.. context.Renters.AsNoTracking()]; - } - - /// - /// Get renter by identifier - /// - public Renter? GetRenterById(int id) - { - return context.Renters - .AsNoTracking() - .FirstOrDefault(r => r.Id == id); - } - - /// - /// Add new renter - /// - public void AddRenter(Renter renter) - { - ArgumentNullException.ThrowIfNull(renter); - context.Renters.Add(renter); - context.SaveChanges(); - } - - /// - /// Update renter - /// - public void UpdateRenter(Renter renter) - { - ArgumentNullException.ThrowIfNull(renter); - context.Renters.Update(renter); - context.SaveChanges(); - } - - /// - /// Delete renter - /// - public void DeleteRenter(int id) - { - var renter = context.Renters.Find(id); - if (renter != null) - { - context.Renters.Remove(renter); - context.SaveChanges(); - } - } -} - -/// -/// EF Core implementation of rent repository -/// -public class EfCoreRentRepository(BikesDbContext context) : IRentRepository -{ - /// - /// Get all rents - /// - public List GetAllRents() - { - return [.. context.Rents - .Include(r => r.Bike) - .ThenInclude(b => b.Model) - .Include(r => r.Renter) - .AsNoTracking()]; - } - - /// - /// Get rent by identifier - /// - public Rent? GetRentById(int id) - { - return context.Rents - .Include(r => r.Bike) - .ThenInclude(b => b.Model) - .Include(r => r.Renter) - .AsNoTracking() - .FirstOrDefault(r => r.Id == id); - } - - /// - /// Add new rent - /// - public void AddRent(Rent rent) - { - ArgumentNullException.ThrowIfNull(rent); - context.Rents.Add(rent); - context.SaveChanges(); - } - - /// - /// Update rent - /// - public void UpdateRent(Rent rent) - { - ArgumentNullException.ThrowIfNull(rent); - context.Rents.Update(rent); - context.SaveChanges(); - } - - /// - /// Delete rent - /// - public void DeleteRent(int id) - { - var rent = context.Rents.Find(id); - if (rent != null) - { - context.Rents.Remove(rent); - context.SaveChanges(); - } - } -} \ No newline at end of file +// НИЧЕГО БОЛЬШЕ ТУТ НЕ ДОЛЖНО БЫТЬ! \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs new file mode 100644 index 000000000..2606fe9f5 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// EF Core implementation of rent repository +/// +public class EfCoreRentRepository(BikesDbContext context) : IRentRepository +{ + /// + /// Get all rents + /// + public List GetAllRents() + { + return [.. context.Rents + .Include(r => r.Bike) + .ThenInclude(b => b.Model) + .Include(r => r.Renter) + .AsNoTracking()]; + } + + /// + /// Get rent by identifier + /// + public Rent? GetRentById(int id) + { + return context.Rents + .Include(r => r.Bike) + .ThenInclude(b => b.Model) + .Include(r => r.Renter) + .AsNoTracking() + .FirstOrDefault(r => r.Id == id); + } + + /// + /// Add new rent + /// + public void AddRent(Rent rent) + { + ArgumentNullException.ThrowIfNull(rent); + context.Rents.Add(rent); + context.SaveChanges(); + } + + /// + /// Update rent + /// + public void UpdateRent(Rent rent) + { + ArgumentNullException.ThrowIfNull(rent); + context.Rents.Update(rent); + context.SaveChanges(); + } + + /// + /// Delete rent + /// + public void DeleteRent(int id) + { + var rent = context.Rents.Find(id); + if (rent != null) + { + context.Rents.Remove(rent); + context.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/EfCoreRenterRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreRenterRepository.cs new file mode 100644 index 000000000..cb1f6aec0 --- /dev/null +++ b/Bikes.Infrastructure.EfCore/EfCoreRenterRepository.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore; +using Bikes.Domain.Models; +using Bikes.Domain.Repositories; + +namespace Bikes.Infrastructure.EfCore; + +/// +/// EF Core implementation of renter repository +/// +public class EfCoreRenterRepository(BikesDbContext context) : IRenterRepository +{ + /// + /// Get all renters + /// + public List GetAllRenters() + { + return [.. context.Renters.AsNoTracking()]; + } + + /// + /// Get renter by identifier + /// + public Renter? GetRenterById(int id) + { + return context.Renters + .AsNoTracking() + .FirstOrDefault(r => r.Id == id); + } + + /// + /// Add new renter + /// + public void AddRenter(Renter renter) + { + ArgumentNullException.ThrowIfNull(renter); + context.Renters.Add(renter); + context.SaveChanges(); + } + + /// + /// Update renter + /// + public void UpdateRenter(Renter renter) + { + ArgumentNullException.ThrowIfNull(renter); + context.Renters.Update(renter); + context.SaveChanges(); + } + + /// + /// Delete renter + /// + public void DeleteRenter(int id) + { + var renter = context.Renters.Find(id); + if (renter != null) + { + context.Renters.Remove(renter); + context.SaveChanges(); + } + } +} \ No newline at end of file From 9043f60e8a1cd2c984b67c4b9d0f5de3a69f75e9 Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Wed, 24 Dec 2025 20:56:07 +0400 Subject: [PATCH 18/19] Added Kafka generator and consumer for streaming data transfer between services --- Bikes.Api.Host/Program.cs | 14 +- Bikes.AppHost/AppHost.cs | 34 ++- Bikes.AppHost/Bikes.AppHost.csproj | 41 ++-- Bikes.AppHost/appsettings.json | 21 ++ .../Rents/IRentService.cs | 4 + Bikes.Application/Services/RentService.cs | 23 ++ .../Bikes.Generator.Kafka.csproj | 22 ++ Bikes.Generator.Kafka/IDataGenerator.cs | 12 ++ Bikes.Generator.Kafka/KafkaProducerService.cs | 87 ++++++++ Bikes.Generator.Kafka/Program.cs | 31 +++ .../Properties/launchSettings.json | 12 ++ Bikes.Generator.Kafka/RandomDataGenerator.cs | 62 ++++++ .../Serializers/KeySerializer.cs | 15 ++ .../Serializers/ValueSerializer.cs | 21 ++ .../Services/IProducerService.cs | 8 + .../Services/KafkaGeneratorService.cs | 107 ++++++++++ .../appsettings.Development.json | 8 + Bikes.Generator.Kafka/appsettings.json | 19 ++ .../EfCoreBikeRepository.cs | 3 +- .../Bikes.Infrastructure.Kafka.csproj | 28 +++ .../Deserializers/KeyDeserializer.cs | 16 ++ .../Deserializers/ValueDeserializer.cs | 27 +++ Bikes.Infrastructure.Kafka/KafkaConsumer.cs | 202 ++++++++++++++++++ Bikes.Infrastructure.Kafka/Program.cs | 31 +++ .../Properties/launchSettings.json | 11 + Bikes.Infrastructure.Kafka/Worker.cs | 23 ++ .../appsettings.Development.json | 8 + Bikes.Infrastructure.Kafka/appsettings.json | 12 ++ Bikes.sln | 28 +++ 29 files changed, 907 insertions(+), 23 deletions(-) create mode 100644 Bikes.AppHost/appsettings.json create mode 100644 Bikes.Generator.Kafka/Bikes.Generator.Kafka.csproj create mode 100644 Bikes.Generator.Kafka/IDataGenerator.cs create mode 100644 Bikes.Generator.Kafka/KafkaProducerService.cs create mode 100644 Bikes.Generator.Kafka/Program.cs create mode 100644 Bikes.Generator.Kafka/Properties/launchSettings.json create mode 100644 Bikes.Generator.Kafka/RandomDataGenerator.cs create mode 100644 Bikes.Generator.Kafka/Serializers/KeySerializer.cs create mode 100644 Bikes.Generator.Kafka/Serializers/ValueSerializer.cs create mode 100644 Bikes.Generator.Kafka/Services/IProducerService.cs create mode 100644 Bikes.Generator.Kafka/Services/KafkaGeneratorService.cs create mode 100644 Bikes.Generator.Kafka/appsettings.Development.json create mode 100644 Bikes.Generator.Kafka/appsettings.json create mode 100644 Bikes.Infrastructure.Kafka/Bikes.Infrastructure.Kafka.csproj create mode 100644 Bikes.Infrastructure.Kafka/Deserializers/KeyDeserializer.cs create mode 100644 Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs create mode 100644 Bikes.Infrastructure.Kafka/KafkaConsumer.cs create mode 100644 Bikes.Infrastructure.Kafka/Program.cs create mode 100644 Bikes.Infrastructure.Kafka/Properties/launchSettings.json create mode 100644 Bikes.Infrastructure.Kafka/Worker.cs create mode 100644 Bikes.Infrastructure.Kafka/appsettings.Development.json create mode 100644 Bikes.Infrastructure.Kafka/appsettings.json diff --git a/Bikes.Api.Host/Program.cs b/Bikes.Api.Host/Program.cs index cd1ac1d4f..14b3b6434 100644 --- a/Bikes.Api.Host/Program.cs +++ b/Bikes.Api.Host/Program.cs @@ -16,6 +16,17 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy( + policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("bikes-db")); @@ -36,13 +47,14 @@ var app = builder.Build(); +app.UseCors(); + if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } -app.UseHttpsRedirection(); app.UseAuthorization(); app.MapDefaultEndpoints(); app.MapControllers(); diff --git a/Bikes.AppHost/AppHost.cs b/Bikes.AppHost/AppHost.cs index 5a86ccb42..8ffb62041 100644 --- a/Bikes.AppHost/AppHost.cs +++ b/Bikes.AppHost/AppHost.cs @@ -1,10 +1,42 @@ +using Microsoft.Extensions.Configuration; + var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres"); var bikesDb = postgres.AddDatabase("bikes-db"); -builder.AddProject("bikes-api") +var api = builder.AddProject("bikes-api") .WithReference(bikesDb) .WaitFor(bikesDb); +var kafka = builder.AddKafka("bikes-kafka") + .WithKafkaUI(); + +var generatorSettings = builder.Configuration.GetSection("Generator"); +var batchSize = generatorSettings.GetValue("BatchSize", 10); +var payloadLimit = generatorSettings.GetValue("PayloadLimit", 100); +var waitTime = generatorSettings.GetValue("WaitTime", 10000); + +var kafkaSettings = builder.Configuration.GetSection("Kafka"); +var topic = kafkaSettings["Topic"] ?? "bike-rents"; +var groupId = kafkaSettings["GroupId"] ?? "bikes-consumer-group"; + +var consumer = builder.AddProject("bikes-consumer") + .WithReference(kafka) + .WithReference(bikesDb) + .WaitFor(kafka) + .WithEnvironment("Kafka__Topic", topic) + .WithEnvironment("Kafka__GroupId", groupId) + .WithEnvironment("ConnectionStrings__bikes-db", bikesDb); + +var generator = builder.AddProject("bikes-generator") + .WithReference(kafka) + .WaitFor(kafka) + .WaitFor(api) + .WaitFor(consumer) + .WithEnvironment("Kafka__Topic", topic) + .WithEnvironment("Generator__BatchSize", batchSize.ToString()) + .WithEnvironment("Generator__PayloadLimit", payloadLimit.ToString()) + .WithEnvironment("Generator__WaitTime", waitTime.ToString()); + builder.Build().Run(); \ No newline at end of file diff --git a/Bikes.AppHost/Bikes.AppHost.csproj b/Bikes.AppHost/Bikes.AppHost.csproj index 4e3acc588..0d24da477 100644 --- a/Bikes.AppHost/Bikes.AppHost.csproj +++ b/Bikes.AppHost/Bikes.AppHost.csproj @@ -1,23 +1,26 @@ - - + + - - Exe - net8.0 - enable - enable - true - aspire-host-id - + + Exe + net8.0 + enable + enable + true + aspire-host-id + - - - - - + + + + + + - - - - + + + + + + \ No newline at end of file diff --git a/Bikes.AppHost/appsettings.json b/Bikes.AppHost/appsettings.json new file mode 100644 index 000000000..833239c93 --- /dev/null +++ b/Bikes.AppHost/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "bikes-kafka": "localhost:51251", + "bikes-db": "Host=localhost;Port=5432;Database=bikes;Username=postgres;Password=postgres" + }, + "Kafka": { + "Topic": "bike-rents", + "GroupId": "bikes-consumer-group" + }, + "Generator": { + "BatchSize": 10, + "PayloadLimit": 100, + "WaitTime": 10000 + } +} \ No newline at end of file diff --git a/Bikes.Application.Contracts/Rents/IRentService.cs b/Bikes.Application.Contracts/Rents/IRentService.cs index 2bae57458..a2e03394c 100644 --- a/Bikes.Application.Contracts/Rents/IRentService.cs +++ b/Bikes.Application.Contracts/Rents/IRentService.cs @@ -5,4 +5,8 @@ /// public interface IRentService : IApplicationService { + /// + /// Receive and process list of rent contracts from Kafka + /// + public Task ReceiveContract(IList contracts); } \ No newline at end of file diff --git a/Bikes.Application/Services/RentService.cs b/Bikes.Application/Services/RentService.cs index c726020da..91eebbd2a 100644 --- a/Bikes.Application/Services/RentService.cs +++ b/Bikes.Application/Services/RentService.cs @@ -12,6 +12,29 @@ public class RentService( IBikeRepository bikeRepository, IRenterRepository renterRepository) : IRentService { + /// + /// Receive and process list of rent contracts from Kafka + /// + public async Task ReceiveContract(IList contracts) + { + if (contracts == null || contracts.Count == 0) + return; + + foreach (var contract in contracts) + { + try + { + Create(contract); + } + catch (InvalidOperationException) + { + continue; + } + } + + await Task.CompletedTask; + } + /// /// Get all rents /// diff --git a/Bikes.Generator.Kafka/Bikes.Generator.Kafka.csproj b/Bikes.Generator.Kafka/Bikes.Generator.Kafka.csproj new file mode 100644 index 000000000..453d31b77 --- /dev/null +++ b/Bikes.Generator.Kafka/Bikes.Generator.Kafka.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + dotnet-Bikes.Generator.Kafka-b1d58c3b-9059-4dd2-8b92-981ad7eb6764 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bikes.Generator.Kafka/IDataGenerator.cs b/Bikes.Generator.Kafka/IDataGenerator.cs new file mode 100644 index 000000000..6495026de --- /dev/null +++ b/Bikes.Generator.Kafka/IDataGenerator.cs @@ -0,0 +1,12 @@ +namespace Bikes.Generator.Kafka; + +/// +/// Interface for generating random test data +/// +public interface IDataGenerator +{ + public IEnumerable GenerateBikeModels(int count); + public IEnumerable GenerateBikes(int count, List modelIds); + public IEnumerable GenerateRenters(int count); + public IEnumerable GenerateRents(int count, List bikeIds, List renterIds); +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/KafkaProducerService.cs b/Bikes.Generator.Kafka/KafkaProducerService.cs new file mode 100644 index 000000000..030a9e1aa --- /dev/null +++ b/Bikes.Generator.Kafka/KafkaProducerService.cs @@ -0,0 +1,87 @@ +using Bikes.Generator.Kafka.Services; + +namespace Bikes.Generator.Kafka; + +/// +/// Background service for generating and sending a specified number of contracts at specified intervals +/// +public class KafkaProducerService : BackgroundService +{ + private readonly int _batchSize; + private readonly int _payloadLimit; + private readonly int _waitTime; + private readonly IProducerService _producer; + private readonly ILogger _logger; + private readonly IDataGenerator _dataGenerator; + + public KafkaProducerService( + IConfiguration configuration, + IProducerService producer, + ILogger logger, + IDataGenerator dataGenerator) + { + _batchSize = configuration.GetValue("Generator:BatchSize", 10); + _payloadLimit = configuration.GetValue("Generator:PayloadLimit", 100); + _waitTime = configuration.GetValue("Generator:WaitTime", 10000); + + if (_batchSize <= 0) + throw new ArgumentException($"Invalid argument value for BatchSize: {_batchSize}"); + if (_payloadLimit <= 0) + throw new ArgumentException($"Invalid argument value for PayloadLimit: {_payloadLimit}"); + if (_waitTime <= 0) + throw new ArgumentException($"Invalid argument value for WaitTime: {_waitTime}"); + + _producer = producer; + _logger = logger; + _dataGenerator = dataGenerator; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Waiting 30 seconds for Kafka to become fully ready..."); + await Task.Delay(60000, stoppingToken); + + _logger.LogInformation("Starting to send {total} messages with {time}s interval with {batch} messages in batch", + _payloadLimit, _waitTime / 1000, _batchSize); + + var counter = 0; + while (counter < _payloadLimit && !stoppingToken.IsCancellationRequested) + { + try + { + await GenerateAndSendBatchAsync(); + counter += _batchSize; + _logger.LogInformation("Sent {sent} messages, total: {total}/{limit}", + _batchSize, counter, _payloadLimit); + } + catch (Exception ex) + { + _logger.LogError(ex, "Send batch with error. Retry"); + } + + await Task.Delay(_waitTime, stoppingToken); + } + + _logger.LogInformation("Finished sending {total} messages with {time}s interval with {batch} messages in batch", + _payloadLimit, _waitTime / 1000, _batchSize); + } + + private async Task GenerateAndSendBatchAsync() + { + // Generate required data for rents + var models = _dataGenerator.GenerateBikeModels(_batchSize / 4).ToList(); + var renters = _dataGenerator.GenerateRenters(_batchSize / 4).ToList(); + var modelIds = Enumerable.Range(1, models.Count).ToList(); + var renterIds = Enumerable.Range(1, renters.Count).ToList(); + var bikes = _dataGenerator.GenerateBikes(_batchSize / 4, modelIds).ToList(); + var bikeIds = Enumerable.Range(1, bikes.Count).ToList(); + + // Generate only rents + var rents = _dataGenerator.GenerateRents(_batchSize, bikeIds, renterIds).ToList(); + + // Send list of rents + await _producer.SendAsync("bike-rents", rents); + + _logger.LogInformation("Generated {RentCount} rents", rents.Count); + } +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Program.cs b/Bikes.Generator.Kafka/Program.cs new file mode 100644 index 000000000..9fcb54bd0 --- /dev/null +++ b/Bikes.Generator.Kafka/Program.cs @@ -0,0 +1,31 @@ +using Bikes.Generator.Kafka; +using Bikes.Generator.Kafka.Services; +using Bikes.ServiceDefaults; + +var builder = Host.CreateApplicationBuilder(args); + +Console.WriteLine("=== Debug Info ==="); +Console.WriteLine($"ConnectionStrings__bikes-kafka: {builder.Configuration.GetConnectionString("bikes-kafka")}"); +Console.WriteLine($"Kafka:BootstrapServers: {builder.Configuration["Kafka:BootstrapServers"]}"); +Console.WriteLine($"Kafka__BootstrapServers: {builder.Configuration["Kafka__BootstrapServers"]}"); + +Console.WriteLine("=== All Kafka-related env vars ==="); +foreach (var env in Environment.GetEnvironmentVariables().Cast()) +{ + var key = env.Key.ToString()!; + if (key.Contains("kafka", StringComparison.OrdinalIgnoreCase) || + key.Contains("KAFKA", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"{key} = {env.Value}"); + } +} +Console.WriteLine("=================="); + +builder.AddServiceDefaults(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); + +var host = builder.Build(); +await host.RunAsync(); \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Properties/launchSettings.json b/Bikes.Generator.Kafka/Properties/launchSettings.json new file mode 100644 index 000000000..9507c37f1 --- /dev/null +++ b/Bikes.Generator.Kafka/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Bikes.Generator.Kafka": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Bikes.Generator.Kafka/RandomDataGenerator.cs b/Bikes.Generator.Kafka/RandomDataGenerator.cs new file mode 100644 index 000000000..b3855960f --- /dev/null +++ b/Bikes.Generator.Kafka/RandomDataGenerator.cs @@ -0,0 +1,62 @@ +using Bogus; +using Bikes.Application.Contracts.Bikes; +using Bikes.Application.Contracts.Models; +using Bikes.Application.Contracts.Renters; +using Bikes.Application.Contracts.Rents; + +namespace Bikes.Generator.Kafka; + +/// +/// Implementation of IDataGenerator using Bogus for generating random test data +/// +public class RandomDataGenerator : IDataGenerator +{ + private readonly Faker _bikeModelFaker; + private readonly Faker _bikeFaker; + private readonly Faker _renterFaker; + private readonly Faker _rentFaker; + + public RandomDataGenerator() + { + var bikeTypes = new[] { "Mountain", "Road", "Hybrid", "City", "Sport" }; + var brakeTypes = new[] { "Mechanical", "Hydraulic", "Rim" }; + var colors = new[] { "Red", "Blue", "Green", "Black", "White", "Yellow", "Silver" }; + + _bikeModelFaker = new Faker() + .RuleFor(m => m.Name, f => $"Model {f.Commerce.ProductName()}") + .RuleFor(m => m.Type, f => f.PickRandom(bikeTypes)) + .RuleFor(m => m.WheelSize, f => f.Random.Decimal(10, 30)) + .RuleFor(m => m.MaxWeight, f => f.Random.Decimal(50, 300)) + .RuleFor(m => m.Weight, f => f.Random.Decimal(5, 50)) + .RuleFor(m => m.BrakeType, f => f.PickRandom(brakeTypes)) + .RuleFor(m => m.ModelYear, f => f.Random.Int(2000, 2024)) + .RuleFor(m => m.PricePerHour, f => f.Random.Decimal(1, 50)); + + _bikeFaker = new Faker() + .RuleFor(b => b.SerialNumber, f => $"SN{f.Random.AlphaNumeric(8).ToUpper()}") + .RuleFor(b => b.ModelId, f => f.Random.Int(1, 100)) + .RuleFor(b => b.Color, f => f.PickRandom(colors)); + + _renterFaker = new Faker() + .RuleFor(r => r.FullName, f => f.Name.FullName()) + .RuleFor(r => r.Phone, f => f.Phone.PhoneNumber("+7##########")); + + _rentFaker = new Faker() + .RuleFor(r => r.BikeId, f => f.Random.Int(1, 1000)) + .RuleFor(r => r.RenterId, f => f.Random.Int(1, 500)) + .RuleFor(r => r.StartTime, f => f.Date.Recent(30)) + .RuleFor(r => r.DurationHours, f => f.Random.Int(1, 168)); + } + + public IEnumerable GenerateBikeModels(int count) + => _bikeModelFaker.Generate(count); + + public IEnumerable GenerateBikes(int count, List modelIds) + => _bikeFaker.Generate(count); + + public IEnumerable GenerateRenters(int count) + => _renterFaker.Generate(count); + + public IEnumerable GenerateRents(int count, List bikeIds, List renterIds) + => _rentFaker.Generate(count); +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Serializers/KeySerializer.cs b/Bikes.Generator.Kafka/Serializers/KeySerializer.cs new file mode 100644 index 000000000..cf9b30659 --- /dev/null +++ b/Bikes.Generator.Kafka/Serializers/KeySerializer.cs @@ -0,0 +1,15 @@ +using Confluent.Kafka; +using System.Text; + +namespace Bikes.Generator.Kafka.Serializers; + +/// +/// Serializer for Guid keys in Kafka messages +/// +public class KeySerializer : ISerializer +{ + public byte[] Serialize(Guid data, SerializationContext context) + { + return Encoding.UTF8.GetBytes(data.ToString()); + } +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs b/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs new file mode 100644 index 000000000..29ef6c408 --- /dev/null +++ b/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs @@ -0,0 +1,21 @@ +using Confluent.Kafka; +using System.Text; +using System.Text.Json; +using Bikes.Application.Contracts.Rents; + +namespace Bikes.Generator.Kafka.Serializers; + +/// +/// Serializer for list of RentCreateUpdateDto values in Kafka messages +/// +public class ValueSerializer : ISerializer> +{ + public byte[] Serialize(IList data, SerializationContext context) + { + if (data == null || data.Count == 0) + return Array.Empty(); + + var json = JsonSerializer.Serialize(data); + return Encoding.UTF8.GetBytes(json); + } +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Services/IProducerService.cs b/Bikes.Generator.Kafka/Services/IProducerService.cs new file mode 100644 index 000000000..185d2371b --- /dev/null +++ b/Bikes.Generator.Kafka/Services/IProducerService.cs @@ -0,0 +1,8 @@ +using Bikes.Application.Contracts.Rents; + +namespace Bikes.Generator.Kafka.Services; + +public interface IProducerService +{ + public Task SendAsync(string topic, IList items); +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/Services/KafkaGeneratorService.cs b/Bikes.Generator.Kafka/Services/KafkaGeneratorService.cs new file mode 100644 index 000000000..8cd7196a2 --- /dev/null +++ b/Bikes.Generator.Kafka/Services/KafkaGeneratorService.cs @@ -0,0 +1,107 @@ +using Bikes.Generator.Kafka.Serializers; +using Confluent.Kafka; +using Polly; +using Bikes.Application.Contracts.Rents; + +namespace Bikes.Generator.Kafka.Services; + +/// +/// Service for producing messages to Kafka with retry logic and error handling +/// +public class KafkaGeneratorService : IProducerService, IDisposable +{ + private readonly ILogger _logger; + private readonly IProducer> _producer; + private readonly int _retryCount; + private readonly int _retryDelayMs; + private bool _disposed = false; + + public KafkaGeneratorService( + ILogger logger, + IConfiguration configuration) + { + _logger = logger; + + var bootstrapServers = configuration.GetConnectionString("bikes-kafka") + ?? configuration["Kafka:BootstrapServers"] + ?? configuration["Kafka__BootstrapServers"] + ?? "localhost:9092"; + + _logger.LogInformation("Kafka BootstrapServers: {BootstrapServers}", bootstrapServers); + + var config = new ProducerConfig + { + BootstrapServers = bootstrapServers, + MessageSendMaxRetries = 20, + RetryBackoffMs = 10000, + SocketTimeoutMs = 60000, + MessageTimeoutMs = 120000, + EnableDeliveryReports = true, + Acks = Acks.All, + SecurityProtocol = SecurityProtocol.Plaintext + }; + + _producer = new ProducerBuilder>(config) + .SetKeySerializer(new KeySerializer()) + .SetValueSerializer(new ValueSerializer()) + .SetLogHandler((_, message) => + { + _logger.LogDebug("Kafka log: {Facility} {Message}", message.Facility, message.Message); + }) + .SetErrorHandler((_, error) => + { + if (error.IsFatal) + _logger.LogError("Kafka fatal error: {Reason} (Code: {Code})", error.Reason, error.Code); + else + _logger.LogWarning("Kafka error: {Reason} (Code: {Code})", error.Reason, error.Code); + }) + .Build(); + + _retryCount = configuration.GetValue("Kafka:RetryCount", 10); + _retryDelayMs = configuration.GetValue("Kafka:RetryDelayMs", 5000); + } + + public async Task SendAsync(string topic, IList items) + { + if (items == null || items.Count == 0) + return; + + var retryPolicy = Policy + .Handle() + .WaitAndRetryAsync( + _retryCount, + attempt => TimeSpan.FromMilliseconds(_retryDelayMs * Math.Pow(2, attempt - 1)), + onRetry: (exception, delay, attempt, context) => + { + _logger.LogWarning(exception, + "Retry {Attempt}/{MaxAttempts} after {Delay}ms", + attempt, _retryCount, delay.TotalMilliseconds); + }); + + await retryPolicy.ExecuteAsync(async () => + { + var key = Guid.NewGuid(); + + var message = new Message> + { + Key = key, + Value = items + }; + + var result = await _producer.ProduceAsync(topic, message); + _logger.LogInformation("Sent {Count} items to Kafka topic {Topic} (Partition: {Partition}, Offset: {Offset})", + items.Count, topic, result.Partition, result.Offset); + }); + } + + public void Dispose() + { + if (!_disposed) + { + _producer?.Flush(TimeSpan.FromSeconds(5)); + _producer?.Dispose(); + _disposed = true; + } + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Bikes.Generator.Kafka/appsettings.Development.json b/Bikes.Generator.Kafka/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/Bikes.Generator.Kafka/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Bikes.Generator.Kafka/appsettings.json b/Bikes.Generator.Kafka/appsettings.json new file mode 100644 index 000000000..21582870d --- /dev/null +++ b/Bikes.Generator.Kafka/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Generator": { + "BatchSize": 10, + "PayloadLimit": 100, + "WaitTime": 10000 + }, + "Kafka": { + "BootstrapServers": "bikes-kafka:9092", + "Topic": "bike-rents", + "RetryCount": 10, + "RetryDelayMs": 5000 + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs index 77b245045..2085cc40d 100644 --- a/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs +++ b/Bikes.Infrastructure.EfCore/EfCoreBikeRepository.cs @@ -62,5 +62,4 @@ public void DeleteBike(int id) context.SaveChanges(); } } -} -// НИЧЕГО БОЛЬШЕ ТУТ НЕ ДОЛЖНО БЫТЬ! \ No newline at end of file +} \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Bikes.Infrastructure.Kafka.csproj b/Bikes.Infrastructure.Kafka/Bikes.Infrastructure.Kafka.csproj new file mode 100644 index 000000000..ae5c34526 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Bikes.Infrastructure.Kafka.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + dotnet-Bikes.Infrastructure.Kafka-33d6809d-7458-4668-a600-1d82cb896a7d + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Deserializers/KeyDeserializer.cs b/Bikes.Infrastructure.Kafka/Deserializers/KeyDeserializer.cs new file mode 100644 index 000000000..495bcb582 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Deserializers/KeyDeserializer.cs @@ -0,0 +1,16 @@ +using Confluent.Kafka; +using System.Text; + +namespace Bikes.Infrastructure.Kafka.Deserializers; + +public class KeyDeserializer : IDeserializer +{ + public Guid Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) + { + if (isNull || data.Length == 0) + return Guid.Empty; + + var guidString = Encoding.UTF8.GetString(data); + return Guid.Parse(guidString); + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs b/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs new file mode 100644 index 000000000..cbe5ae199 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs @@ -0,0 +1,27 @@ +using Confluent.Kafka; +using System.Text; +using System.Text.Json; +using Bikes.Application.Contracts.Rents; + +namespace Bikes.Infrastructure.Kafka.Deserializers; + +public class ValueDeserializer : IDeserializer> +{ + public IList Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) + { + if (isNull || data.Length == 0) + return new List(); + + var json = Encoding.UTF8.GetString(data); + + try + { + var result = JsonSerializer.Deserialize>(json); + return result ?? new List(); + } + catch (JsonException ex) + { + throw new InvalidOperationException($"Failed to deserialize JSON: {json}", ex); + } + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/KafkaConsumer.cs b/Bikes.Infrastructure.Kafka/KafkaConsumer.cs new file mode 100644 index 000000000..51d1e9504 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/KafkaConsumer.cs @@ -0,0 +1,202 @@ +using Confluent.Kafka; +using Bikes.Application.Contracts.Rents; +using Bikes.Infrastructure.Kafka.Deserializers; + +namespace Bikes.Infrastructure.Kafka; + +/// +/// Background service that consumes rent contract messages from Kafka topics +/// +public class KafkaConsumer : BackgroundService +{ + private readonly IConsumer> _consumer; + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private readonly string _topic; + private readonly string _bootstrapServers; + + public KafkaConsumer( + IConfiguration configuration, + ILogger logger, + KeyDeserializer keyDeserializer, + ValueDeserializer valueDeserializer, + IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + + var kafkaConfig = configuration.GetSection("Kafka"); + _topic = kafkaConfig["Topic"] ?? "bike-rents"; + + _bootstrapServers = configuration.GetConnectionString("bikes-kafka") + ?? configuration["Kafka:BootstrapServers"] + ?? "localhost:9092"; + + var groupId = kafkaConfig["GroupId"] ?? "bikes-consumer-group"; + + var consumerConfig = new ConsumerConfig + { + BootstrapServers = _bootstrapServers, + GroupId = groupId, + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = true, + AllowAutoCreateTopics = true, + EnablePartitionEof = true, + MaxPollIntervalMs = 300000, + SessionTimeoutMs = 45000 + }; + + _consumer = new ConsumerBuilder>(consumerConfig) + .SetKeyDeserializer(keyDeserializer) + .SetValueDeserializer(valueDeserializer) + .SetErrorHandler((_, e) => + { + if (e.IsFatal) + _logger.LogError("Kafka Fatal Error: {Reason} (Code: {Code})", e.Reason, e.Code); + else + _logger.LogWarning("Kafka Error: {Reason} (Code: {Code})", e.Reason, e.Code); + }) + .SetLogHandler((_, logMessage) => + { + _logger.LogDebug("Kafka Log: {Facility} - {Message}", logMessage.Facility, logMessage.Message); + }) + .Build(); + + _logger.LogInformation("Kafka Consumer configured for: {Servers}", _bootstrapServers); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Starting Kafka consumer for topic: {Topic}", _topic); + + await WaitForKafkaAvailableAsync(stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + _consumer.Subscribe(_topic); + _logger.LogInformation("Subscribed to Kafka topic: {Topic}", _topic); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var result = _consumer.Consume(stoppingToken); + + if (result == null) + continue; + + if (result.IsPartitionEOF) + { + _logger.LogDebug("Reached end of partition {Partition}", result.Partition); + continue; + } + + _logger.LogInformation( + "Received message {Key} with {Count} rents (Partition: {Partition}, Offset: {Offset})", + result.Message.Key, + result.Message.Value?.Count ?? 0, + result.Partition, + result.Offset + ); + + if (result.Message.Value != null) + { + await ProcessMessageAsync(result.Message.Key, result.Message.Value); + } + } + catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) + { + _logger.LogWarning("Topic {Topic} not available yet, retrying in 5 seconds...", _topic); + await Task.Delay(5000, stoppingToken); + break; + } + catch (ConsumeException ex) + { + _logger.LogError(ex, "Consume error: {Reason} (Code: {Code})", ex.Error.Reason, ex.Error.Code); + await Task.Delay(2000, stoppingToken); + } + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Kafka consumer cancelled"); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in Kafka consumer"); + await Task.Delay(5000, stoppingToken); + } + } + + _logger.LogInformation("Kafka consumer stopped"); + } + + private async Task WaitForKafkaAvailableAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Waiting for Kafka to become available..."); + + for (var i = 0; i < 30; i++) + { + try + { + using var adminClient = new AdminClientBuilder(new AdminClientConfig + { + BootstrapServers = _bootstrapServers + }).Build(); + + var metadata = adminClient.GetMetadata(TimeSpan.FromSeconds(5)); + _logger.LogInformation("Kafka is available. Brokers: {BrokerCount}", metadata.Brokers.Count); + return; + } + catch (Exception ex) + { + _logger.LogDebug("Kafka not available yet (attempt {Attempt}/30): {Message}", i + 1, ex.Message); + await Task.Delay(5000, stoppingToken); + } + } + + _logger.LogWarning("Kafka still not available after 150 seconds, continuing anyway..."); + } + + private async Task ProcessMessageAsync(Guid key, IList rents) + { + if (rents == null || rents.Count == 0) + { + _logger.LogWarning("Empty rents list for message {Key}", key); + return; + } + + try + { + using var scope = _scopeFactory.CreateScope(); + var rentService = scope.ServiceProvider.GetRequiredService(); + + await rentService.ReceiveContract(rents); + + _logger.LogInformation("Processed message {Key}: {Count} rent contracts saved", + key, rents.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing message {Key}", key); + } + } + + public override void Dispose() + { + try + { + _consumer?.Close(); + _consumer?.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing Kafka consumer"); + } + + base.Dispose(); + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Program.cs b/Bikes.Infrastructure.Kafka/Program.cs new file mode 100644 index 000000000..4bcf2bc87 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Program.cs @@ -0,0 +1,31 @@ +using Bikes.Infrastructure.Kafka; +using Bikes.Infrastructure.Kafka.Deserializers; +using Bikes.ServiceDefaults; +using Microsoft.EntityFrameworkCore; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddDbContext(options => +{ + var connectionString = builder.Configuration.GetConnectionString("bikes-db") + ?? "Host=localhost;Port=5432;Database=bikes;Username=postgres;Password=postgres"; + options.UseNpgsql(connectionString); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + sp.GetRequiredService()); + +builder.Services.AddHostedService(); + +var host = builder.Build(); +await host.RunAsync(); \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Properties/launchSettings.json b/Bikes.Infrastructure.Kafka/Properties/launchSettings.json new file mode 100644 index 000000000..f3b7eff27 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Bikes.Infrastructure.Kafka": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Worker.cs b/Bikes.Infrastructure.Kafka/Worker.cs new file mode 100644 index 000000000..fd07726c8 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/Worker.cs @@ -0,0 +1,23 @@ +namespace Bikes.Infrastructure.Kafka; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + await Task.Delay(1000, stoppingToken); + } + } +} diff --git a/Bikes.Infrastructure.Kafka/appsettings.Development.json b/Bikes.Infrastructure.Kafka/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/Bikes.Infrastructure.Kafka/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Bikes.Infrastructure.Kafka/appsettings.json b/Bikes.Infrastructure.Kafka/appsettings.json new file mode 100644 index 000000000..d9d2d78ce --- /dev/null +++ b/Bikes.Infrastructure.Kafka/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Kafka": { + "Topic": "bike-rents", + "GroupId": "bikes-consumer-group" + } +} \ No newline at end of file diff --git a/Bikes.sln b/Bikes.sln index e55a36201..03e741a5a 100644 --- a/Bikes.sln +++ b/Bikes.sln @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.AppHost", "Bikes.AppH EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.ServiceDefaults", "Bikes.ServiceDefaults\Bikes.ServiceDefaults.csproj", "{2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Generator.Kafka", "Bikes.Generator.Kafka\Bikes.Generator.Kafka.csproj", "{72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bikes.Infrastructure.Kafka", "Bikes.Infrastructure.Kafka\Bikes.Infrastructure.Kafka.csproj", "{8F0E4A35-35F3-41B4-B096-989EED9592BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +143,30 @@ Global {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x64.Build.0 = Release|Any CPU {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x86.ActiveCfg = Release|Any CPU {2AD23FB4-44F5-4B2E-9AC9-0E8266A6BE1E}.Release|x86.Build.0 = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|x64.Build.0 = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Debug|x86.Build.0 = Debug|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|Any CPU.Build.0 = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|x64.ActiveCfg = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|x64.Build.0 = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|x86.ActiveCfg = Release|Any CPU + {72ACBC0F-E8D4-4D02-A82B-C9A0A2D5E6DE}.Release|x86.Build.0 = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|x64.Build.0 = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Debug|x86.Build.0 = Debug|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|Any CPU.Build.0 = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|x64.ActiveCfg = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|x64.Build.0 = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|x86.ActiveCfg = Release|Any CPU + {8F0E4A35-35F3-41B4-B096-989EED9592BC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 6248b9d2987e6a705bcc1c241ea76c50884cca8f Mon Sep 17 00:00:00 2001 From: Leshechka123 Date: Fri, 26 Dec 2025 18:50:20 +0400 Subject: [PATCH 19/19] Fixed remarks --- Bikes.AppHost/AppHost.cs | 3 ++- Bikes.Generator.Kafka/KafkaProducerService.cs | 4 ++-- Bikes.Generator.Kafka/RandomDataGenerator.cs | 8 +++---- .../Serializers/ValueSerializer.cs | 2 +- .../EfCoreRentRepository.cs | 16 +++++++++++++ .../Deserializers/ValueDeserializer.cs | 4 ++-- Bikes.Infrastructure.Kafka/KafkaConsumer.cs | 11 +++++---- Bikes.Infrastructure.Kafka/Worker.cs | 23 ------------------- 8 files changed, 33 insertions(+), 38 deletions(-) delete mode 100644 Bikes.Infrastructure.Kafka/Worker.cs diff --git a/Bikes.AppHost/AppHost.cs b/Bikes.AppHost/AppHost.cs index 8ffb62041..e40704eab 100644 --- a/Bikes.AppHost/AppHost.cs +++ b/Bikes.AppHost/AppHost.cs @@ -25,11 +25,12 @@ .WithReference(kafka) .WithReference(bikesDb) .WaitFor(kafka) + .WaitFor(bikesDb) .WithEnvironment("Kafka__Topic", topic) .WithEnvironment("Kafka__GroupId", groupId) .WithEnvironment("ConnectionStrings__bikes-db", bikesDb); -var generator = builder.AddProject("bikes-generator") +_ = builder.AddProject("bikes-generator") .WithReference(kafka) .WaitFor(kafka) .WaitFor(api) diff --git a/Bikes.Generator.Kafka/KafkaProducerService.cs b/Bikes.Generator.Kafka/KafkaProducerService.cs index 030a9e1aa..80dfc88b9 100644 --- a/Bikes.Generator.Kafka/KafkaProducerService.cs +++ b/Bikes.Generator.Kafka/KafkaProducerService.cs @@ -38,8 +38,8 @@ public KafkaProducerService( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Waiting 30 seconds for Kafka to become fully ready..."); - await Task.Delay(60000, stoppingToken); + _logger.LogInformation("Waiting 15 seconds for Kafka to become fully ready..."); + await Task.Delay(15000, stoppingToken); _logger.LogInformation("Starting to send {total} messages with {time}s interval with {batch} messages in batch", _payloadLimit, _waitTime / 1000, _batchSize); diff --git a/Bikes.Generator.Kafka/RandomDataGenerator.cs b/Bikes.Generator.Kafka/RandomDataGenerator.cs index b3855960f..400e142ae 100644 --- a/Bikes.Generator.Kafka/RandomDataGenerator.cs +++ b/Bikes.Generator.Kafka/RandomDataGenerator.cs @@ -34,7 +34,7 @@ public RandomDataGenerator() _bikeFaker = new Faker() .RuleFor(b => b.SerialNumber, f => $"SN{f.Random.AlphaNumeric(8).ToUpper()}") - .RuleFor(b => b.ModelId, f => f.Random.Int(1, 100)) + .RuleFor(b => b.ModelId, f => f.Random.Int(1, 11)) .RuleFor(b => b.Color, f => f.PickRandom(colors)); _renterFaker = new Faker() @@ -42,9 +42,9 @@ public RandomDataGenerator() .RuleFor(r => r.Phone, f => f.Phone.PhoneNumber("+7##########")); _rentFaker = new Faker() - .RuleFor(r => r.BikeId, f => f.Random.Int(1, 1000)) - .RuleFor(r => r.RenterId, f => f.Random.Int(1, 500)) - .RuleFor(r => r.StartTime, f => f.Date.Recent(30)) + .RuleFor(r => r.BikeId, f => f.Random.Int(1, 10)) + .RuleFor(r => r.RenterId, f => f.Random.Int(1, 12)) + .RuleFor(r => r.StartTime, f => f.Date.Recent(30).ToUniversalTime()) .RuleFor(r => r.DurationHours, f => f.Random.Int(1, 168)); } diff --git a/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs b/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs index 29ef6c408..b6fdc83b8 100644 --- a/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs +++ b/Bikes.Generator.Kafka/Serializers/ValueSerializer.cs @@ -13,7 +13,7 @@ public class ValueSerializer : ISerializer> public byte[] Serialize(IList data, SerializationContext context) { if (data == null || data.Count == 0) - return Array.Empty(); + return []; var json = JsonSerializer.Serialize(data); return Encoding.UTF8.GetBytes(json); diff --git a/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs b/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs index 2606fe9f5..46d211c35 100644 --- a/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs +++ b/Bikes.Infrastructure.EfCore/EfCoreRentRepository.cs @@ -40,6 +40,22 @@ public List GetAllRents() public void AddRent(Rent rent) { ArgumentNullException.ThrowIfNull(rent); + + if (rent.Bike != null && context.Entry(rent.Bike).State == EntityState.Detached) + { + context.Attach(rent.Bike); + + if (rent.Bike.Model != null && context.Entry(rent.Bike.Model).State == EntityState.Detached) + { + context.Attach(rent.Bike.Model); + } + } + + if (rent.Renter != null && context.Entry(rent.Renter).State == EntityState.Detached) + { + context.Attach(rent.Renter); + } + context.Rents.Add(rent); context.SaveChanges(); } diff --git a/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs b/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs index cbe5ae199..fa13da0e6 100644 --- a/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs +++ b/Bikes.Infrastructure.Kafka/Deserializers/ValueDeserializer.cs @@ -10,14 +10,14 @@ public class ValueDeserializer : IDeserializer> public IList Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) { if (isNull || data.Length == 0) - return new List(); + return []; var json = Encoding.UTF8.GetString(data); try { var result = JsonSerializer.Deserialize>(json); - return result ?? new List(); + return result ?? []; } catch (JsonException ex) { diff --git a/Bikes.Infrastructure.Kafka/KafkaConsumer.cs b/Bikes.Infrastructure.Kafka/KafkaConsumer.cs index 51d1e9504..854a22828 100644 --- a/Bikes.Infrastructure.Kafka/KafkaConsumer.cs +++ b/Bikes.Infrastructure.Kafka/KafkaConsumer.cs @@ -138,15 +138,15 @@ private async Task WaitForKafkaAvailableAsync(CancellationToken stoppingToken) { _logger.LogInformation("Waiting for Kafka to become available..."); + using var adminClient = new AdminClientBuilder(new AdminClientConfig + { + BootstrapServers = _bootstrapServers + }).Build(); + for (var i = 0; i < 30; i++) { try { - using var adminClient = new AdminClientBuilder(new AdminClientConfig - { - BootstrapServers = _bootstrapServers - }).Build(); - var metadata = adminClient.GetMetadata(TimeSpan.FromSeconds(5)); _logger.LogInformation("Kafka is available. Brokers: {BrokerCount}", metadata.Brokers.Count); return; @@ -198,5 +198,6 @@ public override void Dispose() } base.Dispose(); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/Bikes.Infrastructure.Kafka/Worker.cs b/Bikes.Infrastructure.Kafka/Worker.cs deleted file mode 100644 index fd07726c8..000000000 --- a/Bikes.Infrastructure.Kafka/Worker.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Bikes.Infrastructure.Kafka; - -public class Worker : BackgroundService -{ - private readonly ILogger _logger; - - public Worker(ILogger logger) - { - _logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - } - await Task.Delay(1000, stoppingToken); - } - } -}