From 88412c34528e53c3e436618ae9cbfbb9ae0cbe02 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 16 Oct 2025 05:11:43 +0400 Subject: [PATCH 01/36] class implementation --- AirlineApplication/Items/Flight.cs | 52 +++++++++++++++++++++++++ AirlineApplication/Items/ModelFamily.cs | 22 +++++++++++ AirlineApplication/Items/Passenger.cs | 28 +++++++++++++ AirlineApplication/Items/PlaneModel.cs | 37 ++++++++++++++++++ AirlineApplication/Items/Ticket.cs | 37 ++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 AirlineApplication/Items/Flight.cs create mode 100644 AirlineApplication/Items/ModelFamily.cs create mode 100644 AirlineApplication/Items/Passenger.cs create mode 100644 AirlineApplication/Items/PlaneModel.cs create mode 100644 AirlineApplication/Items/Ticket.cs diff --git a/AirlineApplication/Items/Flight.cs b/AirlineApplication/Items/Flight.cs new file mode 100644 index 000000000..34d682826 --- /dev/null +++ b/AirlineApplication/Items/Flight.cs @@ -0,0 +1,52 @@ +namespace AirlineApplication.Items + +/// +/// The class for flight description and information about it. +/// +public class Flight +{ + /// + /// Unique flight's ID. + /// + public required int ID { get; set; } + + /// + /// Flight's code. + /// + public required string FlightCode { get; set; } + + /// + /// The place of departure. + /// + public required string DepartureCity { get; set; } + + /// + /// The place of arrival. + /// + public required string ArrivalCity { get; set; } + + /// + /// Date of the departure. + /// + public DateTime? DepartureDate { get; set; } + + /// + /// Date of the arrival. + /// + public DateTime? ArrivalDate { get; set; } + + /// + /// Flight's eparture time. + /// + public TimeSpan? DepartureTime { get; set; }; + + /// + /// Flight's travel time. + /// + public TimeSpan? TravelTime { get; set; }; + + /// + /// The type of plane for the flight. + /// + public required PlaneModel Model { get; set; } +} diff --git a/AirlineApplication/Items/ModelFamily.cs b/AirlineApplication/Items/ModelFamily.cs new file mode 100644 index 000000000..98c0521df --- /dev/null +++ b/AirlineApplication/Items/ModelFamily.cs @@ -0,0 +1,22 @@ +namespace AirlineApplication.Items + +/// +/// The class for information about a model family. +/// +public class ModelFamily +{ + /// + /// Unique model's ID. + /// + public required int ID { get; set; } + + /// + /// The name of model's family. + /// + public required string NameOfFamily { get; set; } + + /// + /// The name of the model manufacturer. + /// + public required string ManufacturerName { get; set; } +} diff --git a/AirlineApplication/Items/Passenger.cs b/AirlineApplication/Items/Passenger.cs new file mode 100644 index 000000000..fa9bc28ed --- /dev/null +++ b/AirlineApplication/Items/Passenger.cs @@ -0,0 +1,28 @@ +namespace AirlineApplication.Items + +/// +/// The class for describing a passenger. +/// +public class Passenger +{ + /// + /// Unique passenger's ID. + /// + public required int ID { get; set; } + + /// + /// The number of passenger's pasport. + /// + public required string Passport { get; set; } + + /// + /// Passenger's full name. + /// + public required string PassengerName { get; set; } + + /// + /// Passenger's date of birth. + /// + public required string DateOfBirth { get; set; } + +} diff --git a/AirlineApplication/Items/PlaneModel.cs b/AirlineApplication/Items/PlaneModel.cs new file mode 100644 index 000000000..b7d8e940b --- /dev/null +++ b/AirlineApplication/Items/PlaneModel.cs @@ -0,0 +1,37 @@ +namespace AirlineApplication.Items + +/// +/// The class for information about a plane model. +/// +public class PlaneModel +{ + /// + /// Unique plane model's ID. + /// + public required int ID { get; set; } + + /// + /// The name of plane model. + /// + public required string ModelName { get; set; } + + /// + /// The model family of the plane. + /// + public required ModelFamily PlaneFamily { get; set; } + + /// + /// The max flight range of the plane model. + /// + public required double MaxRange { get; set; } + + /// + /// The passenger capacity of the plane model (tons). + /// + public required double PassengerCapacity { get; set; } + + /// + /// The cargo capacity of the plane model (tons). + /// + public required double CargoCapacity { get; set; } +} diff --git a/AirlineApplication/Items/Ticket.cs b/AirlineApplication/Items/Ticket.cs new file mode 100644 index 000000000..13e8d7871 --- /dev/null +++ b/AirlineApplication/Items/Ticket.cs @@ -0,0 +1,37 @@ +namespace AirlineApplication.Items + +/// +/// The class for information about ticket. +/// +public class Ticket +{ + /// + /// Unique ticket's ID. + /// + public int ID { get; set; } + + /// + /// The connection between the ticket and the flight. + /// + public required Flight Flight { get; set; } + + /// + /// The connection between the ticket and the passenger. + /// + public required Passenger Passenger{ get; set; } + + /// + /// The passenger's seat number. + /// + public required string SeatNumber { get; set; } + + /// + /// The flag to indicate if there is a hand luggage. + /// + public required bool HandLuggage { get; set; } + + /// + /// Total baggage weight. (kilograms) + /// + public double? BaggageWeight { get; set; } +} From 81ec9d4c1206c9d58a42ee35c094af4fc639f7a5 Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 24 Oct 2025 02:44:40 +0400 Subject: [PATCH 02/36] adding tests --- Airline.Domain/Airline.Domain.csproj | 9 + Airline.Domain/DataSeed/DataSeed.cs | 488 +++++++++++++++++++++++++++ Airline.Domain/Items/Flight.cs | 59 ++++ Airline.Domain/Items/ModelFamily.cs | 30 ++ Airline.Domain/Items/Passengers.cs | 35 ++ Airline.Domain/Items/PlaneModel.cs | 46 +++ Airline.Domain/Items/Ticket.cs | 46 +++ 7 files changed, 713 insertions(+) create mode 100644 Airline.Domain/Airline.Domain.csproj create mode 100644 Airline.Domain/DataSeed/DataSeed.cs create mode 100644 Airline.Domain/Items/Flight.cs create mode 100644 Airline.Domain/Items/ModelFamily.cs create mode 100644 Airline.Domain/Items/Passengers.cs create mode 100644 Airline.Domain/Items/PlaneModel.cs create mode 100644 Airline.Domain/Items/Ticket.cs diff --git a/Airline.Domain/Airline.Domain.csproj b/Airline.Domain/Airline.Domain.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Airline.Domain/Airline.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Airline.Domain/DataSeed/DataSeed.cs b/Airline.Domain/DataSeed/DataSeed.cs new file mode 100644 index 000000000..d874d7937 --- /dev/null +++ b/Airline.Domain/DataSeed/DataSeed.cs @@ -0,0 +1,488 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Airline.Domain.Items; +namespace Airline.Domain.DataSeed; + +/// +/// Seeds test data for the airline system, including model families, plane models, passengers, flights, and tickets. +/// +public class DataSeed +{ + public List ModelFamilies { get; } + public List PlaneModels { get; } + public List Passengers { get; } + public List Flights { get; } + public List Tickets { get; } + + public DataSeed() + { + ModelFamilies = InitModelFamilies(); + PlaneModels = InitPlaneModels(ModelFamilies); + Passengers = InitPassengers(); + Flights = InitFlights(PlaneModels); + Tickets = InitTickets(Flights, Passengers); + } + + /// + /// Initializes the model families with predefined data. + /// + /// List of objects. + private static List InitModelFamilies() => new() + { + new() + { + ID = 1, + NameOfFamily = "A320 Family", + ManufacturerName = "Airbus" + }, + new() + { + ID = 2, + NameOfFamily = "737 Family", + ManufacturerName = "Boeing" + }, + new() + { + ID = 3, + NameOfFamily = "777 Family", + ManufacturerName = "Boeing" + }, + new() + { + ID = 4, + NameOfFamily = "787 Dreamliner", + ManufacturerName = "Boeing" + }, + new() + { + ID = 5, + NameOfFamily = "A330 Family", + ManufacturerName = "Airbus" + } + }; + + /// + /// Initializes plane models linked to their families. + /// + /// List of model families. + /// List of objects. + private static List InitPlaneModels(List families) => new() + { + new() + { + ID = 1, + ModelName = "A320", + PlaneFamily = families[0], + MaxRange = 6000, + PassengerCapacity = 180, + CargoCapacity = 20 + }, + new() + { + ID = 2, + ModelName = "B737-800", + PlaneFamily = families[1], + MaxRange = 5500, + PassengerCapacity = 189, + CargoCapacity = 23 + }, + new() + { + ID = 3, + ModelName = "B777-300ER", + PlaneFamily = families[2], + MaxRange = 11000, + PassengerCapacity = 370, + CargoCapacity = 45 + }, + new() + { + ID = 4, + ModelName = "B787-9", + PlaneFamily = families[3], + MaxRange = 12000, + PassengerCapacity = 290, + CargoCapacity = 40 + }, + new() + { + ID = 5, + ModelName = "A330-300", + PlaneFamily = families[4], + MaxRange = 10500, + PassengerCapacity = 300, + CargoCapacity = 42 + } + }; + + /// + /// Initializes a list of passengers. + /// + /// List of objects. + private static List InitPassengers() => new() + { + new() + { + ID = 1, + Passport = "477419070", + PassengerName = "Ivanov Ivan", + DateOfBirth = "1990-01-15" + }, + new() + { + ID = 2, + Passport = "719011722", + PassengerName = "Petrov Petr", + DateOfBirth = "1985-05-22" + }, + new() + { + ID = 3, + Passport = "269997862", + PassengerName = "Alyohin Alexey", + DateOfBirth = "1992-03-10" + }, + new() + { + ID = 4, + Passport = "690256588", + PassengerName = "Kuzina Anna", + DateOfBirth = "1991-07-30" + }, + new() + { + ID = 5, + Passport = "816817823", + PassengerName = "Kuzin Dmitry", + DateOfBirth = "1988-11-05" + }, + new() + { + ID = 6, + Passport = "303776467", + PassengerName = "Nikitich Dobrynya", + DateOfBirth = "1995-09-18" + }, + new() + { + ID = 7, + Passport = "510907182", + PassengerName = "Popovich Alex", + DateOfBirth = "1993-04-12" + }, + new() + { + ID = 8, + Passport = "463835340", + PassengerName = "Kolyan", + DateOfBirth = "1987-08-25" + }, + new() + { + ID = 9, + Passport = "877654233", + PassengerName = "Lebedev Nikolay Ivanovich", + DateOfBirth = "1960-02-14" + }, + new() + { + ID = 10, + Passport = "112971133", + PassengerName = "Sokolov Tigran", + DateOfBirth = "1994-12-03" + } + }; + + /// + /// Initializes flights with plane models and schedules. + /// + /// List of plane models. + /// List of objects. + private static List InitFlights(List models) => new() + { + // Moscow → Berlin (2h) — A320 + new() + { + ID = 1, + FlightCode = "SU101", + DepartureCity = "Moscow", + ArrivalCity = "Berlin", + DepartureDate = new(2025, 10, 10), + ArrivalDate = new(2025, 10, 10), + DepartureTime = new(8, 0, 0), + TravelTime = TimeSpan.FromHours(2), + Model = models[0] + }, + + // Moscow → Paris (3.5h) — B737-800 + new() + { + ID = 2, + FlightCode = "SU102", + DepartureCity = "Moscow", + ArrivalCity = "Paris", + DepartureDate = new(2025, 10, 10), + ArrivalDate = new(2025, 10, 10), + DepartureTime = new(9, 0, 0), + TravelTime = TimeSpan.FromHours(3.5), + Model = models[1] + }, + + // Berlin → Paris (1.5h) — B777-300ER + new() + { + ID = 3, + FlightCode = "SU103", + DepartureCity = "Berlin", + ArrivalCity = "Paris", + DepartureDate = new(2025, 10, 10), + ArrivalDate = new(2025, 10, 10), + DepartureTime = new(11, 0, 0), + TravelTime = TimeSpan.FromHours(1.5), + Model = models[2] + }, + + // Moscow → Berlin (2h) — B787-9 + new() + { + ID = 4, + FlightCode = "SU104", + DepartureCity = "Moscow", + ArrivalCity = "Berlin", + DepartureDate = new(2025, 10, 11), + ArrivalDate = new(2025, 10, 11), + DepartureTime = new(14, 0, 0), + TravelTime = TimeSpan.FromHours(2), + Model = models[3] + }, + + // Rome → Milan (1h) — A330-300 + new() + { + ID = 5, + FlightCode = "AZ201", + DepartureCity = "Rome", + ArrivalCity = "Milan", + DepartureDate = new(2025, 10, 11), + ArrivalDate = new(2025, 10, 11), + DepartureTime = new(7, 0, 0), + TravelTime = TimeSpan.FromHours(1), + Model = models[4] + }, + + // Moscow → Tokyo (10h) — A320 + new() + { + ID = 6, + FlightCode = "SU200", + DepartureCity = "Moscow", + ArrivalCity = "Tokyo", + DepartureDate = new(2025, 10, 12), + ArrivalDate = new(2025, 10, 12), + DepartureTime = new(1, 0, 0), + TravelTime = TimeSpan.FromHours(10), + Model = models[0] + }, + + // New York → London (6h) — B737-800 + new() + { + ID = 7, + FlightCode = "DL100", + DepartureCity = "New York", + ArrivalCity = "London", + DepartureDate = new(2025, 10, 12), + ArrivalDate = new(2025, 10, 13), + DepartureTime = new(18, 0, 0), + TravelTime = TimeSpan.FromHours(6), + Model = models[1] + }, + + // Paris → Moscow (3h) — A320 + new() + { + ID = 8, + FlightCode = "SU105", + DepartureCity = "Paris", + ArrivalCity = "Moscow", + DepartureDate = new(2025, 10, 13), + ArrivalDate = new(2025, 10, 13), + DepartureTime = new(13, 0, 0), + TravelTime = TimeSpan.FromHours(3), + Model = models[0] + } + }; + + /// + /// Initializes tickets linking flights to passengers. + /// + /// List of flights. + /// List of passengers. + /// List of objects. + private static List InitTickets(List flights, List passengers) => new() + { + // SU101 (Moscow → Berlin) — 5 пассажиров, 2 без багажа + new() + { + ID = 1, + Flight = flights[0], + Passenger = passengers[0], + SeatNumber = "12A", + HandLuggage = true, + BaggageWeight = 20.0 + }, + new() + { + ID = 2, + Flight = flights[0], + Passenger = passengers[1], + SeatNumber = "12B", + HandLuggage = false, + BaggageWeight = null + }, + new() + { + ID = 3, + Flight = flights[0], + Passenger = passengers[2], + SeatNumber = "12C", + HandLuggage = true, + BaggageWeight = null + }, + new() + { + ID = 4, + Flight = flights[0], + Passenger = passengers[3], + SeatNumber = "13A", + HandLuggage = true, + BaggageWeight = 15.0 + }, + new() + { + ID = 5, + Flight = flights[0], + Passenger = passengers[4], + SeatNumber = "13B", + HandLuggage = true, + BaggageWeight = 10.0 + }, + + // SU102 (Moscow → Paris) — 3 пассажира, 1 без багажа + new() + { + ID = 6, + Flight = flights[1], + Passenger = passengers[5], + SeatNumber = "15A", + HandLuggage = true, + BaggageWeight = 12.0 + }, + new() + { + ID = 7, + Flight = flights[1], + Passenger = passengers[6], + SeatNumber = "15B", + HandLuggage = true, + BaggageWeight = 8.0 + }, + new() + { + ID = 8, + Flight = flights[1], + Passenger = passengers[7], + SeatNumber = "15C", + HandLuggage = false, + BaggageWeight = null + }, + + // SU103 (Berlin → Paris) — 2 пассажира + new() + { + ID = 9, + Flight = flights[2], + Passenger = passengers[8], + SeatNumber = "20A", + HandLuggage = true, + BaggageWeight = 5.0 + }, + new() + { + ID = 10, + Flight = flights[2], + Passenger = passengers[9], + SeatNumber = "20B", + HandLuggage = true, + BaggageWeight = 7.0 + }, + + // SU104 (Moscow → Berlin) — 1 пассажир без багажа + new() + { + ID = 11, + Flight = flights[3], + Passenger = passengers[0], + SeatNumber = "10A", + HandLuggage = false, + BaggageWeight = null + }, + + // AZ201 (Rome → Milan) — 1 пассажир + new() + { + ID = 12, + Flight = flights[4], + Passenger = passengers[1], + SeatNumber = "5A", + HandLuggage = true, + BaggageWeight = 6.0 + }, + + // SU200 (Moscow → Tokyo) — 1 пассажир + new() + { + ID = 13, + Flight = flights[5], + Passenger = passengers[2], + SeatNumber = "1A", + HandLuggage = true, + BaggageWeight = 25.0 + }, + + // DL100 (New York → London) — 1 пассажир без багажа + new() + { + ID = 14, + Flight = flights[6], + Passenger = passengers[3], + SeatNumber = "8A", + HandLuggage = false, + BaggageWeight = null + }, + + // SU105 (Paris → Moscow) — 2 пассажира без багажа + new() + { + ID = 15, + Flight = flights[7], + Passenger = passengers[4], + SeatNumber = "7A", + HandLuggage = true, + BaggageWeight = null + }, + new() + { + ID = 16, + Flight = flights[7], + Passenger = passengers[5], + SeatNumber = "7B", + HandLuggage = false, + BaggageWeight = null + } + }; +} \ No newline at end of file diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs new file mode 100644 index 000000000..6ef0b05cf --- /dev/null +++ b/Airline.Domain/Items/Flight.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Airline.Domain.Items +{ + /// + /// The class for flight description and information about it. + /// + public class Flight + { + /// + /// Unique flight's ID. + /// + public required int ID { get; set; } + + /// + /// Flight's code. + /// + public required string FlightCode { get; set; } + + /// + /// The place of departure. + /// + public required string DepartureCity { get; set; } + + /// + /// The place of arrival. + /// + public required string ArrivalCity { get; set; } + + /// + /// Date of the departure. + /// + public DateOnly? DepartureDate { get; set; } + + /// + /// Date of the arrival. + /// + public DateOnly? ArrivalDate { get; set; } + + /// + /// Flight's eparture time. + /// + public TimeSpan? DepartureTime { get; set; } + + /// + /// Flight's travel time. + /// + public TimeSpan? TravelTime { get; set; } + + /// + /// The type of plane for the flight. + /// + public required PlaneModel Model { get; set; } + } +} \ No newline at end of file diff --git a/Airline.Domain/Items/ModelFamily.cs b/Airline.Domain/Items/ModelFamily.cs new file mode 100644 index 000000000..25652a0cc --- /dev/null +++ b/Airline.Domain/Items/ModelFamily.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Airline.Domain.Items +{ + /// + /// The class for information about a model family. + /// + public class ModelFamily + { + /// + /// Unique model's ID. + /// + public required int ID { get; set; } + + /// + /// The name of model's family. + /// + public required string NameOfFamily { get; set; } + + /// + /// The name of the model manufacturer. + /// + public required string ManufacturerName { get; set; } + } + +} diff --git a/Airline.Domain/Items/Passengers.cs b/Airline.Domain/Items/Passengers.cs new file mode 100644 index 000000000..bfc255a00 --- /dev/null +++ b/Airline.Domain/Items/Passengers.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Airline.Domain.Items +{ + /// + /// The class for describing a passenger. + /// + public class Passenger + { + /// + /// Unique passenger's ID. + /// + public required int ID { get; set; } + + /// + /// The number of passenger's pasport. + /// + public required string Passport { get; set; } + + /// + /// Passenger's full name. + /// + public required string PassengerName { get; set; } + + /// + /// Passenger's date of birth. + /// + public required string DateOfBirth { get; set; } + + } +} diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs new file mode 100644 index 000000000..e0e158878 --- /dev/null +++ b/Airline.Domain/Items/PlaneModel.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Airline.Domain.Items +{ + + /// + /// The class for information about a plane model. + /// + public class PlaneModel + { + /// + /// Unique plane model's ID. + /// + public required int ID { get; set; } + + /// + /// The name of plane model. + /// + public required string ModelName { get; set; } + + /// + /// The model family of the plane. + /// + public required ModelFamily PlaneFamily { get; set; } + + /// + /// The max flight range of the plane model. + /// + public required double MaxRange { get; set; } + + /// + /// The passenger capacity of the plane model (tons). + /// + public required double PassengerCapacity { get; set; } + + /// + /// The cargo capacity of the plane model (tons). + /// + public required double CargoCapacity { get; set; } + } + +} diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs new file mode 100644 index 000000000..aec3a69c5 --- /dev/null +++ b/Airline.Domain/Items/Ticket.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Airline.Domain.Items +{ + + /// + /// The class for information about ticket. + /// + public class Ticket + { + /// + /// Unique ticket's ID. + /// + public int ID { get; set; } + + /// + /// The connection between the ticket and the flight. + /// + public required Flight Flight { get; set; } + + /// + /// The connection between the ticket and the passenger. + /// + public required Passenger Passenger { get; set; } + + /// + /// The passenger's seat number. + /// + public required string SeatNumber { get; set; } + + /// + /// The flag to indicate if there is a hand luggage. + /// + public required bool HandLuggage { get; set; } + + /// + /// Total baggage weight. (kilograms) + /// + public double? BaggageWeight { get; set; } + } + +} From 8c5ddbbebc5f1d513f6baa805a4b765d5c20062d Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 24 Oct 2025 02:49:00 +0400 Subject: [PATCH 03/36] adding tests second take --- Airline.Tests/Airline.Tests.cs | 85 +++++++++++++++++++++++++ Airline.Tests/Airline.Tests.csproj | 23 +++++++ Airline.sln | 31 +++++++++ AirlineApplication/Items/Flight.cs | 52 --------------- AirlineApplication/Items/ModelFamily.cs | 22 ------- AirlineApplication/Items/Passenger.cs | 28 -------- AirlineApplication/Items/PlaneModel.cs | 37 ----------- AirlineApplication/Items/Ticket.cs | 37 ----------- 8 files changed, 139 insertions(+), 176 deletions(-) create mode 100644 Airline.Tests/Airline.Tests.cs create mode 100644 Airline.Tests/Airline.Tests.csproj create mode 100644 Airline.sln delete mode 100644 AirlineApplication/Items/Flight.cs delete mode 100644 AirlineApplication/Items/ModelFamily.cs delete mode 100644 AirlineApplication/Items/Passenger.cs delete mode 100644 AirlineApplication/Items/PlaneModel.cs delete mode 100644 AirlineApplication/Items/Ticket.cs diff --git a/Airline.Tests/Airline.Tests.cs b/Airline.Tests/Airline.Tests.cs new file mode 100644 index 000000000..4d8d069e7 --- /dev/null +++ b/Airline.Tests/Airline.Tests.cs @@ -0,0 +1,85 @@ +using Airline.Domain.DataSeed; +using Xunit; + +namespace Airline.Tests; + +public class AirCompanyTests(DataSeed _seed) : IClassFixture +{ + [Fact] + public void GetTop5FlightsByPassengerCount_ReturnsCorrectFlights() + { + var flightPassengerCounts = _seed.Tickets + .GroupBy(t => t.Flight) + .Select(g => new { Flight = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .Select(x => x.Flight) + .ToList(); + + Assert.Equal(5, flightPassengerCounts.Count); + Assert.Equal("SU101", flightPassengerCounts[0].FlightCode); + } + + [Fact] + public void GetFlightsWithMinTravelTime_ReturnsCorrectFlights() + { + var validFlights = _seed.Flights.Where(f => f.TravelTime.HasValue).ToList(); + var minTime = validFlights.Min(f => f.TravelTime!.Value); + var result = validFlights + .Where(f => f.TravelTime == minTime) + .ToList(); + + Assert.Single(result); + Assert.Equal("AZ201", result[0].FlightCode); + Assert.Equal(TimeSpan.FromHours(1), result[0].TravelTime); + } + + [Fact] + public void GetPassengersWithZeroBaggageOnFlight_ReturnsSortedPassengers() + { + var flight = _seed.Flights.First(f => f.FlightCode == "SU101"); + var passengersWithNoBaggage = _seed.Tickets + .Where(t => t.Flight == flight && t.BaggageWeight == null) + .Select(t => t.Passenger) + .OrderBy(p => p.PassengerName) + .ToList(); + + Assert.Equal(2, passengersWithNoBaggage.Count); + Assert.Equal("Alyohin Alexey", passengersWithNoBaggage[0].PassengerName); + Assert.Equal("Petrov Petr", passengersWithNoBaggage[1].PassengerName); + } + + [Fact] + public void GetFlightsByModelInPeriod_ReturnsCorrectFlights() + { + var modelName = "A320"; + var from = new DateOnly(2025, 10, 10); + var to = new DateOnly(2025, 10, 12); + + var result = _seed.Flights + .Where(f => f.Model.ModelName == modelName && + f.DepartureDate >= from && + f.DepartureDate <= to) + .ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, f => f.FlightCode == "SU101"); + Assert.Contains(result, f => f.FlightCode == "SU200"); + Assert.DoesNotContain(result, f => f.FlightCode == "SU105"); + } + + [Fact] + public void GetFlightsByRoute_ReturnsCorrectFlights() + { + var departure = "Moscow"; + var arrival = "Berlin"; + + var result = _seed.Flights + .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) + .ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, f => f.FlightCode == "SU101"); + Assert.Contains(result, f => f.FlightCode == "SU104"); + } +} \ No newline at end of file diff --git a/Airline.Tests/Airline.Tests.csproj b/Airline.Tests/Airline.Tests.csproj new file mode 100644 index 000000000..12709c181 --- /dev/null +++ b/Airline.Tests/Airline.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Airline.sln b/Airline.sln new file mode 100644 index 000000000..3e6aac49f --- /dev/null +++ b/Airline.sln @@ -0,0 +1,31 @@ + +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}") = "Airline.Domain", "Airline.Domain\Airline.Domain.csproj", "{38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Tests", "Airline.Tests\Airline.Tests.csproj", "{B813F501-EA93-4258-A2CA-A43542C8EEF4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Release|Any CPU.Build.0 = Release|Any CPU + {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8A363FDF-5B26-493B-876E-8807EF719DEA} + EndGlobalSection +EndGlobal diff --git a/AirlineApplication/Items/Flight.cs b/AirlineApplication/Items/Flight.cs deleted file mode 100644 index 34d682826..000000000 --- a/AirlineApplication/Items/Flight.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace AirlineApplication.Items - -/// -/// The class for flight description and information about it. -/// -public class Flight -{ - /// - /// Unique flight's ID. - /// - public required int ID { get; set; } - - /// - /// Flight's code. - /// - public required string FlightCode { get; set; } - - /// - /// The place of departure. - /// - public required string DepartureCity { get; set; } - - /// - /// The place of arrival. - /// - public required string ArrivalCity { get; set; } - - /// - /// Date of the departure. - /// - public DateTime? DepartureDate { get; set; } - - /// - /// Date of the arrival. - /// - public DateTime? ArrivalDate { get; set; } - - /// - /// Flight's eparture time. - /// - public TimeSpan? DepartureTime { get; set; }; - - /// - /// Flight's travel time. - /// - public TimeSpan? TravelTime { get; set; }; - - /// - /// The type of plane for the flight. - /// - public required PlaneModel Model { get; set; } -} diff --git a/AirlineApplication/Items/ModelFamily.cs b/AirlineApplication/Items/ModelFamily.cs deleted file mode 100644 index 98c0521df..000000000 --- a/AirlineApplication/Items/ModelFamily.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace AirlineApplication.Items - -/// -/// The class for information about a model family. -/// -public class ModelFamily -{ - /// - /// Unique model's ID. - /// - public required int ID { get; set; } - - /// - /// The name of model's family. - /// - public required string NameOfFamily { get; set; } - - /// - /// The name of the model manufacturer. - /// - public required string ManufacturerName { get; set; } -} diff --git a/AirlineApplication/Items/Passenger.cs b/AirlineApplication/Items/Passenger.cs deleted file mode 100644 index fa9bc28ed..000000000 --- a/AirlineApplication/Items/Passenger.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace AirlineApplication.Items - -/// -/// The class for describing a passenger. -/// -public class Passenger -{ - /// - /// Unique passenger's ID. - /// - public required int ID { get; set; } - - /// - /// The number of passenger's pasport. - /// - public required string Passport { get; set; } - - /// - /// Passenger's full name. - /// - public required string PassengerName { get; set; } - - /// - /// Passenger's date of birth. - /// - public required string DateOfBirth { get; set; } - -} diff --git a/AirlineApplication/Items/PlaneModel.cs b/AirlineApplication/Items/PlaneModel.cs deleted file mode 100644 index b7d8e940b..000000000 --- a/AirlineApplication/Items/PlaneModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace AirlineApplication.Items - -/// -/// The class for information about a plane model. -/// -public class PlaneModel -{ - /// - /// Unique plane model's ID. - /// - public required int ID { get; set; } - - /// - /// The name of plane model. - /// - public required string ModelName { get; set; } - - /// - /// The model family of the plane. - /// - public required ModelFamily PlaneFamily { get; set; } - - /// - /// The max flight range of the plane model. - /// - public required double MaxRange { get; set; } - - /// - /// The passenger capacity of the plane model (tons). - /// - public required double PassengerCapacity { get; set; } - - /// - /// The cargo capacity of the plane model (tons). - /// - public required double CargoCapacity { get; set; } -} diff --git a/AirlineApplication/Items/Ticket.cs b/AirlineApplication/Items/Ticket.cs deleted file mode 100644 index 13e8d7871..000000000 --- a/AirlineApplication/Items/Ticket.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace AirlineApplication.Items - -/// -/// The class for information about ticket. -/// -public class Ticket -{ - /// - /// Unique ticket's ID. - /// - public int ID { get; set; } - - /// - /// The connection between the ticket and the flight. - /// - public required Flight Flight { get; set; } - - /// - /// The connection between the ticket and the passenger. - /// - public required Passenger Passenger{ get; set; } - - /// - /// The passenger's seat number. - /// - public required string SeatNumber { get; set; } - - /// - /// The flag to indicate if there is a hand luggage. - /// - public required bool HandLuggage { get; set; } - - /// - /// Total baggage weight. (kilograms) - /// - public double? BaggageWeight { get; set; } -} From 329eeae533458edfef09e71e8a431990dfd4ebeb Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 24 Oct 2025 04:05:29 +0400 Subject: [PATCH 04/36] cosmetic edits --- Airline.Tests/Airline.Tests.cs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Airline.Tests/Airline.Tests.cs b/Airline.Tests/Airline.Tests.cs index 4d8d069e7..275259fa5 100644 --- a/Airline.Tests/Airline.Tests.cs +++ b/Airline.Tests/Airline.Tests.cs @@ -5,6 +5,9 @@ namespace Airline.Tests; public class AirCompanyTests(DataSeed _seed) : IClassFixture { + /// + /// Verifies that the method returns the top 5 flights based on the number of passengers transported. + /// [Fact] public void GetTop5FlightsByPassengerCount_ReturnsCorrectFlights() { @@ -20,6 +23,9 @@ public void GetTop5FlightsByPassengerCount_ReturnsCorrectFlights() Assert.Equal("SU101", flightPassengerCounts[0].FlightCode); } + /// + /// Verifies that the method correctly finds flights with minimal travel time. + /// [Fact] public void GetFlightsWithMinTravelTime_ReturnsCorrectFlights() { @@ -30,12 +36,15 @@ public void GetFlightsWithMinTravelTime_ReturnsCorrectFlights() .ToList(); Assert.Single(result); - Assert.Equal("AZ201", result[0].FlightCode); - Assert.Equal(TimeSpan.FromHours(1), result[0].TravelTime); + Assert.Equal(TimeSpan.FromHours(2), result[0].TravelTime); } + /// + /// Checks that the method returns a list of passengers on the selected flight without baggage, + /// sorted by full name (PassengerName) in alphabetical order. + /// [Fact] - public void GetPassengersWithZeroBaggageOnFlight_ReturnsSortedPassengers() + public void GetPassengersWithZeroBaggageOnsFlight_ReturnsSortedPassengers() { var flight = _seed.Flights.First(f => f.FlightCode == "SU101"); var passengersWithNoBaggage = _seed.Tickets @@ -49,6 +58,10 @@ public void GetPassengersWithZeroBaggageOnFlight_ReturnsSortedPassengers() Assert.Equal("Petrov Petr", passengersWithNoBaggage[1].PassengerName); } + /// + /// Verifies that the method returns all flights of the specified aircraft model + /// that departed during the specified date period. + /// [Fact] public void GetFlightsByModelInPeriod_ReturnsCorrectFlights() { @@ -65,14 +78,17 @@ public void GetFlightsByModelInPeriod_ReturnsCorrectFlights() Assert.Equal(2, result.Count); Assert.Contains(result, f => f.FlightCode == "SU101"); Assert.Contains(result, f => f.FlightCode == "SU200"); - Assert.DoesNotContain(result, f => f.FlightCode == "SU105"); } + /// + /// Verifies that the method returns all flights from the specified departure point + /// to the specified arrival point. + /// [Fact] public void GetFlightsByRoute_ReturnsCorrectFlights() { - var departure = "Moscow"; - var arrival = "Berlin"; + var departure = "Samara"; + var arrival = "Wonderland"; var result = _seed.Flights .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) @@ -82,4 +98,4 @@ public void GetFlightsByRoute_ReturnsCorrectFlights() Assert.Contains(result, f => f.FlightCode == "SU101"); Assert.Contains(result, f => f.FlightCode == "SU104"); } -} \ No newline at end of file +} From ced06493ecc9c45981386eadc3f1521ee9efbe08 Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 24 Oct 2025 04:09:20 +0400 Subject: [PATCH 05/36] some more cosmetic edits --- Airline.Domain/DataSeed/DataSeed.cs | 92 +++++++++++------------------ Airline.Domain/Items/Flight.cs | 6 +- Airline.Domain/Items/ModelFamily.cs | 2 +- Airline.Domain/Items/Passengers.cs | 4 +- Airline.Domain/Items/PlaneModel.cs | 2 +- 5 files changed, 42 insertions(+), 64 deletions(-) diff --git a/Airline.Domain/DataSeed/DataSeed.cs b/Airline.Domain/DataSeed/DataSeed.cs index d874d7937..676f32674 100644 --- a/Airline.Domain/DataSeed/DataSeed.cs +++ b/Airline.Domain/DataSeed/DataSeed.cs @@ -8,7 +8,8 @@ namespace Airline.Domain.DataSeed; /// -/// Seeds test data for the airline system, including model families, plane models, passengers, flights, and tickets. +/// Seeds test data for the airline system, including model families, +/// plane models, passengers, flights, and tickets. /// public class DataSeed { @@ -30,7 +31,6 @@ public DataSeed() /// /// Initializes the model families with predefined data. /// - /// List of objects. private static List InitModelFamilies() => new() { new() @@ -42,7 +42,7 @@ public DataSeed() new() { ID = 2, - NameOfFamily = "737 Family", + NameOfFamily = "767 Family", ManufacturerName = "Boeing" }, new() @@ -68,8 +68,6 @@ public DataSeed() /// /// Initializes plane models linked to their families. /// - /// List of model families. - /// List of objects. private static List InitPlaneModels(List families) => new() { new() @@ -84,7 +82,7 @@ public DataSeed() new() { ID = 2, - ModelName = "B737-800", + ModelName = "B767-300", PlaneFamily = families[1], MaxRange = 5500, PassengerCapacity = 189, @@ -122,7 +120,6 @@ public DataSeed() /// /// Initializes a list of passengers. /// - /// List of objects. private static List InitPassengers() => new() { new() @@ -130,87 +127,85 @@ public DataSeed() ID = 1, Passport = "477419070", PassengerName = "Ivanov Ivan", - DateOfBirth = "1990-01-15" + DateOfBirth = new(1990, 01, 15) }, new() { ID = 2, Passport = "719011722", PassengerName = "Petrov Petr", - DateOfBirth = "1985-05-22" + DateOfBirth = new(1985, 05, 22) }, new() { ID = 3, Passport = "269997862", PassengerName = "Alyohin Alexey", - DateOfBirth = "1992-03-10" + DateOfBirth = new(1992, 03, 10) }, new() { ID = 4, Passport = "690256588", PassengerName = "Kuzina Anna", - DateOfBirth = "1991-07-30" + DateOfBirth = new(1991, 07, 30) }, new() { ID = 5, Passport = "816817823", PassengerName = "Kuzin Dmitry", - DateOfBirth = "1988-11-05" + DateOfBirth = new(1988, 11, 05) }, new() { ID = 6, Passport = "303776467", PassengerName = "Nikitich Dobrynya", - DateOfBirth = "1995-09-18" + DateOfBirth = new(1995, 09, 18) }, new() { ID = 7, Passport = "510907182", PassengerName = "Popovich Alex", - DateOfBirth = "1993-04-12" + DateOfBirth = new(1993, 04, 12) }, new() { ID = 8, Passport = "463835340", PassengerName = "Kolyan", - DateOfBirth = "1987-08-25" + DateOfBirth = new(1987, 08, 25) }, new() { ID = 9, Passport = "877654233", PassengerName = "Lebedev Nikolay Ivanovich", - DateOfBirth = "1960-02-14" + DateOfBirth = new(1960, 02, 14) }, new() { ID = 10, Passport = "112971133", PassengerName = "Sokolov Tigran", - DateOfBirth = "1994-12-03" + DateOfBirth = new(1994, 12, 03) } }; /// /// Initializes flights with plane models and schedules. /// - /// List of plane models. - /// List of objects. private static List InitFlights(List models) => new() { - // Moscow → Berlin (2h) — A320 + new() { ID = 1, FlightCode = "SU101", - DepartureCity = "Moscow", - ArrivalCity = "Berlin", + DepartureCity = "Samara", + ArrivalCity = "Wonderland", DepartureDate = new(2025, 10, 10), ArrivalDate = new(2025, 10, 10), DepartureTime = new(8, 0, 0), @@ -218,7 +213,6 @@ public DataSeed() Model = models[0] }, - // Moscow → Paris (3.5h) — B737-800 new() { ID = 2, @@ -228,11 +222,10 @@ public DataSeed() DepartureDate = new(2025, 10, 10), ArrivalDate = new(2025, 10, 10), DepartureTime = new(9, 0, 0), - TravelTime = TimeSpan.FromHours(3.5), + TravelTime = TimeSpan.FromHours(3), Model = models[1] }, - // Berlin → Paris (1.5h) — B777-300ER new() { ID = 3, @@ -242,25 +235,23 @@ public DataSeed() DepartureDate = new(2025, 10, 10), ArrivalDate = new(2025, 10, 10), DepartureTime = new(11, 0, 0), - TravelTime = TimeSpan.FromHours(1.5), + TravelTime = TimeSpan.FromHours(5), Model = models[2] }, - // Moscow → Berlin (2h) — B787-9 new() { ID = 4, FlightCode = "SU104", - DepartureCity = "Moscow", - ArrivalCity = "Berlin", + DepartureCity = "Samara", + ArrivalCity = "Wonderland", DepartureDate = new(2025, 10, 11), ArrivalDate = new(2025, 10, 11), DepartureTime = new(14, 0, 0), - TravelTime = TimeSpan.FromHours(2), + TravelTime = TimeSpan.FromHours(2.5), Model = models[3] }, - // Rome → Milan (1h) — A330-300 new() { ID = 5, @@ -270,11 +261,10 @@ public DataSeed() DepartureDate = new(2025, 10, 11), ArrivalDate = new(2025, 10, 11), DepartureTime = new(7, 0, 0), - TravelTime = TimeSpan.FromHours(1), + TravelTime = TimeSpan.FromHours(4.5), Model = models[4] }, - // Moscow → Tokyo (10h) — A320 new() { ID = 6, @@ -284,11 +274,10 @@ public DataSeed() DepartureDate = new(2025, 10, 12), ArrivalDate = new(2025, 10, 12), DepartureTime = new(1, 0, 0), - TravelTime = TimeSpan.FromHours(10), + TravelTime = TimeSpan.FromHours(15), Model = models[0] }, - // New York → London (6h) — B737-800 new() { ID = 7, @@ -302,7 +291,6 @@ public DataSeed() Model = models[1] }, - // Paris → Moscow (3h) — A320 new() { ID = 8, @@ -312,7 +300,7 @@ public DataSeed() DepartureDate = new(2025, 10, 13), ArrivalDate = new(2025, 10, 13), DepartureTime = new(13, 0, 0), - TravelTime = TimeSpan.FromHours(3), + TravelTime = TimeSpan.FromHours(7), Model = models[0] } }; @@ -320,12 +308,9 @@ public DataSeed() /// /// Initializes tickets linking flights to passengers. /// - /// List of flights. - /// List of passengers. - /// List of objects. private static List InitTickets(List flights, List passengers) => new() { - // SU101 (Moscow → Berlin) — 5 пассажиров, 2 без багажа + new() { ID = 1, @@ -333,7 +318,7 @@ public DataSeed() Passenger = passengers[0], SeatNumber = "12A", HandLuggage = true, - BaggageWeight = 20.0 + BaggageWeight = 15.6 }, new() { @@ -360,7 +345,7 @@ public DataSeed() Passenger = passengers[3], SeatNumber = "13A", HandLuggage = true, - BaggageWeight = 15.0 + BaggageWeight = 1.2 }, new() { @@ -372,7 +357,6 @@ public DataSeed() BaggageWeight = 10.0 }, - // SU102 (Moscow → Paris) — 3 пассажира, 1 без багажа new() { ID = 6, @@ -380,7 +364,7 @@ public DataSeed() Passenger = passengers[5], SeatNumber = "15A", HandLuggage = true, - BaggageWeight = 12.0 + BaggageWeight = 5.2 }, new() { @@ -389,7 +373,7 @@ public DataSeed() Passenger = passengers[6], SeatNumber = "15B", HandLuggage = true, - BaggageWeight = 8.0 + BaggageWeight = 18.0 }, new() { @@ -401,7 +385,6 @@ public DataSeed() BaggageWeight = null }, - // SU103 (Berlin → Paris) — 2 пассажира new() { ID = 9, @@ -409,7 +392,7 @@ public DataSeed() Passenger = passengers[8], SeatNumber = "20A", HandLuggage = true, - BaggageWeight = 5.0 + BaggageWeight = 3.2 }, new() { @@ -421,7 +404,6 @@ public DataSeed() BaggageWeight = 7.0 }, - // SU104 (Moscow → Berlin) — 1 пассажир без багажа new() { ID = 11, @@ -429,10 +411,9 @@ public DataSeed() Passenger = passengers[0], SeatNumber = "10A", HandLuggage = false, - BaggageWeight = null + BaggageWeight = 4.2 }, - // AZ201 (Rome → Milan) — 1 пассажир new() { ID = 12, @@ -443,7 +424,6 @@ public DataSeed() BaggageWeight = 6.0 }, - // SU200 (Moscow → Tokyo) — 1 пассажир new() { ID = 13, @@ -454,7 +434,6 @@ public DataSeed() BaggageWeight = 25.0 }, - // DL100 (New York → London) — 1 пассажир без багажа new() { ID = 14, @@ -465,7 +444,6 @@ public DataSeed() BaggageWeight = null }, - // SU105 (Paris → Moscow) — 2 пассажира без багажа new() { ID = 15, @@ -473,7 +451,7 @@ public DataSeed() Passenger = passengers[4], SeatNumber = "7A", HandLuggage = true, - BaggageWeight = null + BaggageWeight = 11.6 }, new() { @@ -482,7 +460,7 @@ public DataSeed() Passenger = passengers[5], SeatNumber = "7B", HandLuggage = false, - BaggageWeight = null + BaggageWeight = 0.5 } }; -} \ No newline at end of file +} diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs index 6ef0b05cf..97736036e 100644 --- a/Airline.Domain/Items/Flight.cs +++ b/Airline.Domain/Items/Flight.cs @@ -14,7 +14,7 @@ public class Flight /// /// Unique flight's ID. /// - public required int ID { get; set; } + public int ID { get; set; } /// /// Flight's code. @@ -52,8 +52,8 @@ public class Flight public TimeSpan? TravelTime { get; set; } /// - /// The type of plane for the flight. + /// The model of plane. /// public required PlaneModel Model { get; set; } } -} \ No newline at end of file +} diff --git a/Airline.Domain/Items/ModelFamily.cs b/Airline.Domain/Items/ModelFamily.cs index 25652a0cc..d27acbe72 100644 --- a/Airline.Domain/Items/ModelFamily.cs +++ b/Airline.Domain/Items/ModelFamily.cs @@ -14,7 +14,7 @@ public class ModelFamily /// /// Unique model's ID. /// - public required int ID { get; set; } + public int ID { get; set; } /// /// The name of model's family. diff --git a/Airline.Domain/Items/Passengers.cs b/Airline.Domain/Items/Passengers.cs index bfc255a00..6cf51b4f0 100644 --- a/Airline.Domain/Items/Passengers.cs +++ b/Airline.Domain/Items/Passengers.cs @@ -14,7 +14,7 @@ public class Passenger /// /// Unique passenger's ID. /// - public required int ID { get; set; } + public int ID { get; set; } /// /// The number of passenger's pasport. @@ -29,7 +29,7 @@ public class Passenger /// /// Passenger's date of birth. /// - public required string DateOfBirth { get; set; } + public DateOnly? DateOfBirth { get; set; } } } diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs index e0e158878..97b7fdbd8 100644 --- a/Airline.Domain/Items/PlaneModel.cs +++ b/Airline.Domain/Items/PlaneModel.cs @@ -15,7 +15,7 @@ public class PlaneModel /// /// Unique plane model's ID. /// - public required int ID { get; set; } + public int ID { get; set; } /// /// The name of plane model. From 35d83d728078dd9c2caf31b9ac22363adb96ec9e Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 29 Oct 2025 02:11:31 +0400 Subject: [PATCH 06/36] some required fixes --- .github/workflows/tests.yml | 29 +++ Airline.Domain/DataSeed/DataSeed.cs | 282 ++++++++++++++-------------- Airline.Domain/Items/Flight.cs | 100 +++++----- Airline.Domain/Items/ModelFamily.cs | 40 ++-- Airline.Domain/Items/Passengers.cs | 47 ++--- Airline.Domain/Items/PlaneModel.cs | 65 +++---- Airline.Domain/Items/Ticket.cs | 65 +++---- Airline.Tests/Airline.Tests.cs | 29 +-- 8 files changed, 325 insertions(+), 332 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..51dc8f03a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +name: .NET Tests + +on: + push: + branches: [ "main", "lab_1" ] + pull_request: + branches: [ "main", "lab_1" ] + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore ./Airline/Airline.sln + + - name: Build solution + run: dotnet build ./Airline/Airline.sln --no-restore + + - name: Run tests + run: dotnet test ./Airline/Airline.Tests/Airline.Tests.csproj --no-build --verbosity detailed \ No newline at end of file diff --git a/Airline.Domain/DataSeed/DataSeed.cs b/Airline.Domain/DataSeed/DataSeed.cs index 676f32674..c589c3a27 100644 --- a/Airline.Domain/DataSeed/DataSeed.cs +++ b/Airline.Domain/DataSeed/DataSeed.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Airline.Domain.Items; +using Airline.Domain.Items; namespace Airline.Domain.DataSeed; /// @@ -13,12 +7,34 @@ namespace Airline.Domain.DataSeed; /// public class DataSeed { + /// + /// Gets the list of seeded model families. + /// public List ModelFamilies { get; } + + /// + /// Gets the list of seeded plane models. + /// public List PlaneModels { get; } + + /// + /// Gets the list of seeded passengers. + /// public List Passengers { get; } - public List Flights { get; } + + /// + /// Gets the list of seeded flights. + /// + public List Flights { get; } + + /// + /// Gets the list of seeded tickets. + /// public List Tickets { get; } + /// + ///Initializes the DataSeed class and fills it with data. + /// public DataSeed() { ModelFamilies = InitModelFamilies(); @@ -31,325 +47,317 @@ public DataSeed() /// /// Initializes the model families with predefined data. /// - private static List InitModelFamilies() => new() - { - new() + private static List InitModelFamilies() => + [ + new ModelFamily { - ID = 1, + Id = 1, NameOfFamily = "A320 Family", ManufacturerName = "Airbus" }, - new() + new ModelFamily { - ID = 2, + Id = 2, NameOfFamily = "767 Family", ManufacturerName = "Boeing" }, - new() + new ModelFamily { - ID = 3, + Id = 3, NameOfFamily = "777 Family", ManufacturerName = "Boeing" }, - new() + new ModelFamily { - ID = 4, + Id = 4, NameOfFamily = "787 Dreamliner", ManufacturerName = "Boeing" }, - new() + new ModelFamily { - ID = 5, + Id = 5, NameOfFamily = "A330 Family", ManufacturerName = "Airbus" } - }; + ]; /// /// Initializes plane models linked to their families. /// - private static List InitPlaneModels(List families) => new() - { - new() + private static List InitPlaneModels(List families) => + [ + new PlaneModel { - ID = 1, + Id = 1, ModelName = "A320", PlaneFamily = families[0], MaxRange = 6000, PassengerCapacity = 180, CargoCapacity = 20 }, - new() + new PlaneModel { - ID = 2, + Id = 2, ModelName = "B767-300", PlaneFamily = families[1], MaxRange = 5500, PassengerCapacity = 189, CargoCapacity = 23 }, - new() + new PlaneModel { - ID = 3, + Id = 3, ModelName = "B777-300ER", PlaneFamily = families[2], MaxRange = 11000, PassengerCapacity = 370, CargoCapacity = 45 }, - new() + new PlaneModel { - ID = 4, + Id = 4, ModelName = "B787-9", PlaneFamily = families[3], MaxRange = 12000, PassengerCapacity = 290, CargoCapacity = 40 }, - new() + new PlaneModel { - ID = 5, + Id = 5, ModelName = "A330-300", PlaneFamily = families[4], MaxRange = 10500, PassengerCapacity = 300, CargoCapacity = 42 } - }; + ]; /// /// Initializes a list of passengers. /// - private static List InitPassengers() => new() - { - new() + private static List InitPassengers() => + [ + new Passenger { - ID = 1, + Id = 1, Passport = "477419070", PassengerName = "Ivanov Ivan", DateOfBirth = new(1990, 01, 15) }, - new() + new Passenger { - ID = 2, + Id = 2, Passport = "719011722", PassengerName = "Petrov Petr", DateOfBirth = new(1985, 05, 22) }, - new() + new Passenger { - ID = 3, + Id = 3, Passport = "269997862", PassengerName = "Alyohin Alexey", DateOfBirth = new(1992, 03, 10) }, - new() + new Passenger { - ID = 4, + Id = 4, Passport = "690256588", PassengerName = "Kuzina Anna", DateOfBirth = new(1991, 07, 30) }, - new() + new Passenger { - ID = 5, + Id = 5, Passport = "816817823", PassengerName = "Kuzin Dmitry", DateOfBirth = new(1988, 11, 05) }, - new() + new Passenger { - ID = 6, + Id = 6, Passport = "303776467", PassengerName = "Nikitich Dobrynya", DateOfBirth = new(1995, 09, 18) }, - new() + new Passenger { - ID = 7, + Id = 7, Passport = "510907182", PassengerName = "Popovich Alex", DateOfBirth = new(1993, 04, 12) }, - new() + new Passenger { - ID = 8, + Id = 8, Passport = "463835340", PassengerName = "Kolyan", DateOfBirth = new(1987, 08, 25) }, - new() + new Passenger { - ID = 9, + Id = 9, Passport = "877654233", PassengerName = "Lebedev Nikolay Ivanovich", DateOfBirth = new(1960, 02, 14) }, - new() + new Passenger { - ID = 10, + Id = 10, Passport = "112971133", PassengerName = "Sokolov Tigran", DateOfBirth = new(1994, 12, 03) } - }; + ]; /// /// Initializes flights with plane models and schedules. /// - private static List InitFlights(List models) => new() - { + private static List InitFlights(List models) => + [ - new() + new Flight { - ID = 1, + Id = 1, FlightCode = "SU101", DepartureCity = "Samara", ArrivalCity = "Wonderland", - DepartureDate = new(2025, 10, 10), - ArrivalDate = new(2025, 10, 10), - DepartureTime = new(8, 0, 0), + DepartureDateTime = new DateTime(2025, 10, 10, 8, 0, 0), + ArrivalDateTime = new DateTime(2025, 10, 10, 15, 0, 0), TravelTime = TimeSpan.FromHours(2), Model = models[0] }, - new() + new Flight { - ID = 2, + Id = 2, FlightCode = "SU102", DepartureCity = "Moscow", - ArrivalCity = "Paris", - DepartureDate = new(2025, 10, 10), - ArrivalDate = new(2025, 10, 10), - DepartureTime = new(9, 0, 0), + ArrivalCity = "Paris", + DepartureDateTime = new(2025, 10, 10, 6, 5, 0), + ArrivalDateTime = new(2025, 10, 10, 9, 5, 0), TravelTime = TimeSpan.FromHours(3), Model = models[1] }, - new() + new Flight { - ID = 3, + Id = 3, FlightCode = "SU103", DepartureCity = "Berlin", ArrivalCity = "Paris", - DepartureDate = new(2025, 10, 10), - ArrivalDate = new(2025, 10, 10), - DepartureTime = new(11, 0, 0), + DepartureDateTime = new(2025, 10, 10, 5, 0, 0), + ArrivalDateTime = new(2025, 10, 10, 10, 0, 0), TravelTime = TimeSpan.FromHours(5), Model = models[2] }, - new() + new Flight { - ID = 4, + Id = 4, FlightCode = "SU104", DepartureCity = "Samara", ArrivalCity = "Wonderland", - DepartureDate = new(2025, 10, 11), - ArrivalDate = new(2025, 10, 11), - DepartureTime = new(14, 0, 0), + DepartureDateTime = new(2025, 10, 11, 6, 0, 0), + ArrivalDateTime = new(2025, 10, 11, 8, 30, 0), TravelTime = TimeSpan.FromHours(2.5), Model = models[3] }, - new() + new Flight { - ID = 5, + Id = 5, FlightCode = "AZ201", DepartureCity = "Rome", ArrivalCity = "Milan", - DepartureDate = new(2025, 10, 11), - ArrivalDate = new(2025, 10, 11), - DepartureTime = new(7, 0, 0), + DepartureDateTime = new(2025, 10, 11, 22, 0, 0), + ArrivalDateTime = new(2025, 10, 12, 2, 30, 0), TravelTime = TimeSpan.FromHours(4.5), Model = models[4] }, - new() + new Flight { - ID = 6, + Id = 6, FlightCode = "SU200", DepartureCity = "Moscow", ArrivalCity = "Tokyo", - DepartureDate = new(2025, 10, 12), - ArrivalDate = new(2025, 10, 12), - DepartureTime = new(1, 0, 0), + DepartureDateTime = new(2025, 10, 11, 15, 0, 0), + ArrivalDateTime = new(2025, 10, 12, 6, 0, 0), TravelTime = TimeSpan.FromHours(15), Model = models[0] }, - new() + new Flight { - ID = 7, + Id = 7, FlightCode = "DL100", DepartureCity = "New York", ArrivalCity = "London", - DepartureDate = new(2025, 10, 12), - ArrivalDate = new(2025, 10, 13), - DepartureTime = new(18, 0, 0), + DepartureDateTime = new(2025, 10, 12, 7, 20, 0), + ArrivalDateTime = new(2025, 10, 13, 13, 20, 0), TravelTime = TimeSpan.FromHours(6), Model = models[1] }, - new() + new Flight { - ID = 8, + Id = 8, FlightCode = "SU105", DepartureCity = "Paris", ArrivalCity = "Moscow", - DepartureDate = new(2025, 10, 13), - ArrivalDate = new(2025, 10, 13), - DepartureTime = new(13, 0, 0), + DepartureDateTime = new(2025, 10, 13, 23, 0, 0), + ArrivalDateTime = new(2025, 10, 14, 6, 0, 0), TravelTime = TimeSpan.FromHours(7), Model = models[0] } - }; + ]; /// /// Initializes tickets linking flights to passengers. /// - private static List InitTickets(List flights, List passengers) => new() - { + private static List InitTickets(List flights, List passengers) => + [ - new() + new Ticket { - ID = 1, + Id = 1, Flight = flights[0], Passenger = passengers[0], SeatNumber = "12A", HandLuggage = true, BaggageWeight = 15.6 }, - new() + new Ticket { - ID = 2, + Id = 2, Flight = flights[0], Passenger = passengers[1], SeatNumber = "12B", HandLuggage = false, BaggageWeight = null }, - new() + new Ticket { - ID = 3, + Id = 3, Flight = flights[0], Passenger = passengers[2], SeatNumber = "12C", HandLuggage = true, BaggageWeight = null }, - new() + new Ticket { - ID = 4, + Id = 4, Flight = flights[0], Passenger = passengers[3], SeatNumber = "13A", HandLuggage = true, BaggageWeight = 1.2 }, - new() + new Ticket { - ID = 5, + Id = 5, Flight = flights[0], Passenger = passengers[4], SeatNumber = "13B", @@ -357,27 +365,27 @@ public DataSeed() BaggageWeight = 10.0 }, - new() + new Ticket { - ID = 6, + Id = 6, Flight = flights[1], Passenger = passengers[5], SeatNumber = "15A", HandLuggage = true, BaggageWeight = 5.2 }, - new() + new Ticket { - ID = 7, + Id = 7, Flight = flights[1], Passenger = passengers[6], SeatNumber = "15B", HandLuggage = true, BaggageWeight = 18.0 }, - new() + new Ticket { - ID = 8, + Id = 8, Flight = flights[1], Passenger = passengers[7], SeatNumber = "15C", @@ -385,18 +393,18 @@ public DataSeed() BaggageWeight = null }, - new() + new Ticket { - ID = 9, + Id = 9, Flight = flights[2], Passenger = passengers[8], SeatNumber = "20A", HandLuggage = true, BaggageWeight = 3.2 }, - new() + new Ticket { - ID = 10, + Id = 10, Flight = flights[2], Passenger = passengers[9], SeatNumber = "20B", @@ -404,9 +412,9 @@ public DataSeed() BaggageWeight = 7.0 }, - new() + new Ticket { - ID = 11, + Id = 11, Flight = flights[3], Passenger = passengers[0], SeatNumber = "10A", @@ -414,9 +422,9 @@ public DataSeed() BaggageWeight = 4.2 }, - new() + new Ticket { - ID = 12, + Id = 12, Flight = flights[4], Passenger = passengers[1], SeatNumber = "5A", @@ -424,9 +432,9 @@ public DataSeed() BaggageWeight = 6.0 }, - new() + new Ticket { - ID = 13, + Id = 13, Flight = flights[5], Passenger = passengers[2], SeatNumber = "1A", @@ -434,9 +442,9 @@ public DataSeed() BaggageWeight = 25.0 }, - new() + new Ticket { - ID = 14, + Id = 14, Flight = flights[6], Passenger = passengers[3], SeatNumber = "8A", @@ -444,23 +452,23 @@ public DataSeed() BaggageWeight = null }, - new() + new Ticket { - ID = 15, + Id = 15, Flight = flights[7], Passenger = passengers[4], SeatNumber = "7A", HandLuggage = true, BaggageWeight = 11.6 }, - new() + new Ticket { - ID = 16, + Id = 16, Flight = flights[7], Passenger = passengers[5], SeatNumber = "7B", HandLuggage = false, BaggageWeight = 0.5 } - }; -} + ]; +} \ No newline at end of file diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs index 97736036e..f494b608e 100644 --- a/Airline.Domain/Items/Flight.cs +++ b/Airline.Domain/Items/Flight.cs @@ -1,59 +1,47 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Airline.Domain.Items; -namespace Airline.Domain.Items +/// +/// The class for flight description and information about it. +/// +public class Flight { /// - /// The class for flight description and information about it. - /// - public class Flight - { - /// - /// Unique flight's ID. - /// - public int ID { get; set; } - - /// - /// Flight's code. - /// - public required string FlightCode { get; set; } - - /// - /// The place of departure. - /// - public required string DepartureCity { get; set; } - - /// - /// The place of arrival. - /// - public required string ArrivalCity { get; set; } - - /// - /// Date of the departure. - /// - public DateOnly? DepartureDate { get; set; } - - /// - /// Date of the arrival. - /// - public DateOnly? ArrivalDate { get; set; } - - /// - /// Flight's eparture time. - /// - public TimeSpan? DepartureTime { get; set; } - - /// - /// Flight's travel time. - /// - public TimeSpan? TravelTime { get; set; } - - /// - /// The model of plane. - /// - public required PlaneModel Model { get; set; } - } -} + /// Unique flight's Id. + /// + public int Id { get; set; } + + /// + /// Flight's code. + /// + public required string FlightCode { get; set; } + + /// + /// The place of departure. + /// + public required string DepartureCity { get; set; } + + /// + /// The place of arrival. + /// + public required string ArrivalCity { get; set; } + + /// + /// Date and time of the arrival. + /// + public DateTime? ArrivalDateTime { get; set; } + + /// + /// Flight's departure date and time. + /// + public DateTime? DepartureDateTime { get; set; } + + /// + /// Flight's travel time. + /// + public TimeSpan? TravelTime { get; set; } + + /// + /// The model of plane. + /// + public required PlaneModel Model { get; set; } +} \ No newline at end of file diff --git a/Airline.Domain/Items/ModelFamily.cs b/Airline.Domain/Items/ModelFamily.cs index d27acbe72..717d0987e 100644 --- a/Airline.Domain/Items/ModelFamily.cs +++ b/Airline.Domain/Items/ModelFamily.cs @@ -1,30 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Airline.Domain.Items; -namespace Airline.Domain.Items +/// +/// The class for information about a model family. +/// +public class ModelFamily { /// - /// The class for information about a model family. + /// Unique model's Id. /// - public class ModelFamily - { - /// - /// Unique model's ID. - /// - public int ID { get; set; } + public int Id { get; set; } - /// - /// The name of model's family. - /// - public required string NameOfFamily { get; set; } - - /// - /// The name of the model manufacturer. - /// - public required string ManufacturerName { get; set; } - } + /// + /// The name of model's family. + /// + public required string NameOfFamily { get; set; } -} + /// + /// The name of the model manufacturer. + /// + public required string ManufacturerName { get; set; } +} \ No newline at end of file diff --git a/Airline.Domain/Items/Passengers.cs b/Airline.Domain/Items/Passengers.cs index 6cf51b4f0..e2a0aaa46 100644 --- a/Airline.Domain/Items/Passengers.cs +++ b/Airline.Domain/Items/Passengers.cs @@ -1,35 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Airline.Domain.Items; -namespace Airline.Domain.Items +/// +/// The class for describing a passenger. +/// +public class Passenger { /// - /// The class for describing a passenger. + /// Unique passenger's Id. /// - public class Passenger - { - /// - /// Unique passenger's ID. - /// - public int ID { get; set; } + public int Id { get; set; } - /// - /// The number of passenger's pasport. - /// - public required string Passport { get; set; } + /// + /// The number of passenger's pasport. + /// + public required string Passport { get; set; } - /// - /// Passenger's full name. - /// - public required string PassengerName { get; set; } + /// + /// Passenger's full name. + /// + public required string PassengerName { get; set; } - /// - /// Passenger's date of birth. - /// - public DateOnly? DateOfBirth { get; set; } + /// + /// Passenger's date of birth. + /// + public DateOnly? DateOfBirth { get; set; } - } -} +} \ No newline at end of file diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs index 97b7fdbd8..2a5e0a917 100644 --- a/Airline.Domain/Items/PlaneModel.cs +++ b/Airline.Domain/Items/PlaneModel.cs @@ -1,46 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Airline.Domain.Items; -namespace Airline.Domain.Items +/// +/// The class for information about a plane model. +/// +public class PlaneModel { - /// - /// The class for information about a plane model. + /// Unique plane model's Id. /// - public class PlaneModel - { - /// - /// Unique plane model's ID. - /// - public int ID { get; set; } - - /// - /// The name of plane model. - /// - public required string ModelName { get; set; } + public int Id { get; set; } - /// - /// The model family of the plane. - /// - public required ModelFamily PlaneFamily { get; set; } + /// + /// The name of plane model. + /// + public required string ModelName { get; set; } - /// - /// The max flight range of the plane model. - /// - public required double MaxRange { get; set; } + /// + /// The model family of the plane. + /// + public required ModelFamily PlaneFamily { get; set; } - /// - /// The passenger capacity of the plane model (tons). - /// - public required double PassengerCapacity { get; set; } + /// + /// The max flight range of the plane model. + /// + public required double MaxRange { get; set; } - /// - /// The cargo capacity of the plane model (tons). - /// - public required double CargoCapacity { get; set; } - } + /// + /// The passenger capacity of the plane model. + /// + public required int PassengerCapacity { get; set; } -} + /// + /// The cargo capacity of the plane model (tons). + /// + public required double CargoCapacity { get; set; } +} \ No newline at end of file diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs index aec3a69c5..9404baa5f 100644 --- a/Airline.Domain/Items/Ticket.cs +++ b/Airline.Domain/Items/Ticket.cs @@ -1,46 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Airline.Domain.Items; -namespace Airline.Domain.Items +/// +/// The class for information about ticket. +/// +public class Ticket { - /// - /// The class for information about ticket. + /// Unique ticket's Id. /// - public class Ticket - { - /// - /// Unique ticket's ID. - /// - public int ID { get; set; } - - /// - /// The connection between the ticket and the flight. - /// - public required Flight Flight { get; set; } + public int Id { get; set; } - /// - /// The connection between the ticket and the passenger. - /// - public required Passenger Passenger { get; set; } + /// + /// The connection between the ticket and the flight. + /// + public required Flight Flight { get; set; } - /// - /// The passenger's seat number. - /// - public required string SeatNumber { get; set; } + /// + /// The connection between the ticket and the passenger. + /// + public required Passenger Passenger { get; set; } - /// - /// The flag to indicate if there is a hand luggage. - /// - public required bool HandLuggage { get; set; } + /// + /// The passenger's seat number. + /// + public required string SeatNumber { get; set; } - /// - /// Total baggage weight. (kilograms) - /// - public double? BaggageWeight { get; set; } - } + /// + /// The flag to indicate if there is a hand luggage. + /// + public required bool HandLuggage { get; set; } -} + /// + /// Total baggage weight. (kilograms) + /// + public double? BaggageWeight { get; set; } +} \ No newline at end of file diff --git a/Airline.Tests/Airline.Tests.cs b/Airline.Tests/Airline.Tests.cs index 275259fa5..458d800f2 100644 --- a/Airline.Tests/Airline.Tests.cs +++ b/Airline.Tests/Airline.Tests.cs @@ -3,7 +3,7 @@ namespace Airline.Tests; -public class AirCompanyTests(DataSeed _seed) : IClassFixture +public class AirCompanyTests(DataSeed seed) : IClassFixture { /// /// Verifies that the method returns the top 5 flights based on the number of passengers transported. @@ -11,7 +11,7 @@ public class AirCompanyTests(DataSeed _seed) : IClassFixture [Fact] public void GetTop5FlightsByPassengerCount_ReturnsCorrectFlights() { - var flightPassengerCounts = _seed.Tickets + var flightPassengerCounts = seed.Tickets .GroupBy(t => t.Flight) .Select(g => new { Flight = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count) @@ -29,7 +29,7 @@ public void GetTop5FlightsByPassengerCount_ReturnsCorrectFlights() [Fact] public void GetFlightsWithMinTravelTime_ReturnsCorrectFlights() { - var validFlights = _seed.Flights.Where(f => f.TravelTime.HasValue).ToList(); + var validFlights = seed.Flights.Where(f => f.TravelTime.HasValue).ToList(); var minTime = validFlights.Min(f => f.TravelTime!.Value); var result = validFlights .Where(f => f.TravelTime == minTime) @@ -46,16 +46,17 @@ public void GetFlightsWithMinTravelTime_ReturnsCorrectFlights() [Fact] public void GetPassengersWithZeroBaggageOnsFlight_ReturnsSortedPassengers() { - var flight = _seed.Flights.First(f => f.FlightCode == "SU101"); - var passengersWithNoBaggage = _seed.Tickets + var flight = seed.Flights.First(f => f.FlightCode == "SU101"); + var passengersWithNoBaggage = seed.Tickets .Where(t => t.Flight == flight && t.BaggageWeight == null) .Select(t => t.Passenger) .OrderBy(p => p.PassengerName) .ToList(); + // Предположим, у Petrov Petr ID = 2, у Sidorov Alexey ID = 3 + Assert.Contains(passengersWithNoBaggage, p => p.Id == 2); + Assert.Contains(passengersWithNoBaggage, p => p.Id == 3); Assert.Equal(2, passengersWithNoBaggage.Count); - Assert.Equal("Alyohin Alexey", passengersWithNoBaggage[0].PassengerName); - Assert.Equal("Petrov Petr", passengersWithNoBaggage[1].PassengerName); } /// @@ -66,13 +67,13 @@ public void GetPassengersWithZeroBaggageOnsFlight_ReturnsSortedPassengers() public void GetFlightsByModelInPeriod_ReturnsCorrectFlights() { var modelName = "A320"; - var from = new DateOnly(2025, 10, 10); - var to = new DateOnly(2025, 10, 12); + var from = new DateTime(2025, 10, 10); + var to = new DateTime(2025, 10, 12); - var result = _seed.Flights + var result = seed.Flights .Where(f => f.Model.ModelName == modelName && - f.DepartureDate >= from && - f.DepartureDate <= to) + f.DepartureDateTime >= from && + f.DepartureDateTime <= to) .ToList(); Assert.Equal(2, result.Count); @@ -90,7 +91,7 @@ public void GetFlightsByRoute_ReturnsCorrectFlights() var departure = "Samara"; var arrival = "Wonderland"; - var result = _seed.Flights + var result = seed.Flights .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) .ToList(); @@ -98,4 +99,4 @@ public void GetFlightsByRoute_ReturnsCorrectFlights() Assert.Contains(result, f => f.FlightCode == "SU101"); Assert.Contains(result, f => f.FlightCode == "SU104"); } -} +} \ No newline at end of file From f4b9831703d8cdd51b890f921062aefc7515f85a Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 29 Oct 2025 02:17:15 +0400 Subject: [PATCH 07/36] my bad --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51dc8f03a..b4825c02f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: run: dotnet restore ./Airline/Airline.sln - name: Build solution - run: dotnet build ./Airline/Airline.sln --no-restore + run: dotnet build ./Airline.sln --no-restore - name: Run tests run: dotnet test ./Airline/Airline.Tests/Airline.Tests.csproj --no-build --verbosity detailed \ No newline at end of file From 4d7b1e7548ab636cdbd42f2c1ef0927932ee1d95 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 29 Oct 2025 02:19:36 +0400 Subject: [PATCH 08/36] my bad x2 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b4825c02f..a2445a455 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: dotnet-version: '8.0.x' - name: Restore dependencies - run: dotnet restore ./Airline/Airline.sln + run: dotnet restore ./Airline.sln - name: Build solution run: dotnet build ./Airline.sln --no-restore From 6318b4f4f63e1502da883805cece898897d5d7e1 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 29 Oct 2025 02:22:20 +0400 Subject: [PATCH 09/36] editing tests.yml --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a2445a455..ed83212b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,4 +26,4 @@ jobs: run: dotnet build ./Airline.sln --no-restore - name: Run tests - run: dotnet test ./Airline/Airline.Tests/Airline.Tests.csproj --no-build --verbosity detailed \ No newline at end of file + run: dotnet test ./Airline.Tests/Airline.Tests.csproj --no-build --verbosity detailed \ No newline at end of file From 7f6e5e35d2045b5369039ff1dc7ed13da5849543 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 29 Oct 2025 02:48:21 +0400 Subject: [PATCH 10/36] cosmetic details --- Airline.Domain/DataSeed/DataSeed.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Airline.Domain/DataSeed/DataSeed.cs b/Airline.Domain/DataSeed/DataSeed.cs index c589c3a27..71ed083ba 100644 --- a/Airline.Domain/DataSeed/DataSeed.cs +++ b/Airline.Domain/DataSeed/DataSeed.cs @@ -1,4 +1,5 @@ using Airline.Domain.Items; + namespace Airline.Domain.DataSeed; /// @@ -8,27 +9,27 @@ namespace Airline.Domain.DataSeed; public class DataSeed { /// - /// Gets the list of seeded model families. + /// The list of model families. /// public List ModelFamilies { get; } /// - /// Gets the list of seeded plane models. + /// The list of plane models. /// public List PlaneModels { get; } /// - /// Gets the list of seeded passengers. + /// The list of passengers. /// public List Passengers { get; } /// - /// Gets the list of seeded flights. + /// The list of flights. /// public List Flights { get; } /// - /// Gets the list of seeded tickets. + /// The list of tickets. /// public List Tickets { get; } From 6ee8a2c4e7caa24510c4d20c874c7e5cafef25d8 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 13 Nov 2025 04:19:27 +0400 Subject: [PATCH 11/36] first steps with dto --- .../Flight/CreateFlightDto.cs | 18 +++++++ .../Flight/FlightDto.cs | 20 +++++++ .../ModelFamily/CreateModelFamilyDto.cs | 8 +++ .../ModelFamily/ModelFamilyDto.cs | 10 ++++ .../Passenger/CreatePassengerDto.cs | 9 ++++ .../Passenger/PassengerDto.cs | 10 ++++ .../PlaneModel/CreatePlaneModelDto.cs | 16 ++++++ .../PlaneModel/PlaneModelDto.cs | 18 +++++++ .../Ticket/CreateTicketDto.cs | 16 ++++++ .../Ticket/TicketDto.cs | 18 +++++++ Airline.Domain/Repository/IRepository.cs | 54 +++++++++++++++++++ 11 files changed, 197 insertions(+) create mode 100644 Airline.Application.Contracts/Flight/CreateFlightDto.cs create mode 100644 Airline.Application.Contracts/Flight/FlightDto.cs create mode 100644 Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs create mode 100644 Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs create mode 100644 Airline.Application.Contracts/Passenger/CreatePassengerDto.cs create mode 100644 Airline.Application.Contracts/Passenger/PassengerDto.cs create mode 100644 Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs create mode 100644 Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs create mode 100644 Airline.Application.Contracts/Ticket/CreateTicketDto.cs create mode 100644 Airline.Application.Contracts/Ticket/TicketDto.cs create mode 100644 Airline.Domain/Repository/IRepository.cs diff --git a/Airline.Application.Contracts/Flight/CreateFlightDto.cs b/Airline.Application.Contracts/Flight/CreateFlightDto.cs new file mode 100644 index 000000000..7e8cc8337 --- /dev/null +++ b/Airline.Application.Contracts/Flight/CreateFlightDto.cs @@ -0,0 +1,18 @@ +namespace Airline.Application.Contracts.Flight; + +/// +/// DTO for creating a new flight. +/// +/// Flight code (e.g., SU101). +/// City of departure. +/// City of arrival. +/// Date and time of departure. +/// Date and time of arrival. +/// ID of the plane model used for the flight. +public record CreateFlightDto( + string FlightCode, + string DepartureCity, + string ArrivalCity, + DateTime DepartureDateTime, + DateTime ArrivalDateTime, + string ModelId); \ No newline at end of file diff --git a/Airline.Application.Contracts/Flight/FlightDto.cs b/Airline.Application.Contracts/Flight/FlightDto.cs new file mode 100644 index 000000000..dea2c8d3e --- /dev/null +++ b/Airline.Application.Contracts/Flight/FlightDto.cs @@ -0,0 +1,20 @@ +namespace Airline.Application.Contracts.Flight; + +/// +/// DTO representing a flight. +/// +/// Unique identifier of the flight. +/// Flight code (e.g., SU101). +/// City of departure. +/// City of arrival. +/// Date and time of departure. +/// Date and time of arrival. +/// ID of the plane model used for the flight. +public record FlightDto( + string Id, + string FlightCode, + string DepartureCity, + string ArrivalCity, + DateTime DepartureDateTime, + DateTime ArrivalDateTime, + string ModelId); \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs new file mode 100644 index 000000000..03267e44d --- /dev/null +++ b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs @@ -0,0 +1,8 @@ +namespace Airline.Application.Contracts.ModelFamily; + +/// +/// DTO for creating a new model family. +/// +/// Name of the model family. +/// Manufacturer of the model family. +public record CreateModelFamilyDto(string NameOfFamily, string ManufacturerName); \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs new file mode 100644 index 000000000..e54894ade --- /dev/null +++ b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.ModelFamily; + +/// +/// DTO representing a model family. +/// Contains basic information about the family. +/// +/// Unique identifier of the model family. +/// Name of the model family. +/// Manufacturer of the model family. +public record ModelFamilyDto(string Id, string NameOfFamily, string ManufacturerName); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs new file mode 100644 index 000000000..732cf4f1d --- /dev/null +++ b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs @@ -0,0 +1,9 @@ +namespace Airline.Application.Contracts.Passenger; + +/// +/// DTO for creating a new passenger. +/// +/// Passport number. +/// Full name of the passenger. +/// Date of birth (YYYY-MM-DD). +public record CreatePassengerDto(string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/PassengerDto.cs b/Airline.Application.Contracts/Passenger/PassengerDto.cs new file mode 100644 index 000000000..f8acedfa3 --- /dev/null +++ b/Airline.Application.Contracts/Passenger/PassengerDto.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.Passenger; + +/// +/// DTO representing a passenger. +/// +/// Unique identifier of the passenger. +/// Passport number. +/// Full name of the passenger. +/// Date of birth (YYYY-MM-DD). +public record PassengerDto(string Id, string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs new file mode 100644 index 000000000..6b99f5d1c --- /dev/null +++ b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs @@ -0,0 +1,16 @@ +namespace Airline.Application.Contracts.PlaneModel; + +/// +/// DTO for creating a new plane model. +/// +/// Name of the plane model. +/// ID of the associated model family. +/// Maximum flight range (km). +/// Passenger capacity. +/// Cargo capacity (tons). +public record CreatePlaneModelDto( + string ModelName, + string PlaneFamilyId, + double MaxRange, + double PassengerCapacity, + double CargoCapacity); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs new file mode 100644 index 000000000..648912200 --- /dev/null +++ b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs @@ -0,0 +1,18 @@ +namespace Airline.Application.Contracts.PlaneModel; + +/// +/// DTO representing a plane model. +/// +/// Unique identifier of the plane model. +/// Name of the plane model. +/// ID of the associated model family. +/// Maximum flight range (km). +/// Passenger capacity. +/// Cargo capacity (tons). +public record PlaneModelDto( + string Id, + string ModelName, + string PlaneFamilyId, + double MaxRange, + double PassengerCapacity, + double CargoCapacity); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs new file mode 100644 index 000000000..eba4e1638 --- /dev/null +++ b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs @@ -0,0 +1,16 @@ +namespace Airline.Application.Contracts.Ticket; + +/// +/// DTO for creating a new ticket. +/// +/// ID of the associated flight. +/// ID of the associated passenger. +/// Seat number (e.g., 12A). +/// Indicates if hand luggage is present. +/// Total baggage weight in kilograms (null if no baggage). +public record CreateTicketDto( + string FlightId, + string PassengerId, + string SeatNumber, + bool HandLuggage, + double? BaggageWeight); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/TicketDto.cs b/Airline.Application.Contracts/Ticket/TicketDto.cs new file mode 100644 index 000000000..c2c0263a2 --- /dev/null +++ b/Airline.Application.Contracts/Ticket/TicketDto.cs @@ -0,0 +1,18 @@ +namespace Airline.Application.Contracts.Ticket; + +/// +/// DTO representing a ticket. +/// +/// Unique identifier of the ticket. +/// ID of the associated flight. +/// ID of the associated passenger. +/// Seat number (e.g., 12A). +/// Indicates if hand luggage is present. +/// Total baggage weight in kilograms (null if no baggage). +public record TicketDto( + string Id, + string FlightId, + string PassengerId, + string SeatNumber, + bool HandLuggage, + double? BaggageWeight); \ No newline at end of file diff --git a/Airline.Domain/Repository/IRepository.cs b/Airline.Domain/Repository/IRepository.cs new file mode 100644 index 000000000..d93a022c0 --- /dev/null +++ b/Airline.Domain/Repository/IRepository.cs @@ -0,0 +1,54 @@ +namespace Airline.Domain.Repository; + +/// +/// Defines a generic repository interface that provides +/// basic CRUD (Create, Read, Update, Delete) operations. +/// +/// +/// The type of the entity being managed by the repository. +/// +/// +/// The type of the entity's unique identifier (e.g., for MongoDB). +/// +public interface IRepository + where TEntity : class +{ + /// + /// Adds a new entity to the repository. + /// + /// The entity instance to add. + /// The created entity. + Task CreateAsync(TEntity entity); + + /// + /// Retrieves an entity from the repository by its identifier. + /// + /// The unique identifier of the entity. + /// + /// The entity with the specified identifier, or + /// if no entity with such an identifier exists. + /// + Task GetAsync(TKey id); + + /// + /// Retrieves all entities stored in the repository. + /// + /// + /// A list containing all entities in the repository. + /// + Task> GetAllAsync(); + + /// + /// Updates an existing entity in the repository. + /// + /// The entity instance containing updated data. + /// The updated entity. + Task UpdateAsync(TEntity entity); + + /// + /// Removes an entity from the repository by its identifier. + /// + /// The unique identifier of the entity to delete. + /// if the entity was deleted; otherwise, . + Task DeleteAsync(TKey id); +} \ No newline at end of file From 6b41b49e9e91bfa73b29a59f7a7458fd1e262dcd Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 19 Nov 2025 03:11:40 +0400 Subject: [PATCH 12/36] help me. services --- .../Airline.Application.Contracts.csproj | 9 +++++++++ .../Flight/IFlightService.cs | 10 ++++++++++ Airline.Application.Contracts/IAnalyticService.cs | 11 +++++++++++ .../ModelFamily/IModelFamilyService.cs | 10 ++++++++++ .../Passenger/IPassengerService.cs | 10 ++++++++++ .../PlaneModel/IPlaneModel.cs | 10 ++++++++++ .../Ticket/ITicketService.cs | 10 ++++++++++ 7 files changed, 70 insertions(+) create mode 100644 Airline.Application.Contracts/Airline.Application.Contracts.csproj create mode 100644 Airline.Application.Contracts/Flight/IFlightService.cs create mode 100644 Airline.Application.Contracts/IAnalyticService.cs create mode 100644 Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs create mode 100644 Airline.Application.Contracts/Passenger/IPassengerService.cs create mode 100644 Airline.Application.Contracts/PlaneModel/IPlaneModel.cs create mode 100644 Airline.Application.Contracts/Ticket/ITicketService.cs diff --git a/Airline.Application.Contracts/Airline.Application.Contracts.csproj b/Airline.Application.Contracts/Airline.Application.Contracts.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Airline.Application.Contracts/Airline.Application.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Airline.Application.Contracts/Flight/IFlightService.cs b/Airline.Application.Contracts/Flight/IFlightService.cs new file mode 100644 index 000000000..d40816b64 --- /dev/null +++ b/Airline.Application.Contracts/Flight/IFlightService.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.Flight; + +public interface IFlightService +{ + public Task> GetAllAsync(); + public Task GetByIdAsync(string id); + public Task CreateAsync(FlightDto flight); + public Task UpdateAsync(FlightDto flight); + public Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/Airline.Application.Contracts/IAnalyticService.cs b/Airline.Application.Contracts/IAnalyticService.cs new file mode 100644 index 000000000..f5b4cbcf7 --- /dev/null +++ b/Airline.Application.Contracts/IAnalyticService.cs @@ -0,0 +1,11 @@ +namespace Airline.Application.Contracts.Flight; +namespace Airline.Application.Contracts.Passenger; + +public interface IAnalyticsService +{ + public Task> GetTopFlightsByPassengerCountAsync(int top = 5); + public Task> GetFlightsWithMinTravelTimeAsync(); + public Task> GetPassengersWithZeroBaggageOnFlightAsync(string flightId); + public Task> GetFlightsByModelInPeriodAsync(string modelName, DateTime from, DateTime to); + public Task> GetFlightsByRouteAsync(string departure, string arrival); +} \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs new file mode 100644 index 000000000..0c29aa77a --- /dev/null +++ b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.ModelFamily; + +public interface IModelFamilyService +{ + public Task> GetAllAsync(); + public Task GetByIdAsync(string id); + public Task CreateAsync(ModelFamilyDto family); + public Task UpdateAsync(ModelFamilyDto family); + public Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/IPassengerService.cs b/Airline.Application.Contracts/Passenger/IPassengerService.cs new file mode 100644 index 000000000..823d1f4d1 --- /dev/null +++ b/Airline.Application.Contracts/Passenger/IPassengerService.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.Passenger; + +public interface IPassengerService +{ + public Task> GetAllAsync(); + public Task GetByIdAsync(string id); + public Task CreateAsync(PassengerDto passenger); + public Task UpdateAsync(PassengerDto passenger); + public Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs b/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs new file mode 100644 index 000000000..bade458ca --- /dev/null +++ b/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.PlaneModel; + +public interface IPlaneModelService +{ + public Task> GetAllAsync(); + public Task GetByIdAsync(string id); + public Task CreateAsync(PlaneModelDto model); + public Task UpdateAsync(PlaneModelDto model); + public Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/ITicketService.cs b/Airline.Application.Contracts/Ticket/ITicketService.cs new file mode 100644 index 000000000..c5841a633 --- /dev/null +++ b/Airline.Application.Contracts/Ticket/ITicketService.cs @@ -0,0 +1,10 @@ +namespace Airline.Application.Contracts.Ticket; + +public interface ITicketService +{ + public Task> GetAllAsync(); + public Task GetByIdAsync(string id); + public Task CreateAsync(TicketDto ticket); + public Task UpdateAsync(TicketDto ticket); + public Task DeleteAsync(string id); +} \ No newline at end of file From 45dbecd23e2813b8c7f6df71bf639509bf9ed36e Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 27 Nov 2025 14:09:08 +0400 Subject: [PATCH 13/36] playing with efcore --- Airline.Api.Host/Airline.Api.Host.csproj | 13 ++ Airline.Api.Host/Airline.Api.Host.http | 6 + .../Controllers/WeatherForecastController.cs | 31 +++++ Airline.Api.Host/Program.cs | 25 ++++ .../Properties/launchSettings.json | 41 ++++++ Airline.Api.Host/WeatherForecast.cs | 12 ++ Airline.Api.Host/appsettings.Development.json | 8 ++ Airline.Api.Host/appsettings.json | 9 ++ .../Airline.Application.Contracts.csproj | 4 + .../Airline.Application.csproj | 9 ++ Airline.Application/Services/FlightService.cs | 125 ++++++++++++++++++ .../Airline.Infrastructure.EfCore.csproj | 19 +++ .../AirlineDbContext.cs | 109 +++++++++++++++ Airline.Tests/Airline.Tests.csproj | 8 +- Airline.sln | 38 +++++- 15 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 Airline.Api.Host/Airline.Api.Host.csproj create mode 100644 Airline.Api.Host/Airline.Api.Host.http create mode 100644 Airline.Api.Host/Controllers/WeatherForecastController.cs create mode 100644 Airline.Api.Host/Program.cs create mode 100644 Airline.Api.Host/Properties/launchSettings.json create mode 100644 Airline.Api.Host/WeatherForecast.cs create mode 100644 Airline.Api.Host/appsettings.Development.json create mode 100644 Airline.Api.Host/appsettings.json create mode 100644 Airline.Application/Airline.Application.csproj create mode 100644 Airline.Application/Services/FlightService.cs create mode 100644 Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj create mode 100644 Airline.Infrastructure.EfCore/AirlineDbContext.cs diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj new file mode 100644 index 000000000..5419ef0c2 --- /dev/null +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Airline.Api.Host/Airline.Api.Host.http b/Airline.Api.Host/Airline.Api.Host.http new file mode 100644 index 000000000..15bf8ba7b --- /dev/null +++ b/Airline.Api.Host/Airline.Api.Host.http @@ -0,0 +1,6 @@ +@Airline.Api.Host_HostAddress = http://localhost:5264 + +GET {{Airline.Api.Host_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Airline.Api.Host/Controllers/WeatherForecastController.cs b/Airline.Api.Host/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..61192a3bb --- /dev/null +++ b/Airline.Api.Host/Controllers/WeatherForecastController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Airline.Api.Host.Controllers; +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs new file mode 100644 index 000000000..48863a6d6 --- /dev/null +++ b/Airline.Api.Host/Program.cs @@ -0,0 +1,25 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +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(); diff --git a/Airline.Api.Host/Properties/launchSettings.json b/Airline.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..90f8fd242 --- /dev/null +++ b/Airline.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:34305", + "sslPort": 44395 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5264", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7171;http://localhost:5264", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Airline.Api.Host/WeatherForecast.cs b/Airline.Api.Host/WeatherForecast.cs new file mode 100644 index 000000000..e6ebe52c5 --- /dev/null +++ b/Airline.Api.Host/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Airline.Api.Host; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/Airline.Api.Host/appsettings.Development.json b/Airline.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Airline.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Airline.Api.Host/appsettings.json b/Airline.Api.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Airline.Api.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Airline.Application.Contracts/Airline.Application.Contracts.csproj b/Airline.Application.Contracts/Airline.Application.Contracts.csproj index fa71b7ae6..be813cb62 100644 --- a/Airline.Application.Contracts/Airline.Application.Contracts.csproj +++ b/Airline.Application.Contracts/Airline.Application.Contracts.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Airline.Application/Airline.Application.csproj b/Airline.Application/Airline.Application.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Airline.Application/Airline.Application.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs new file mode 100644 index 000000000..23bb8e0d9 --- /dev/null +++ b/Airline.Application/Services/FlightService.cs @@ -0,0 +1,125 @@ +using Airline.Application.Contracts.Flight; +using Airline.Domain.Items; +using Airline.Domain.Repositories; +using Microsoft.Extensions.Logging; + +namespace Airline.Application.Service; + +/// +/// Provides operations for managing flights in the airline system. +/// +/// Repository for accessing flight data. +/// Repository for validating and accessing aircraft model data. +public class FlightService( + IRepository flightRepository, + IRepository planeModelRepository +) : IFlightService +{ + /// + /// Creates a new flight. + /// + /// The flight information. + /// The created flight. + /// + /// Thrown if the specified aircraft model does not exist. + /// + public async Task CreateAsync(Flight entity) + { + // , + if (await planeModelRepository.GetAsync(entity.ModelId) is null) + { + throw new KeyNotFoundException($"Aircraft model with Id = {entity.ModelId} does not exist."); + } + + return await flightRepository.CreateAsync(entity); + } + + /// + /// Retrieves all flights from the system. + /// + /// A list of all flights. + public async Task> GetAllAsync() => + await flightRepository.GetAllAsync(); + + /// + /// Retrieves a specific flight by its unique identifier. + /// + /// The unique identifier of the flight. + /// The flight if found; otherwise, null. + public async Task GetByIdAsync(string id) => + await flightRepository.GetAsync(id); + + /// + /// Updates an existing flight. + /// + /// The updated flight information. + /// The updated flight. + public async Task UpdateAsync(Flight entity) => + await flightRepository.UpdateAsync(entity); + + /// + /// Deletes a flight from the system. + /// + /// The unique identifier of the flight to delete. + /// True if deletion was successful; otherwise, false. + public async Task DeleteAsync(string id) => + await flightRepository.DeleteAsync(id); + + // ---------- ( -) ---------- + + /// + /// Retrieves the top N flights by number of passengers. + /// + /// Number of top flights to return. + /// List of top flights. + public async Task> GetTopFlightsByPassengerCountAsync(int top = 5) + { + var allFlights = await flightRepository.GetAllAsync(); + // JOIN + // ( ) + return allFlights.Take(top).ToList(); + } + + /// + /// Retrieves flights with the minimal travel time. + /// + /// List of flights with minimal travel time. + public async Task> GetFlightsWithMinTravelTimeAsync() + { + var allFlights = await flightRepository.GetAllAsync(); + var minTime = allFlights.Min(f => f.ArrivalDateTime - f.DepartureDateTime); + return allFlights + .Where(f => f.ArrivalDateTime - f.DepartureDateTime == minTime) + .ToList(); + } + + /// + /// Retrieves flights for a specific route. + /// + /// Departure city. + /// Arrival city. + /// List of flights matching the route. + public async Task> GetFlightsByRouteAsync(string departure, string arrival) => + (await flightRepository.GetAllAsync()) + .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) + .ToList(); + + /// + /// Retrieves flights of a specific aircraft model within a date period. + /// + /// Name of the aircraft model. + /// Start date (inclusive). + /// End date (inclusive). + /// List of matching flights. + public async Task> GetFlightsByModelInPeriodAsync(string modelName, DateTime from, DateTime to) + { + var allFlights = await flightRepository.GetAllAsync(); + // JOIN PlaneModel + // , modelName Flight + return allFlights + .Where(f => f.ModelName == modelName && + f.DepartureDateTime >= from && + f.DepartureDateTime <= to) + .ToList(); + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj new file mode 100644 index 000000000..70f5e304f --- /dev/null +++ b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs new file mode 100644 index 000000000..d5eed669c --- /dev/null +++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs @@ -0,0 +1,109 @@ +using Airline.Domain.Items; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Reflection.Emit; + +namespace Airline.Infrastructure.EfCore; + +/// +/// Database context for the airline management system. +/// Configures entity mappings and collections for MongoDB. +/// +public class AirlineDbContext(DbContextOptions options) : DbContext(options) +{ + /// + /// Collection of aircraft model families. + /// + public DbSet ModelFamilies => Set(); + + /// + /// Collection of aircraft models. + /// + public DbSet PlaneModels => Set(); + + /// + /// Collection of flights. + /// + public DbSet Flights => Set(); + + /// + /// Collection of passengers. + /// + public DbSet Passengers => Set(); + + /// + /// Collection of tickets. + /// + public DbSet Tickets => Set(); + + /// + /// Configures entity-to-collection mappings and property names for MongoDB. + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Отключаем автоматические транзакции (MongoDB не поддерживает) + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + + // ModelFamily → collection "model_families" + modelBuilder.Entity(entity => + { + entity.ToCollection("model_families"); + entity.HasKey(f => f.Id); + entity.Property(f => f.Id).HasElementName("_id"); + entity.Property(f => f.NameOfFamily).HasElementName("family_name"); + entity.Property(f => f.ManufacturerName).HasElementName("manufacturer"); + }); + + // PlaneModel → collection "plane_models" + modelBuilder.Entity(entity => + { + entity.ToCollection("plane_models"); + entity.HasKey(m => m.Id); + entity.Property(m => m.Id).HasElementName("_id"); + entity.Property(m => m.ModelName).HasElementName("model_name"); + entity.Property(m => m.MaxRange).HasElementName("max_range_km"); + entity.Property(m => m.PassengerCapacity).HasElementName("passenger_capacity"); + entity.Property(m => m.CargoCapacity).HasElementName("cargo_capacity_tons"); + entity.Property(m => m.PlaneFamilyId).HasElementName("family_id"); // ← ссылка на ModelFamily + }); + + // Passenger → collection "passengers" + modelBuilder.Entity(entity => + { + entity.ToCollection("passengers"); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).HasElementName("_id"); + entity.Property(p => p.Passport).HasElementName("passport_number"); + entity.Property(p => p.PassengerName).HasElementName("full_name"); + entity.Property(p => p.DateOfBirth).HasElementName("date_of_birth"); + }); + + // Flight → collection "flights" + modelBuilder.Entity(entity => + { + entity.ToCollection("flights"); + entity.HasKey(f => f.Id); + entity.Property(f => f.Id).HasElementName("_id"); + entity.Property(f => f.FlightCode).HasElementName("flight_code"); + entity.Property(f => f.DepartureCity).HasElementName("departure_city"); + entity.Property(f => f.ArrivalCity).HasElementName("arrival_city"); + entity.Property(f => f.DepartureDateTime).HasElementName("departure_datetime"); + entity.Property(f => f.ArrivalDateTime).HasElementName("arrival_datetime"); + entity.Property(f => f.ModelId).HasElementName("plane_model_id"); // ← ссылка на PlaneModel + }); + + // Ticket → collection "tickets" + modelBuilder.Entity(entity => + { + entity.ToCollection("tickets"); + entity.HasKey(t => t.Id); + entity.Property(t => t.Id).HasElementName("_id"); + entity.Property(t => t.SeatNumber).HasElementName("seat_number"); + entity.Property(t => t.HandLuggage).HasElementName("has_hand_luggage"); + entity.Property(t => t.BaggageWeight).HasElementName("baggage_weight_kg"); + entity.Property(t => t.FlightId).HasElementName("flight_id"); + entity.Property(t => t.PassengerId).HasElementName("passenger_id"); + }); + } +} \ No newline at end of file diff --git a/Airline.Tests/Airline.Tests.csproj b/Airline.Tests/Airline.Tests.csproj index 12709c181..4adeea4ea 100644 --- a/Airline.Tests/Airline.Tests.csproj +++ b/Airline.Tests/Airline.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -16,8 +16,8 @@ - - - + + + \ No newline at end of file diff --git a/Airline.sln b/Airline.sln index 3e6aac49f..782ccedae 100644 --- a/Airline.sln +++ b/Airline.sln @@ -1,26 +1,50 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36603.0 d17.14 +VisualStudioVersion = 17.14.36603.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Domain", "Airline.Domain\Airline.Domain.csproj", "{38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Tests", "Airline.Tests\Airline.Tests.csproj", "{B813F501-EA93-4258-A2CA-A43542C8EEF4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Application.Contracts", "Airline.Application.Contracts\Airline.Application.Contracts.csproj", "{0C965A98-CE55-DB0F-63D6-281C96FF8704}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Infrastructure.EfCore", "Airline.Infrastructure.EfCore\Airline.Infrastructure.EfCore.csproj", "{E4208D1E-7EBE-1EF9-34F7-16DC641B398B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Application", "Airline.Application\Airline.Application.csproj", "{92A1A31C-40CB-802C-CCC3-246E0C50E4E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Api.Host", "Airline.Api.Host\Airline.Api.Host.csproj", "{2E9E28E0-D750-4996-A32D-84171942C3DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Domain", "Airline.Domain\Airline.Domain.csproj", "{1A3ED493-FDE5-C37C-498C-1431A58B395A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38BA7188-91A0-4C7F-89C8-3021DC8BCDA3}.Release|Any CPU.Build.0 = Release|Any CPU {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B813F501-EA93-4258-A2CA-A43542C8EEF4}.Release|Any CPU.Build.0 = Release|Any CPU + {0C965A98-CE55-DB0F-63D6-281C96FF8704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C965A98-CE55-DB0F-63D6-281C96FF8704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C965A98-CE55-DB0F-63D6-281C96FF8704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C965A98-CE55-DB0F-63D6-281C96FF8704}.Release|Any CPU.Build.0 = Release|Any CPU + {E4208D1E-7EBE-1EF9-34F7-16DC641B398B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4208D1E-7EBE-1EF9-34F7-16DC641B398B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4208D1E-7EBE-1EF9-34F7-16DC641B398B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4208D1E-7EBE-1EF9-34F7-16DC641B398B}.Release|Any CPU.Build.0 = Release|Any CPU + {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Release|Any CPU.Build.0 = Release|Any CPU + {2E9E28E0-D750-4996-A32D-84171942C3DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E9E28E0-D750-4996-A32D-84171942C3DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E9E28E0-D750-4996-A32D-84171942C3DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E9E28E0-D750-4996-A32D-84171942C3DE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 6ab0ddb7e0630ad723c34f5cb313930cdd4da133 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 27 Nov 2025 19:56:56 +0400 Subject: [PATCH 14/36] playing with repca --- Airline.Domain/Items/Flight.cs | 7 +++- Airline.Domain/Items/PlaneModel.cs | 7 +++- Airline.Domain/Items/Ticket.cs | 14 ++++++- Airline.Domain/Repository/IRepository.cs | 10 ++--- .../AirlineDbContext.cs | 4 +- .../Repositories/FlightRepository.cs | 38 +++++++++++++++++++ .../Repositories/ModelFamilyRepository.cs | 38 +++++++++++++++++++ .../Repositories/PassengerRepository.cs | 38 +++++++++++++++++++ .../Repositories/PlaneModelRepository.cs | 38 +++++++++++++++++++ .../Repositories/TicketRepository.cs | 38 +++++++++++++++++++ 10 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs create mode 100644 Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs create mode 100644 Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs create mode 100644 Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs create mode 100644 Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs index f494b608e..a7de132eb 100644 --- a/Airline.Domain/Items/Flight.cs +++ b/Airline.Domain/Items/Flight.cs @@ -43,5 +43,10 @@ public class Flight /// /// The model of plane. /// - public required PlaneModel Model { get; set; } + public required PlaneModel? Model { get; set; } + + /// + /// The id of the model. + /// + public string ModelId { get; set; } = string.Empty; // ← ссылка на PlaneModel } \ No newline at end of file diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs index 2a5e0a917..3a886b451 100644 --- a/Airline.Domain/Items/PlaneModel.cs +++ b/Airline.Domain/Items/PlaneModel.cs @@ -18,7 +18,7 @@ public class PlaneModel /// /// The model family of the plane. /// - public required ModelFamily PlaneFamily { get; set; } + public required ModelFamily? PlaneFamily { get; set; } /// /// The max flight range of the plane model. @@ -34,4 +34,9 @@ public class PlaneModel /// The cargo capacity of the plane model (tons). /// public required double CargoCapacity { get; set; } + + /// + /// The id of model family. + /// + public string PlaneFamilyId { get; set; } = string.Empty; // ← ссылка на ModelFamily } \ No newline at end of file diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs index 9404baa5f..2a425a731 100644 --- a/Airline.Domain/Items/Ticket.cs +++ b/Airline.Domain/Items/Ticket.cs @@ -13,12 +13,12 @@ public class Ticket /// /// The connection between the ticket and the flight. /// - public required Flight Flight { get; set; } + public required Flight? Flight { get; set; } /// /// The connection between the ticket and the passenger. /// - public required Passenger Passenger { get; set; } + public required Passenger? Passenger { get; set; } /// /// The passenger's seat number. @@ -34,4 +34,14 @@ public class Ticket /// Total baggage weight. (kilograms) /// public double? BaggageWeight { get; set; } + + /// + /// The id to connect between the ticket and the flight. + /// + public string FlightId { get; set; } = string.Empty; // ← ссылка на Flight + + /// + /// The id to connect between the ticket and the passenger. + /// + public string PassengerId { get; set; } = string.Empty; // ← ссылка на Passenger } \ No newline at end of file diff --git a/Airline.Domain/Repository/IRepository.cs b/Airline.Domain/Repository/IRepository.cs index d93a022c0..4aaec24ef 100644 --- a/Airline.Domain/Repository/IRepository.cs +++ b/Airline.Domain/Repository/IRepository.cs @@ -18,7 +18,7 @@ public interface IRepository /// /// The entity instance to add. /// The created entity. - Task CreateAsync(TEntity entity); + public Task CreateAsync(TEntity entity); /// /// Retrieves an entity from the repository by its identifier. @@ -28,7 +28,7 @@ public interface IRepository /// The entity with the specified identifier, or /// if no entity with such an identifier exists. /// - Task GetAsync(TKey id); + public Task GetAsync(TKey id); /// /// Retrieves all entities stored in the repository. @@ -36,19 +36,19 @@ public interface IRepository /// /// A list containing all entities in the repository. /// - Task> GetAllAsync(); + public Task> GetAllAsync(); /// /// Updates an existing entity in the repository. /// /// The entity instance containing updated data. /// The updated entity. - Task UpdateAsync(TEntity entity); + public Task UpdateAsync(TEntity entity); /// /// Removes an entity from the repository by its identifier. /// /// The unique identifier of the entity to delete. /// if the entity was deleted; otherwise, . - Task DeleteAsync(TKey id); + public Task DeleteAsync(TKey id); } \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs index d5eed669c..4d498411f 100644 --- a/Airline.Infrastructure.EfCore/AirlineDbContext.cs +++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs @@ -1,8 +1,6 @@ using Airline.Domain.Items; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Net.Sockets; -using System.Reflection.Emit; +using MongoDB.EntityFrameworkCore.Extensions; namespace Airline.Infrastructure.EfCore; diff --git a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs new file mode 100644 index 000000000..b5f87a6ff --- /dev/null +++ b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs @@ -0,0 +1,38 @@ +using Airline.Domain.Repository; +using Airline.Domain.Items; +using Microsoft.EntityFrameworkCore; + +namespace Airline.Infrastructure.EfCore.Repositories; + +public class FlightRepository(AirlineDbContext context) : IRepository +{ + public async Task CreateAsync(Flight entity) + { + var entry = await context.Flights.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task DeleteAsync(string id) + { + var entity = await context.Flights.FindAsync(id); + if (entity == null) return false; + + context.Flights.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetAsync(string id) => + await context.Flights.FindAsync(id); + + public async Task> GetAllAsync() => + await context.Flights.ToListAsync(); + + public async Task UpdateAsync(Flight entity) + { + context.Flights.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs new file mode 100644 index 000000000..09066de3d --- /dev/null +++ b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs @@ -0,0 +1,38 @@ +using Airline.Domain.Items; +using Airline.Domain.Repository; +using Microsoft.EntityFrameworkCore; + +namespace Airline.Infrastructure.EfCore.Repositories; + +public class ModelFamilyRepository(AirlineDbContext context) : IRepository +{ + public async Task CreateAsync(ModelFamily entity) + { + var entry = await context.ModelFamilies.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task DeleteAsync(string id) + { + var entity = await context.ModelFamilies.FindAsync(id); + if (entity == null) return false; + + context.ModelFamilies.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetAsync(string id) => + await context.ModelFamilies.FindAsync(id); + + public async Task> GetAllAsync() => + await context.ModelFamilies.ToListAsync(); + + public async Task UpdateAsync(ModelFamily entity) + { + context.ModelFamilies.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs new file mode 100644 index 000000000..32bf3fdfc --- /dev/null +++ b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs @@ -0,0 +1,38 @@ +using Airline.Domain.Repository; +using Airline.Domain.Items; +using Microsoft.EntityFrameworkCore; + +namespace Airline.Infrastructure.EfCore.Repositories; + +public class PassengerRepository(AirlineDbContext context) : IRepository +{ + public async Task CreateAsync(Passenger entity) + { + var entry = await context.Passengers.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task DeleteAsync(string id) + { + var entity = await context.Passengers.FindAsync(id); + if (entity == null) return false; + + context.Passengers.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetAsync(string id) => + await context.Passengers.FindAsync(id); + + public async Task> GetAllAsync() => + await context.Passengers.ToListAsync(); + + public async Task UpdateAsync(Passenger entity) + { + context.Passengers.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs new file mode 100644 index 000000000..172203aa2 --- /dev/null +++ b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs @@ -0,0 +1,38 @@ +using Airline.Domain.Items; +using Airline.Domain.Repository; +using Microsoft.EntityFrameworkCore; + +namespace Airline.Infrastructure.EfCore.Repositories; + +public class PlaneModelRepository(AirlineDbContext context) : IRepository +{ + public async Task CreateAsync(PlaneModel entity) + { + var entry = await context.PlaneModels.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task DeleteAsync(string id) + { + var entity = await context.PlaneModels.FindAsync(id); + if (entity == null) return false; + + context.PlaneModels.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetAsync(string id) => + await context.PlaneModels.FindAsync(id); + + public async Task> GetAllAsync() => + await context.PlaneModels.ToListAsync(); + + public async Task UpdateAsync(PlaneModel entity) + { + context.PlaneModels.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs new file mode 100644 index 000000000..d532074a5 --- /dev/null +++ b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs @@ -0,0 +1,38 @@ +using Airline.Domain.Repository; +using Airline.Domain.Items; +using Microsoft.EntityFrameworkCore; + +namespace Airline.Infrastructure.EfCore.Repositories; + +public class TicketRepository(AirlineDbContext context) : IRepository +{ + public async Task CreateAsync(Ticket entity) + { + var entry = await context.Tickets.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task DeleteAsync(string id) + { + var entity = await context.Tickets.FindAsync(id); + if (entity == null) return false; + + context.Tickets.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetAsync(string id) => + await context.Tickets.FindAsync(id); + + public async Task> GetAllAsync() => + await context.Tickets.ToListAsync(); + + public async Task UpdateAsync(Ticket entity) + { + context.Tickets.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} \ No newline at end of file From 349449a6e3633c9744d35d32795983ff087f8aed Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 9 Dec 2025 04:56:03 +0400 Subject: [PATCH 15/36] near to the end --- Airline.Api.Host/Airline.Api.Host.csproj | 13 -- Airline.Api.Host/Airline.Api.Host.http | 6 - .../Controllers/WeatherForecastController.cs | 31 ---- Airline.Api.Host/Program.cs | 25 --- .../Properties/launchSettings.json | 41 ----- Airline.Api.Host/WeatherForecast.cs | 12 -- Airline.Api.Host/appsettings.Development.json | 8 - Airline.Api.Host/appsettings.json | 9 -- .../Flight/IFlightService.cs | 10 +- .../IAnalyticService.cs | 6 +- .../IApplicationService.cs | 13 ++ Airline.Application/AirlineMapperProfile.cs | 33 ++++ .../Services/AnalyticService.cs | 55 +++++++ Airline.Application/Services/FlightService.cs | 146 +++++------------- .../Services/ModelFamilyService.cs | 19 +++ .../Services/PassengerService.cs | 15 ++ .../Services/PlaneModelService.cs | 15 ++ Airline.Application/Services/TicketService.cs | 15 ++ Airline.Domain/IRepository.cs | 54 +++++++ .../AirlineDbContext.cs | 12 +- 20 files changed, 275 insertions(+), 263 deletions(-) delete mode 100644 Airline.Api.Host/Airline.Api.Host.csproj delete mode 100644 Airline.Api.Host/Airline.Api.Host.http delete mode 100644 Airline.Api.Host/Controllers/WeatherForecastController.cs delete mode 100644 Airline.Api.Host/Program.cs delete mode 100644 Airline.Api.Host/Properties/launchSettings.json delete mode 100644 Airline.Api.Host/WeatherForecast.cs delete mode 100644 Airline.Api.Host/appsettings.Development.json delete mode 100644 Airline.Api.Host/appsettings.json create mode 100644 Airline.Application.Contracts/IApplicationService.cs create mode 100644 Airline.Application/AirlineMapperProfile.cs create mode 100644 Airline.Application/Services/AnalyticService.cs create mode 100644 Airline.Application/Services/ModelFamilyService.cs create mode 100644 Airline.Application/Services/PassengerService.cs create mode 100644 Airline.Application/Services/PlaneModelService.cs create mode 100644 Airline.Application/Services/TicketService.cs create mode 100644 Airline.Domain/IRepository.cs diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj deleted file mode 100644 index 5419ef0c2..000000000 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/Airline.Api.Host/Airline.Api.Host.http b/Airline.Api.Host/Airline.Api.Host.http deleted file mode 100644 index 15bf8ba7b..000000000 --- a/Airline.Api.Host/Airline.Api.Host.http +++ /dev/null @@ -1,6 +0,0 @@ -@Airline.Api.Host_HostAddress = http://localhost:5264 - -GET {{Airline.Api.Host_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/Airline.Api.Host/Controllers/WeatherForecastController.cs b/Airline.Api.Host/Controllers/WeatherForecastController.cs deleted file mode 100644 index 61192a3bb..000000000 --- a/Airline.Api.Host/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Airline.Api.Host.Controllers; -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs deleted file mode 100644 index 48863a6d6..000000000 --- a/Airline.Api.Host/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -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(); diff --git a/Airline.Api.Host/Properties/launchSettings.json b/Airline.Api.Host/Properties/launchSettings.json deleted file mode 100644 index 90f8fd242..000000000 --- a/Airline.Api.Host/Properties/launchSettings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:34305", - "sslPort": 44395 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5264", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7171;http://localhost:5264", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Airline.Api.Host/WeatherForecast.cs b/Airline.Api.Host/WeatherForecast.cs deleted file mode 100644 index e6ebe52c5..000000000 --- a/Airline.Api.Host/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Airline.Api.Host; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/Airline.Api.Host/appsettings.Development.json b/Airline.Api.Host/appsettings.Development.json deleted file mode 100644 index 0c208ae91..000000000 --- a/Airline.Api.Host/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/Airline.Api.Host/appsettings.json b/Airline.Api.Host/appsettings.json deleted file mode 100644 index 10f68b8c8..000000000 --- a/Airline.Api.Host/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Airline.Application.Contracts/Flight/IFlightService.cs b/Airline.Application.Contracts/Flight/IFlightService.cs index d40816b64..e433ec6e5 100644 --- a/Airline.Application.Contracts/Flight/IFlightService.cs +++ b/Airline.Application.Contracts/Flight/IFlightService.cs @@ -2,9 +2,9 @@ namespace Airline.Application.Contracts.Flight; public interface IFlightService { - public Task> GetAllAsync(); - public Task GetByIdAsync(string id); - public Task CreateAsync(FlightDto flight); - public Task UpdateAsync(FlightDto flight); - public Task DeleteAsync(string id); + public Task CreateAsync(CreateFlightDto dto); + public Task GetByIdAsync(string id); + public Task> GetAllAsync(); + public Task UpdateAsync(CreateFlightDto dto); + public Task DeleteAsync(string id); } \ No newline at end of file diff --git a/Airline.Application.Contracts/IAnalyticService.cs b/Airline.Application.Contracts/IAnalyticService.cs index f5b4cbcf7..ab3f4649b 100644 --- a/Airline.Application.Contracts/IAnalyticService.cs +++ b/Airline.Application.Contracts/IAnalyticService.cs @@ -1,11 +1,11 @@ -namespace Airline.Application.Contracts.Flight; -namespace Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; public interface IAnalyticsService { public Task> GetTopFlightsByPassengerCountAsync(int top = 5); public Task> GetFlightsWithMinTravelTimeAsync(); public Task> GetPassengersWithZeroBaggageOnFlightAsync(string flightId); - public Task> GetFlightsByModelInPeriodAsync(string modelName, DateTime from, DateTime to); + public Task> GetFlightsByModelInPeriodAsync(string modelId, DateTime from, DateTime to); public Task> GetFlightsByRouteAsync(string departure, string arrival); } \ No newline at end of file diff --git a/Airline.Application.Contracts/IApplicationService.cs b/Airline.Application.Contracts/IApplicationService.cs new file mode 100644 index 000000000..f935bb327 --- /dev/null +++ b/Airline.Application.Contracts/IApplicationService.cs @@ -0,0 +1,13 @@ +namespace Airline.Application.Contracts; + +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + public Task Create(TCreateUpdateDto dto); + public Task Get(TKey dtoId); + public Task> GetAll(); + public Task Update(TCreateUpdateDto dto, TKey dtoId); + public Task Delete(TKey dtoId); +} \ No newline at end of file diff --git a/Airline.Application/AirlineMapperProfile.cs b/Airline.Application/AirlineMapperProfile.cs new file mode 100644 index 000000000..ea77c9d09 --- /dev/null +++ b/Airline.Application/AirlineMapperProfile.cs @@ -0,0 +1,33 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.PlaneModel; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application; + +public class AirlineProfile : Profile +{ + /// + /// Конструктор профиля, создающий связи между Entity и Dto классами + /// + public AirlineProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Airline.Application/Services/AnalyticService.cs b/Airline.Application/Services/AnalyticService.cs new file mode 100644 index 000000000..c6f6bd56e --- /dev/null +++ b/Airline.Application/Services/AnalyticService.cs @@ -0,0 +1,55 @@ +using Airline.Application.Contracts.PlaneModel; +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Airline.Domain; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application.Services; + +public class AnalyticsService( + IRepository flightRepository, + IRepository passengerRepository, + IRepository ticketRepository, + IMapper mapper +) : IAnalyticsService +{ + public async Task> GetTopFlightsByPassengerCountAsync(int top = 5) + { + var flights = await flightRepository.GetAllAsync(); + return flights.Take(top).Select(f => mapper.Map(f)).ToList(); + } + + public async Task> GetFlightsWithMinTravelTimeAsync() + { + var flights = await flightRepository.GetAllAsync(); + var minTime = flights.Min(f => f.ArrivalDateTime - f.DepartureDateTime); + return flights + .Where(f => f.ArrivalDateTime - f.DepartureDateTime == minTime) + .Select(f => mapper.Map(f)) + .ToList(); + } + + public async Task> GetFlightsByRouteAsync(string departure, string arrival) + { + var flights = await flightRepository.GetAllAsync(); + return flights + .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) + .Select(f => mapper.Map(f)) + .ToList(); + } + + public async Task> GetFlightsOfModelWithinPeriod(string planeModelId, DateTime from, DateTime to) + { + var flights = await flightRepository.GetAllAsync(); + + var result = flights + .Where(f => f.ModelId == planeModelId + && f.DepartureDateTime >= from + && f.DepartureDateTime <= to) + .ToList(); + + return mapper.Map>(result); + } +} diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs index 23bb8e0d9..c6eb24dde 100644 --- a/Airline.Application/Services/FlightService.cs +++ b/Airline.Application/Services/FlightService.cs @@ -1,125 +1,63 @@ +using Airline.Application.Contracts.PlaneModel; using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Airline.Domain; using Airline.Domain.Items; -using Airline.Domain.Repositories; -using Microsoft.Extensions.Logging; +using AutoMapper; -namespace Airline.Application.Service; +namespace Airline.Application.Services; -/// -/// Provides operations for managing flights in the airline system. -/// -/// Repository for accessing flight data. -/// Repository for validating and accessing aircraft model data. public class FlightService( - IRepository flightRepository, - IRepository planeModelRepository + IRepository flightRepository, + IRepository planeModelRepository, + IRepository ticketRepository, + IRepository passengerRepository, + IMapper mapper ) : IFlightService + { - /// - /// Creates a new flight. - /// - /// The flight information. - /// The created flight. - /// - /// Thrown if the specified aircraft model does not exist. - /// - public async Task CreateAsync(Flight entity) + public async Task CreateAsync(CreateFlightDto dto) { - // , - if (await planeModelRepository.GetAsync(entity.ModelId) is null) - { - throw new KeyNotFoundException($"Aircraft model with Id = {entity.ModelId} does not exist."); - } - - return await flightRepository.CreateAsync(entity); - } - - /// - /// Retrieves all flights from the system. - /// - /// A list of all flights. - public async Task> GetAllAsync() => - await flightRepository.GetAllAsync(); + // Проверяем, существует ли модель + if (await planeModelRepository.GetAsync(dto.ModelId) is null) + throw new KeyNotFoundException($"Aircraft model with Id = {dto.ModelId} does not exist."); - /// - /// Retrieves a specific flight by its unique identifier. - /// - /// The unique identifier of the flight. - /// The flight if found; otherwise, null. - public async Task GetByIdAsync(string id) => - await flightRepository.GetAsync(id); + // Преобразуем DTO → Domain + var flight = mapper.Map(dto); - /// - /// Updates an existing flight. - /// - /// The updated flight information. - /// The updated flight. - public async Task UpdateAsync(Flight entity) => - await flightRepository.UpdateAsync(entity); - - /// - /// Deletes a flight from the system. - /// - /// The unique identifier of the flight to delete. - /// True if deletion was successful; otherwise, false. - public async Task DeleteAsync(string id) => - await flightRepository.DeleteAsync(id); + // Сохраняем + var created = await flightRepository.CreateAsync(flight); - // ---------- ( -) ---------- + // Возвращаем Domain → DTO + return mapper.Map(created); + } - /// - /// Retrieves the top N flights by number of passengers. - /// - /// Number of top flights to return. - /// List of top flights. - public async Task> GetTopFlightsByPassengerCountAsync(int top = 5) + public async Task GetByIdAsync(string id) { - var allFlights = await flightRepository.GetAllAsync(); - // JOIN - // ( ) - return allFlights.Take(top).ToList(); + var flight = await flightRepository.GetAsync(id); + return flight is null ? null : mapper.Map(flight); } - /// - /// Retrieves flights with the minimal travel time. - /// - /// List of flights with minimal travel time. - public async Task> GetFlightsWithMinTravelTimeAsync() + public async Task> GetAllAsync() { - var allFlights = await flightRepository.GetAllAsync(); - var minTime = allFlights.Min(f => f.ArrivalDateTime - f.DepartureDateTime); - return allFlights - .Where(f => f.ArrivalDateTime - f.DepartureDateTime == minTime) - .ToList(); + var flights = await flightRepository.GetAllAsync(); + return flights.Select(f => mapper.Map(f)).ToList(); } - /// - /// Retrieves flights for a specific route. - /// - /// Departure city. - /// Arrival city. - /// List of flights matching the route. - public async Task> GetFlightsByRouteAsync(string departure, string arrival) => - (await flightRepository.GetAllAsync()) - .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) - .ToList(); - - /// - /// Retrieves flights of a specific aircraft model within a date period. - /// - /// Name of the aircraft model. - /// Start date (inclusive). - /// End date (inclusive). - /// List of matching flights. - public async Task> GetFlightsByModelInPeriodAsync(string modelName, DateTime from, DateTime to) + public async Task UpdateAsync(CreateFlightDto dto, int Id) { - var allFlights = await flightRepository.GetAllAsync(); - // JOIN PlaneModel - // , modelName Flight - return allFlights - .Where(f => f.ModelName == modelName && - f.DepartureDateTime >= from && - f.DepartureDateTime <= to) - .ToList(); + var existing = await flightRepository.GetAsync(Id); + if (existing is null) + throw new KeyNotFoundException($"Flight with Id = {Id} does not exist."); + + // Маппим обновлённые данные (можно использовать AutoMapper.UpdateFrom) + mapper.Map(dto, existing); + + var updated = await flightRepository.UpdateAsync(existing); + return mapper.Map(updated); } + + public async Task DeleteAsync(string id) => + await flightRepository.DeleteAsync(id); } \ No newline at end of file diff --git a/Airline.Application/Services/ModelFamilyService.cs b/Airline.Application/Services/ModelFamilyService.cs new file mode 100644 index 000000000..ffc3a6acc --- /dev/null +++ b/Airline.Application/Services/ModelFamilyService.cs @@ -0,0 +1,19 @@ +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.PlaneModel; +using Airline.Domain; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application.Services; + +/// +/// Сервис для управления семействами самолётов и получения связанных моделей +/// +/// Репозиторий для операций с сущностями семейства самолётов +/// Репозиторий для операций с сущностями моделей самолётов +/// Маппер для преобразования доменных моделей в DTO и обратно +public class AircraftFamilyService( + IRepository familyRepository, + IRepository modelRepository, + IMapper mapper +) : IModelFamilyService \ No newline at end of file diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs new file mode 100644 index 000000000..4631e8431 --- /dev/null +++ b/Airline.Application/Services/PassengerService.cs @@ -0,0 +1,15 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Airline.Domain; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application.Services; + +public class PassengerService( + IRepository passengerRepository, + IRepository ticketRepository, + IRepository flightRepository, + IMapper mapper +) : IPassengerService \ No newline at end of file diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs new file mode 100644 index 000000000..33b5c9735 --- /dev/null +++ b/Airline.Application/Services/PlaneModelService.cs @@ -0,0 +1,15 @@ +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.PlaneModel; +using Airline.Application.Contracts.Flight; +using Airline.Domain; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application.Services; + +public class AircraftModelService( + IRepository modelRepository, + IRepository familyRepository, + IRepository flightRepository, + IMapper mapper +) : IPlaneModelService \ No newline at end of file diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs new file mode 100644 index 000000000..c5169c3fa --- /dev/null +++ b/Airline.Application/Services/TicketService.cs @@ -0,0 +1,15 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Airline.Domain; +using Airline.Domain.Items; +using AutoMapper; + +namespace Airline.Application.Services; + +public class TicketService( + IRepository ticketRepository, + IRepository flightRepository, + IRepository passengerRepository, + IMapper mapper +) : ITicketService \ No newline at end of file diff --git a/Airline.Domain/IRepository.cs b/Airline.Domain/IRepository.cs new file mode 100644 index 000000000..a78822266 --- /dev/null +++ b/Airline.Domain/IRepository.cs @@ -0,0 +1,54 @@ +namespace Airline.Domain; + +/// +/// Defines a generic repository interface that provides +/// basic CRUD (Create, Read, Update, Delete) operations. +/// +/// +/// The type of the entity being managed by the repository. +/// +/// +/// The type of the entity's unique identifier (e.g., for MongoDB). +/// +public interface IRepository + where TEntity : class +{ + /// + /// Adds a new entity to the repository. + /// + /// The entity instance to add. + /// The created entity. + public Task CreateAsync(TEntity entity); + + /// + /// Retrieves an entity from the repository by its identifier. + /// + /// The unique identifier of the entity. + /// + /// The entity with the specified identifier, or + /// if no entity with such an identifier exists. + /// + public Task GetAsync(TKey id); + + /// + /// Retrieves all entities stored in the repository. + /// + /// + /// A list containing all entities in the repository. + /// + public Task> GetAllAsync(); + + /// + /// Updates an existing entity in the repository. + /// + /// The entity instance containing updated data. + /// The updated entity. + public Task UpdateAsync(TEntity entity); + + /// + /// Removes an entity from the repository by its identifier. + /// + /// The unique identifier of the entity to delete. + /// if the entity was deleted; otherwise, . + public Task DeleteAsync(TKey id); +} \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs index 4d498411f..a6a143223 100644 --- a/Airline.Infrastructure.EfCore/AirlineDbContext.cs +++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs @@ -13,34 +13,34 @@ public class AirlineDbContext(DbContextOptions options) : DbCo /// /// Collection of aircraft model families. /// - public DbSet ModelFamilies => Set(); + public DbSet ModelFamilies { get; set; } /// /// Collection of aircraft models. /// - public DbSet PlaneModels => Set(); + public DbSet PlaneModels { get; set; } /// /// Collection of flights. /// - public DbSet Flights => Set(); + public DbSet Flights { get; set; } /// /// Collection of passengers. /// - public DbSet Passengers => Set(); + public DbSet Passengers { get; set; } /// /// Collection of tickets. /// - public DbSet Tickets => Set(); + public DbSet Tickets { get; set; } /// /// Configures entity-to-collection mappings and property names for MongoDB. /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - // Отключаем автоматические транзакции (MongoDB не поддерживает) + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; // ModelFamily → collection "model_families" From 64252561a99fbc619377290176f03202c24c4eb4 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 10 Dec 2025 03:52:33 +0400 Subject: [PATCH 16/36] services are done --- .../Flight/CreateFlightDto.cs | 2 +- .../Flight/FlightDto.cs | 4 +- .../Flight/IFlightService.cs | 17 ++-- .../IAnalyticService.cs | 4 +- .../IApplicationService.cs | 11 +-- .../ModelFamily/IModelFamilyService.cs | 12 ++- .../ModelFamily/ModelFamilyDto.cs | 2 +- .../Passenger/IPassengerService.cs | 12 +-- .../Passenger/PassengerDto.cs | 2 +- .../PlaneModel/CreatePlaneModelDto.cs | 4 +- .../PlaneModel/IPlaneModelService.cs | 8 ++ .../PlaneModel/PlaneModelDto.cs | 6 +- .../Ticket/CreateTicketDto.cs | 10 ++- .../Ticket/ITicketService.cs | 14 ++-- .../Ticket/TicketDto.cs | 6 +- .../Airline.Application.csproj | 9 +++ .../Services/AnalyticService.cs | 81 ++++++++++++++----- Airline.Application/Services/FlightService.cs | 67 ++++++++------- .../Services/ModelFamilyService.cs | 55 ++++++++++--- .../Services/PassengerService.cs | 46 ++++++++++- .../Services/PlaneModelService.cs | 48 +++++++++-- Airline.Application/Services/TicketService.cs | 54 ++++++++++++- Airline.Domain/Items/Flight.cs | 2 +- Airline.Domain/Items/PlaneModel.cs | 2 +- Airline.Domain/Items/Ticket.cs | 4 +- 25 files changed, 364 insertions(+), 118 deletions(-) create mode 100644 Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs diff --git a/Airline.Application.Contracts/Flight/CreateFlightDto.cs b/Airline.Application.Contracts/Flight/CreateFlightDto.cs index 7e8cc8337..55772b62d 100644 --- a/Airline.Application.Contracts/Flight/CreateFlightDto.cs +++ b/Airline.Application.Contracts/Flight/CreateFlightDto.cs @@ -15,4 +15,4 @@ public record CreateFlightDto( string ArrivalCity, DateTime DepartureDateTime, DateTime ArrivalDateTime, - string ModelId); \ No newline at end of file + int ModelId); \ No newline at end of file diff --git a/Airline.Application.Contracts/Flight/FlightDto.cs b/Airline.Application.Contracts/Flight/FlightDto.cs index dea2c8d3e..f55b6430f 100644 --- a/Airline.Application.Contracts/Flight/FlightDto.cs +++ b/Airline.Application.Contracts/Flight/FlightDto.cs @@ -11,10 +11,10 @@ namespace Airline.Application.Contracts.Flight; /// Date and time of arrival. /// ID of the plane model used for the flight. public record FlightDto( - string Id, + int Id, string FlightCode, string DepartureCity, string ArrivalCity, DateTime DepartureDateTime, DateTime ArrivalDateTime, - string ModelId); \ No newline at end of file + int ModelId); \ No newline at end of file diff --git a/Airline.Application.Contracts/Flight/IFlightService.cs b/Airline.Application.Contracts/Flight/IFlightService.cs index e433ec6e5..6634af432 100644 --- a/Airline.Application.Contracts/Flight/IFlightService.cs +++ b/Airline.Application.Contracts/Flight/IFlightService.cs @@ -1,10 +1,13 @@ +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.PlaneModel; +using Airline.Application.Contracts.Ticket; + namespace Airline.Application.Contracts.Flight; -public interface IFlightService +public interface IFlightService : IApplicationService { - public Task CreateAsync(CreateFlightDto dto); - public Task GetByIdAsync(string id); - public Task> GetAllAsync(); - public Task UpdateAsync(CreateFlightDto dto); - public Task DeleteAsync(string id); -} \ No newline at end of file + + public Task GetPlaneModelAsync(int flightId); + public Task> GetTicketsAsync(int flightId); + public Task> GetPassengersAsync(int flightId); +} diff --git a/Airline.Application.Contracts/IAnalyticService.cs b/Airline.Application.Contracts/IAnalyticService.cs index ab3f4649b..ac966a6c3 100644 --- a/Airline.Application.Contracts/IAnalyticService.cs +++ b/Airline.Application.Contracts/IAnalyticService.cs @@ -5,7 +5,7 @@ public interface IAnalyticsService { public Task> GetTopFlightsByPassengerCountAsync(int top = 5); public Task> GetFlightsWithMinTravelTimeAsync(); - public Task> GetPassengersWithZeroBaggageOnFlightAsync(string flightId); - public Task> GetFlightsByModelInPeriodAsync(string modelId, DateTime from, DateTime to); + public Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId); + public Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to); public Task> GetFlightsByRouteAsync(string departure, string arrival); } \ No newline at end of file diff --git a/Airline.Application.Contracts/IApplicationService.cs b/Airline.Application.Contracts/IApplicationService.cs index f935bb327..c42571621 100644 --- a/Airline.Application.Contracts/IApplicationService.cs +++ b/Airline.Application.Contracts/IApplicationService.cs @@ -5,9 +5,10 @@ public interface IApplicationService where TCreateUpdateDto : class where TKey : struct { - public Task Create(TCreateUpdateDto dto); - public Task Get(TKey dtoId); - public Task> GetAll(); - public Task Update(TCreateUpdateDto dto, TKey dtoId); - public Task Delete(TKey dtoId); + public Task CreateAsync(TCreateUpdateDto dto); + public Task GetByIdAsync(TKey id); + public Task> GetAllAsync(); + public Task UpdateAsync(TCreateUpdateDto dto, TKey id); + public Task DeleteAsync(TKey id); + } \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs index 0c29aa77a..6a38d68ac 100644 --- a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs +++ b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs @@ -1,10 +1,8 @@ +using Airline.Application.Contracts.PlaneModel; + namespace Airline.Application.Contracts.ModelFamily; -public interface IModelFamilyService +public interface IModelFamilyService : IApplicationService { - public Task> GetAllAsync(); - public Task GetByIdAsync(string id); - public Task CreateAsync(ModelFamilyDto family); - public Task UpdateAsync(ModelFamilyDto family); - public Task DeleteAsync(string id); -} \ No newline at end of file + public Task> GetPlaneModelsAsync(int familyId); +} diff --git a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs index e54894ade..010eb3d28 100644 --- a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs +++ b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs @@ -7,4 +7,4 @@ namespace Airline.Application.Contracts.ModelFamily; /// Unique identifier of the model family. /// Name of the model family. /// Manufacturer of the model family. -public record ModelFamilyDto(string Id, string NameOfFamily, string ManufacturerName); \ No newline at end of file +public record ModelFamilyDto(int Id, string NameOfFamily, string ManufacturerName); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/IPassengerService.cs b/Airline.Application.Contracts/Passenger/IPassengerService.cs index 823d1f4d1..e2553f44c 100644 --- a/Airline.Application.Contracts/Passenger/IPassengerService.cs +++ b/Airline.Application.Contracts/Passenger/IPassengerService.cs @@ -1,10 +1,10 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Ticket; + namespace Airline.Application.Contracts.Passenger; -public interface IPassengerService +public interface IPassengerService : IApplicationService { - public Task> GetAllAsync(); - public Task GetByIdAsync(string id); - public Task CreateAsync(PassengerDto passenger); - public Task UpdateAsync(PassengerDto passenger); - public Task DeleteAsync(string id); + public Task> GetTicketsAsync(int passengerId); + public Task> GetFlightsAsync(int passengerId); } \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/PassengerDto.cs b/Airline.Application.Contracts/Passenger/PassengerDto.cs index f8acedfa3..a18429af7 100644 --- a/Airline.Application.Contracts/Passenger/PassengerDto.cs +++ b/Airline.Application.Contracts/Passenger/PassengerDto.cs @@ -7,4 +7,4 @@ namespace Airline.Application.Contracts.Passenger; /// Passport number. /// Full name of the passenger. /// Date of birth (YYYY-MM-DD). -public record PassengerDto(string Id, string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file +public record PassengerDto(int Id, string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs index 6b99f5d1c..f0df96816 100644 --- a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs +++ b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs @@ -4,13 +4,13 @@ namespace Airline.Application.Contracts.PlaneModel; /// DTO for creating a new plane model. /// /// Name of the plane model. -/// ID of the associated model family. +/// ID of the associated model family. /// Maximum flight range (km). /// Passenger capacity. /// Cargo capacity (tons). public record CreatePlaneModelDto( string ModelName, - string PlaneFamilyId, + int ModelFamilyId, double MaxRange, double PassengerCapacity, double CargoCapacity); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs new file mode 100644 index 000000000..73150f6bb --- /dev/null +++ b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs @@ -0,0 +1,8 @@ +using Airline.Application.Contracts.ModelFamily; + +namespace Airline.Application.Contracts.PlaneModel; + +public interface IPlaneModelService : IApplicationService +{ + public Task GetModelFamilyAsync(int modelId); +} diff --git a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs index 648912200..b1592ec6e 100644 --- a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs +++ b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs @@ -5,14 +5,14 @@ namespace Airline.Application.Contracts.PlaneModel; /// /// Unique identifier of the plane model. /// Name of the plane model. -/// ID of the associated model family. +/// ID of the associated model family. /// Maximum flight range (km). /// Passenger capacity. /// Cargo capacity (tons). public record PlaneModelDto( - string Id, + int Id, string ModelName, - string PlaneFamilyId, + int ModelFamilyId, double MaxRange, double PassengerCapacity, double CargoCapacity); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs index eba4e1638..d2ebe2e9c 100644 --- a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs +++ b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs @@ -8,9 +8,11 @@ namespace Airline.Application.Contracts.Ticket; /// Seat number (e.g., 12A). /// Indicates if hand luggage is present. /// Total baggage weight in kilograms (null if no baggage). -public record CreateTicketDto( - string FlightId, - string PassengerId, +public record CreateTicketDto +( + int FlightId, + int PassengerId, string SeatNumber, bool HandLuggage, - double? BaggageWeight); \ No newline at end of file + double? BaggageWeight + ); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/ITicketService.cs b/Airline.Application.Contracts/Ticket/ITicketService.cs index c5841a633..e52fe3079 100644 --- a/Airline.Application.Contracts/Ticket/ITicketService.cs +++ b/Airline.Application.Contracts/Ticket/ITicketService.cs @@ -1,10 +1,10 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; + namespace Airline.Application.Contracts.Ticket; -public interface ITicketService +public interface ITicketService : IApplicationService { - public Task> GetAllAsync(); - public Task GetByIdAsync(string id); - public Task CreateAsync(TicketDto ticket); - public Task UpdateAsync(TicketDto ticket); - public Task DeleteAsync(string id); -} \ No newline at end of file + public Task GetFlightAsync(int ticketId); + public Task GetPassengerAsync(int ticketId); +} diff --git a/Airline.Application.Contracts/Ticket/TicketDto.cs b/Airline.Application.Contracts/Ticket/TicketDto.cs index c2c0263a2..c5d413b3e 100644 --- a/Airline.Application.Contracts/Ticket/TicketDto.cs +++ b/Airline.Application.Contracts/Ticket/TicketDto.cs @@ -10,9 +10,9 @@ namespace Airline.Application.Contracts.Ticket; /// Indicates if hand luggage is present. /// Total baggage weight in kilograms (null if no baggage). public record TicketDto( - string Id, - string FlightId, - string PassengerId, + int Id, + int FlightId, + int PassengerId, string SeatNumber, bool HandLuggage, double? BaggageWeight); \ No newline at end of file diff --git a/Airline.Application/Airline.Application.csproj b/Airline.Application/Airline.Application.csproj index fa71b7ae6..8e1d69d8a 100644 --- a/Airline.Application/Airline.Application.csproj +++ b/Airline.Application/Airline.Application.csproj @@ -6,4 +6,13 @@ enable + + + + + + + + + diff --git a/Airline.Application/Services/AnalyticService.cs b/Airline.Application/Services/AnalyticService.cs index c6f6bd56e..efcfd9ce9 100644 --- a/Airline.Application/Services/AnalyticService.cs +++ b/Airline.Application/Services/AnalyticService.cs @@ -1,7 +1,5 @@ -using Airline.Application.Contracts.PlaneModel; -using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.Passenger; -using Airline.Application.Contracts.Ticket; using Airline.Domain; using Airline.Domain.Items; using AutoMapper; @@ -10,46 +8,93 @@ namespace Airline.Application.Services; public class AnalyticsService( IRepository flightRepository, - IRepository passengerRepository, IRepository ticketRepository, + IRepository passengerRepository, IMapper mapper ) : IAnalyticsService { public async Task> GetTopFlightsByPassengerCountAsync(int top = 5) { var flights = await flightRepository.GetAllAsync(); - return flights.Take(top).Select(f => mapper.Map(f)).ToList(); + var tickets = await ticketRepository.GetAllAsync(); + + // Группируем билеты по FlightId и считаем количество + var flightPassengerCounts = tickets + .GroupBy(t => t.FlightId) + .Select(g => new { FlightId = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(top) + .Select(x => x.FlightId) + .ToHashSet(); + + // Получаем полные объекты рейсов + var topFlights = flights + .Where(f => flightPassengerCounts.Contains(f.Id)) + .ToList(); + + // Маппим в DTO и сортируем по убыванию числа пассажиров + var flightCountMap = tickets + .GroupBy(t => t.FlightId) + .ToDictionary(g => g.Key, g => g.Count()); + + return topFlights + .OrderByDescending(f => flightCountMap.GetValueOrDefault(f.Id, 0)) + .Select(mapper.Map) + .ToList(); } public async Task> GetFlightsWithMinTravelTimeAsync() { var flights = await flightRepository.GetAllAsync(); + if (!flights.Any()) return []; + var minTime = flights.Min(f => f.ArrivalDateTime - f.DepartureDateTime); return flights .Where(f => f.ArrivalDateTime - f.DepartureDateTime == minTime) - .Select(f => mapper.Map(f)) + .Select(mapper.Map) .ToList(); } - public async Task> GetFlightsByRouteAsync(string departure, string arrival) + public async Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId) + { + // Убеждаемся, что рейс существует + var flight = await flightRepository.GetAsync(flightId); + if (flight == null) + throw new KeyNotFoundException($"Flight with ID '{flightId}' not found."); + + var tickets = await ticketRepository.GetAllAsync(); + var passengerIds = tickets + .Where(t => t.FlightId == flightId && t.BaggageWeight == null) + .Select(t => t.PassengerId) + .ToHashSet(); + + if (!passengerIds.Any()) return []; + + var passengers = await passengerRepository.GetAllAsync(); + return passengers + .Where(p => passengerIds.Contains(p.Id)) + .OrderBy(p => p.PassengerName) // сортировка по ФИО + .Select(mapper.Map) + .ToList(); + } + + public async Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to) { var flights = await flightRepository.GetAllAsync(); return flights - .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) - .Select(f => mapper.Map(f)) + .Where(f => f.ModelId == modelId && + f.DepartureDateTime >= from && + f.DepartureDateTime <= to) + .Select(mapper.Map) .ToList(); } - public async Task> GetFlightsOfModelWithinPeriod(string planeModelId, DateTime from, DateTime to) + public async Task> GetFlightsByRouteAsync(string departure, string arrival) { var flights = await flightRepository.GetAllAsync(); - - var result = flights - .Where(f => f.ModelId == planeModelId - && f.DepartureDateTime >= from - && f.DepartureDateTime <= to) + return flights + .Where(f => f.DepartureCity == departure && f.ArrivalCity == arrival) + .Select(mapper.Map) .ToList(); - - return mapper.Map>(result); } -} +} \ No newline at end of file diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs index c6eb24dde..f8eb05120 100644 --- a/Airline.Application/Services/FlightService.cs +++ b/Airline.Application/Services/FlightService.cs @@ -1,6 +1,6 @@ -using Airline.Application.Contracts.PlaneModel; -using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.PlaneModel; using Airline.Application.Contracts.Ticket; using Airline.Domain; using Airline.Domain.Items; @@ -15,49 +15,58 @@ public class FlightService( IRepository passengerRepository, IMapper mapper ) : IFlightService - { public async Task CreateAsync(CreateFlightDto dto) { - // Проверяем, существует ли модель - if (await planeModelRepository.GetAsync(dto.ModelId) is null) - throw new KeyNotFoundException($"Aircraft model with Id = {dto.ModelId} does not exist."); - - // Преобразуем DTO → Domain + if (await planeModelRepository.GetAsync(dto.ModelId) == null) + throw new KeyNotFoundException($"Plane model '{dto.ModelId}' not found."); var flight = mapper.Map(dto); - - // Сохраняем var created = await flightRepository.CreateAsync(flight); - - // Возвращаем Domain → DTO return mapper.Map(created); } - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(int id) { - var flight = await flightRepository.GetAsync(id); - return flight is null ? null : mapper.Map(flight); + var entity = await flightRepository.GetAsync(id); + return entity == null ? null : mapper.Map(entity); } - public async Task> GetAllAsync() - { - var flights = await flightRepository.GetAllAsync(); - return flights.Select(f => mapper.Map(f)).ToList(); - } + public async Task> GetAllAsync() => + (await flightRepository.GetAllAsync()).Select(mapper.Map).ToList(); - public async Task UpdateAsync(CreateFlightDto dto, int Id) + public async Task UpdateAsync(CreateFlightDto dto, int id) { - var existing = await flightRepository.GetAsync(Id); - if (existing is null) - throw new KeyNotFoundException($"Flight with Id = {Id} does not exist."); - - // Маппим обновлённые данные (можно использовать AutoMapper.UpdateFrom) + var existing = await flightRepository.GetAsync(id) + ?? throw new KeyNotFoundException($"Flight '{id}' not found."); mapper.Map(dto, existing); - var updated = await flightRepository.UpdateAsync(existing); return mapper.Map(updated); } - public async Task DeleteAsync(string id) => - await flightRepository.DeleteAsync(id); + public async Task DeleteAsync(int id) => await flightRepository.DeleteAsync(id); + + public async Task GetPlaneModelAsync(int flightId) + { + var flight = await flightRepository.GetAsync(flightId) + ?? throw new KeyNotFoundException($"Flight '{flightId}' not found."); + var model = await planeModelRepository.GetAsync(flight.ModelId) + ?? throw new InvalidOperationException($"Model '{flight.ModelId}' missing."); + return mapper.Map(model); + } + + public async Task> GetTicketsAsync(int flightId) + { + var all = await ticketRepository.GetAllAsync(); + return all.Where(t => t.FlightId == flightId) + .Select(mapper.Map).ToList(); + } + + public async Task> GetPassengersAsync(int flightId) + { + var ticketDtos = await GetTicketsAsync(flightId); + var passengerIds = ticketDtos.Select(t => t.PassengerId).ToHashSet(); + var allPassengers = await passengerRepository.GetAllAsync(); + return allPassengers.Where(p => passengerIds.Contains(p.Id)) + .Select(mapper.Map).ToList(); + } } \ No newline at end of file diff --git a/Airline.Application/Services/ModelFamilyService.cs b/Airline.Application/Services/ModelFamilyService.cs index ffc3a6acc..fda2b34e1 100644 --- a/Airline.Application/Services/ModelFamilyService.cs +++ b/Airline.Application/Services/ModelFamilyService.cs @@ -6,14 +6,51 @@ namespace Airline.Application.Services; -/// -/// Сервис для управления семействами самолётов и получения связанных моделей -/// -/// Репозиторий для операций с сущностями семейства самолётов -/// Репозиторий для операций с сущностями моделей самолётов -/// Маппер для преобразования доменных моделей в DTO и обратно -public class AircraftFamilyService( +public class ModelFamilyService( + IRepository planeModelRepository, IRepository familyRepository, - IRepository modelRepository, IMapper mapper -) : IModelFamilyService \ No newline at end of file +) : IModelFamilyService +{ + public async Task CreateAsync(CreateModelFamilyDto dto) + { + var family = mapper.Map(dto); + var created = await familyRepository.CreateAsync(family); + return mapper.Map(created); + } + + public async Task GetByIdAsync(int id) + { + var entity = await familyRepository.GetAsync(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAllAsync() => + (await familyRepository.GetAllAsync()).Select(mapper.Map).ToList(); + + public async Task UpdateAsync(CreateModelFamilyDto dto, int id) + { + var existing = await familyRepository.GetAsync(id) + ?? throw new KeyNotFoundException($"Model family '{id}' not found."); + mapper.Map(dto, existing); + var updated = await familyRepository.UpdateAsync(existing); + return mapper.Map(updated); + } + + public async Task DeleteAsync(int id) => await familyRepository.DeleteAsync(id); + + public async Task> GetPlaneModelsAsync(int familyId) + { + var family = await familyRepository.GetAsync(familyId); + if (family == null) + throw new KeyNotFoundException($"Model family with ID '{familyId}' not found."); + var allModels = await planeModelRepository.GetAllAsync(); + + var models = allModels + .Where(m => m.ModelFamilyId == familyId) + .Select(mapper.Map) + .ToList(); + + return models; + } +} \ No newline at end of file diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs index 4631e8431..9a3ab0acf 100644 --- a/Airline.Application/Services/PassengerService.cs +++ b/Airline.Application/Services/PassengerService.cs @@ -12,4 +12,48 @@ public class PassengerService( IRepository ticketRepository, IRepository flightRepository, IMapper mapper -) : IPassengerService \ No newline at end of file +) : IPassengerService +{ + public async Task CreateAsync(CreatePassengerDto dto) + { + var passenger = mapper.Map(dto); + var created = await passengerRepository.CreateAsync(passenger); + return mapper.Map(created); + } + + public async Task GetByIdAsync(int id) + { + var entity = await passengerRepository.GetAsync(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAllAsync() => + (await passengerRepository.GetAllAsync()).Select(mapper.Map).ToList(); + + public async Task UpdateAsync(CreatePassengerDto dto, int id) + { + var existing = await passengerRepository.GetAsync(id) + ?? throw new KeyNotFoundException($"Passenger '{id}' not found."); + mapper.Map(dto, existing); + var updated = await passengerRepository.UpdateAsync(existing); + return mapper.Map(updated); + } + + public async Task DeleteAsync(int id) => await passengerRepository.DeleteAsync(id); + + public async Task> GetTicketsAsync(int passengerId) + { + var all = await ticketRepository.GetAllAsync(); + return all.Where(t => t.PassengerId == passengerId) + .Select(mapper.Map).ToList(); + } + + public async Task> GetFlightsAsync(int passengerId) + { + var ticketDtos = await GetTicketsAsync(passengerId); + var flightIds = ticketDtos.Select(t => t.FlightId).ToHashSet(); + var allFlights = await flightRepository.GetAllAsync(); + return allFlights.Where(f => flightIds.Contains(f.Id)) + .Select(mapper.Map).ToList(); + } +} \ No newline at end of file diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs index 33b5c9735..0399bb98f 100644 --- a/Airline.Application/Services/PlaneModelService.cs +++ b/Airline.Application/Services/PlaneModelService.cs @@ -1,15 +1,53 @@ using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.PlaneModel; -using Airline.Application.Contracts.Flight; using Airline.Domain; using Airline.Domain.Items; using AutoMapper; namespace Airline.Application.Services; -public class AircraftModelService( - IRepository modelRepository, +public class PlaneModelService( + IRepository planeModelRepository, IRepository familyRepository, - IRepository flightRepository, IMapper mapper -) : IPlaneModelService \ No newline at end of file +) : IPlaneModelService +{ + public async Task CreateAsync(CreatePlaneModelDto dto) + { + if (await familyRepository.GetAsync(dto.ModelFamilyId) == null) + throw new KeyNotFoundException($"Model family '{dto.ModelFamilyId}' not found."); + var model = mapper.Map(dto); + var created = await planeModelRepository.CreateAsync(model); + return mapper.Map(created); + } + + public async Task GetByIdAsync(int id) + { + var entity = await planeModelRepository.GetAsync(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAllAsync() => + (await planeModelRepository.GetAllAsync()).Select(mapper.Map).ToList(); + + public async Task UpdateAsync(CreatePlaneModelDto dto, int id) + { + var existing = await planeModelRepository.GetAsync(id) + ?? throw new KeyNotFoundException($"Plane model '{id}' not found."); + mapper.Map(dto, existing); + var updated = await planeModelRepository.UpdateAsync(existing); + return mapper.Map(updated); + } + + public async Task DeleteAsync(int id) => await planeModelRepository.DeleteAsync(id); + + // Уникальный метод + public async Task GetModelFamilyAsync(int modelId) + { + var model = await planeModelRepository.GetAsync(modelId) + ?? throw new KeyNotFoundException($"Plane model '{modelId}' not found."); + var family = await familyRepository.GetAsync(model.ModelFamilyId) + ?? throw new InvalidOperationException($"Family '{model.ModelFamilyId}' missing."); + return mapper.Map(family); + } +} \ No newline at end of file diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs index c5169c3fa..ab17a91cd 100644 --- a/Airline.Application/Services/TicketService.cs +++ b/Airline.Application/Services/TicketService.cs @@ -12,4 +12,56 @@ public class TicketService( IRepository flightRepository, IRepository passengerRepository, IMapper mapper -) : ITicketService \ No newline at end of file +) : ITicketService +{ + public async Task CreateAsync(CreateTicketDto dto) + { + if (await flightRepository.GetAsync(dto.FlightId) == null) + throw new KeyNotFoundException($"Flight '{dto.FlightId}' not found."); + if (await passengerRepository.GetAsync(dto.PassengerId) == null) + throw new KeyNotFoundException($"Passenger '{dto.PassengerId}' not found."); + + var ticket = mapper.Map(dto); + var created = await ticketRepository.CreateAsync(ticket); + return mapper.Map(created); + } + + public async Task GetByIdAsync(int id) + { + var entity = await ticketRepository.GetAsync(id); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAllAsync() => + (await ticketRepository.GetAllAsync()).Select(mapper.Map).ToList(); + + public async Task UpdateAsync(CreateTicketDto dto, int id) + { + var existing = await ticketRepository.GetAsync(id) + ?? throw new KeyNotFoundException($"Ticket '{id}' not found."); + mapper.Map(dto, existing); + var updated = await ticketRepository.UpdateAsync(existing); + return mapper.Map(updated); + } + + public async Task DeleteAsync(int id) => await ticketRepository.DeleteAsync(id); + + // Уникальные методы + public async Task GetFlightAsync(int ticketId) + { + var ticket = await ticketRepository.GetAsync(ticketId) + ?? throw new KeyNotFoundException($"Ticket '{ticketId}' not found."); + var flight = await flightRepository.GetAsync(ticket.FlightId) + ?? throw new InvalidOperationException($"Flight '{ticket.FlightId}' missing."); + return mapper.Map(flight); + } + + public async Task GetPassengerAsync(int ticketId) + { + var ticket = await ticketRepository.GetAsync(ticketId) + ?? throw new KeyNotFoundException($"Ticket '{ticketId}' not found."); + var passenger = await passengerRepository.GetAsync(ticket.PassengerId) + ?? throw new InvalidOperationException($"Passenger '{ticket.PassengerId}' missing."); + return mapper.Map(passenger); + } +} \ No newline at end of file diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs index a7de132eb..7f14fcd0b 100644 --- a/Airline.Domain/Items/Flight.cs +++ b/Airline.Domain/Items/Flight.cs @@ -48,5 +48,5 @@ public class Flight /// /// The id of the model. /// - public string ModelId { get; set; } = string.Empty; // ← ссылка на PlaneModel + public int ModelId { get; set; } // ← ссылка на PlaneModel } \ No newline at end of file diff --git a/Airline.Domain/Items/PlaneModel.cs b/Airline.Domain/Items/PlaneModel.cs index 3a886b451..5c75c43d7 100644 --- a/Airline.Domain/Items/PlaneModel.cs +++ b/Airline.Domain/Items/PlaneModel.cs @@ -38,5 +38,5 @@ public class PlaneModel /// /// The id of model family. /// - public string PlaneFamilyId { get; set; } = string.Empty; // ← ссылка на ModelFamily + public int ModelFamilyId { get; set; } } \ No newline at end of file diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs index 2a425a731..300fb2ead 100644 --- a/Airline.Domain/Items/Ticket.cs +++ b/Airline.Domain/Items/Ticket.cs @@ -38,10 +38,10 @@ public class Ticket /// /// The id to connect between the ticket and the flight. /// - public string FlightId { get; set; } = string.Empty; // ← ссылка на Flight + public int FlightId { get; set; } // ← ссылка на Flight /// /// The id to connect between the ticket and the passenger. /// - public string PassengerId { get; set; } = string.Empty; // ← ссылка на Passenger + public int PassengerId { get; set; } // ← ссылка на Passenger } \ No newline at end of file From 27eaf80426025c800736ec95d153dc5ee5978417 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 10 Dec 2025 04:03:10 +0400 Subject: [PATCH 17/36] repositories update --- Airline.Infrastructure.EfCore/AirlineDbContext.cs | 2 +- .../Repositories/FlightRepository.cs | 10 +++++----- .../Repositories/ModelFamilyRepository.cs | 8 ++++---- .../Repositories/PassengerRepository.cs | 8 ++++---- .../Repositories/PlaneModelRepository.cs | 8 ++++---- .../Repositories/TicketRepository.cs | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs index a6a143223..6b0d5215c 100644 --- a/Airline.Infrastructure.EfCore/AirlineDbContext.cs +++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs @@ -63,7 +63,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(m => m.MaxRange).HasElementName("max_range_km"); entity.Property(m => m.PassengerCapacity).HasElementName("passenger_capacity"); entity.Property(m => m.CargoCapacity).HasElementName("cargo_capacity_tons"); - entity.Property(m => m.PlaneFamilyId).HasElementName("family_id"); // ← ссылка на ModelFamily + entity.Property(m => m.ModelFamilyId).HasElementName("family_id"); }); // Passenger → collection "passengers" diff --git a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs index b5f87a6ff..7a539ae55 100644 --- a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs @@ -1,10 +1,10 @@ -using Airline.Domain.Repository; -using Airline.Domain.Items; +using Airline.Domain.Items; +using Airline.Domain; using Microsoft.EntityFrameworkCore; namespace Airline.Infrastructure.EfCore.Repositories; -public class FlightRepository(AirlineDbContext context) : IRepository +public class FlightRepository(AirlineDbContext context) : IRepository { public async Task CreateAsync(Flight entity) { @@ -13,7 +13,7 @@ public async Task CreateAsync(Flight entity) return entry.Entity; } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(int id) { var entity = await context.Flights.FindAsync(id); if (entity == null) return false; @@ -23,7 +23,7 @@ public async Task DeleteAsync(string id) return true; } - public async Task GetAsync(string id) => + public async Task GetAsync(int id) => await context.Flights.FindAsync(id); public async Task> GetAllAsync() => diff --git a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs index 09066de3d..f1e23a453 100644 --- a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs @@ -1,10 +1,10 @@ using Airline.Domain.Items; -using Airline.Domain.Repository; +using Airline.Domain; using Microsoft.EntityFrameworkCore; namespace Airline.Infrastructure.EfCore.Repositories; -public class ModelFamilyRepository(AirlineDbContext context) : IRepository +public class ModelFamilyRepository(AirlineDbContext context) : IRepository { public async Task CreateAsync(ModelFamily entity) { @@ -13,7 +13,7 @@ public async Task CreateAsync(ModelFamily entity) return entry.Entity; } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(int id) { var entity = await context.ModelFamilies.FindAsync(id); if (entity == null) return false; @@ -23,7 +23,7 @@ public async Task DeleteAsync(string id) return true; } - public async Task GetAsync(string id) => + public async Task GetAsync(int id) => await context.ModelFamilies.FindAsync(id); public async Task> GetAllAsync() => diff --git a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs index 32bf3fdfc..196885ade 100644 --- a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs @@ -1,10 +1,10 @@ -using Airline.Domain.Repository; +using Airline.Domain; using Airline.Domain.Items; using Microsoft.EntityFrameworkCore; namespace Airline.Infrastructure.EfCore.Repositories; -public class PassengerRepository(AirlineDbContext context) : IRepository +public class PassengerRepository(AirlineDbContext context) : IRepository { public async Task CreateAsync(Passenger entity) { @@ -13,7 +13,7 @@ public async Task CreateAsync(Passenger entity) return entry.Entity; } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(int id) { var entity = await context.Passengers.FindAsync(id); if (entity == null) return false; @@ -23,7 +23,7 @@ public async Task DeleteAsync(string id) return true; } - public async Task GetAsync(string id) => + public async Task GetAsync(int id) => await context.Passengers.FindAsync(id); public async Task> GetAllAsync() => diff --git a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs index 172203aa2..c34cf1073 100644 --- a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs @@ -1,10 +1,10 @@ using Airline.Domain.Items; -using Airline.Domain.Repository; +using Airline.Domain; using Microsoft.EntityFrameworkCore; namespace Airline.Infrastructure.EfCore.Repositories; -public class PlaneModelRepository(AirlineDbContext context) : IRepository +public class PlaneModelRepository(AirlineDbContext context) : IRepository { public async Task CreateAsync(PlaneModel entity) { @@ -13,7 +13,7 @@ public async Task CreateAsync(PlaneModel entity) return entry.Entity; } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(int id) { var entity = await context.PlaneModels.FindAsync(id); if (entity == null) return false; @@ -23,7 +23,7 @@ public async Task DeleteAsync(string id) return true; } - public async Task GetAsync(string id) => + public async Task GetAsync(int id) => await context.PlaneModels.FindAsync(id); public async Task> GetAllAsync() => diff --git a/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs index d532074a5..6d950af8c 100644 --- a/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs @@ -1,10 +1,10 @@ -using Airline.Domain.Repository; +using Airline.Domain; using Airline.Domain.Items; using Microsoft.EntityFrameworkCore; namespace Airline.Infrastructure.EfCore.Repositories; -public class TicketRepository(AirlineDbContext context) : IRepository +public class TicketRepository(AirlineDbContext context) : IRepository { public async Task CreateAsync(Ticket entity) { @@ -13,7 +13,7 @@ public async Task CreateAsync(Ticket entity) return entry.Entity; } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(int id) { var entity = await context.Tickets.FindAsync(id); if (entity == null) return false; @@ -23,7 +23,7 @@ public async Task DeleteAsync(string id) return true; } - public async Task GetAsync(string id) => + public async Task GetAsync(int id) => await context.Tickets.FindAsync(id); public async Task> GetAllAsync() => From 4a56476848aad1631ac7f44d478a9b0e82fcefba Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 10 Dec 2025 04:46:13 +0400 Subject: [PATCH 18/36] app and api added --- Airline.Api.Host/Airline.Api.Host.csproj | 17 ++++++++ Airline.Api.Host/Airline.Api.Host.http | 6 +++ .../Controllers/WeatherForecastController.cs | 32 +++++++++++++++ Airline.Api.Host/Program.cs | 29 +++++++++++++ .../Properties/launchSettings.json | 41 +++++++++++++++++++ Airline.Api.Host/WeatherForecast.cs | 12 ++++++ Airline.Api.Host/appsettings.Development.json | 8 ++++ Airline.Api.Host/appsettings.json | 9 ++++ Airline.AppHost/Airline.AppHost.csproj | 15 +++++++ Airline.AppHost/AppHost.cs | 5 +++ .../Properties/launchSettings.json | 31 ++++++++++++++ Airline.AppHost/appsettings.Development.json | 8 ++++ Airline.AppHost/appsettings.json | 9 ++++ 13 files changed, 222 insertions(+) create mode 100644 Airline.Api.Host/Airline.Api.Host.csproj create mode 100644 Airline.Api.Host/Airline.Api.Host.http create mode 100644 Airline.Api.Host/Controllers/WeatherForecastController.cs create mode 100644 Airline.Api.Host/Program.cs create mode 100644 Airline.Api.Host/Properties/launchSettings.json create mode 100644 Airline.Api.Host/WeatherForecast.cs create mode 100644 Airline.Api.Host/appsettings.Development.json create mode 100644 Airline.Api.Host/appsettings.json create mode 100644 Airline.AppHost/Airline.AppHost.csproj create mode 100644 Airline.AppHost/AppHost.cs create mode 100644 Airline.AppHost/Properties/launchSettings.json create mode 100644 Airline.AppHost/appsettings.Development.json create mode 100644 Airline.AppHost/appsettings.json diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj new file mode 100644 index 000000000..96c6c4d71 --- /dev/null +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Airline.Api.Host/Airline.Api.Host.http b/Airline.Api.Host/Airline.Api.Host.http new file mode 100644 index 000000000..797dadd06 --- /dev/null +++ b/Airline.Api.Host/Airline.Api.Host.http @@ -0,0 +1,6 @@ +@Airline.Api.Host_HostAddress = http://localhost:5147 + +GET {{Airline.Api.Host_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Airline.Api.Host/Controllers/WeatherForecastController.cs b/Airline.Api.Host/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..a91fa8982 --- /dev/null +++ b/Airline.Api.Host/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Airline.Api.Host.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs new file mode 100644 index 000000000..edda8a7f4 --- /dev/null +++ b/Airline.Api.Host/Program.cs @@ -0,0 +1,29 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Airline.Api.Host/Properties/launchSettings.json b/Airline.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..43157dc31 --- /dev/null +++ b/Airline.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:53894", + "sslPort": 44359 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5147", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7089;http://localhost:5147", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Airline.Api.Host/WeatherForecast.cs b/Airline.Api.Host/WeatherForecast.cs new file mode 100644 index 000000000..e6ebe52c5 --- /dev/null +++ b/Airline.Api.Host/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Airline.Api.Host; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/Airline.Api.Host/appsettings.Development.json b/Airline.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Airline.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Airline.Api.Host/appsettings.json b/Airline.Api.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Airline.Api.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj new file mode 100644 index 000000000..c4fff901f --- /dev/null +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + c5640d6f-84de-4448-bbef-056105f657a0 + + + + + + + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs new file mode 100644 index 000000000..5441193b0 --- /dev/null +++ b/Airline.AppHost/AppHost.cs @@ -0,0 +1,5 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("airline-api-host"); + +builder.Build().Run(); diff --git a/Airline.AppHost/Properties/launchSettings.json b/Airline.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..643d26e64 --- /dev/null +++ b/Airline.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17255;http://localhost:15201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21277", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23151", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22297" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19082", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18107", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20078" + } + } + } +} diff --git a/Airline.AppHost/appsettings.Development.json b/Airline.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Airline.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Airline.AppHost/appsettings.json b/Airline.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/Airline.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} From ee3fbb091f89b933eb4fff84f00a45096dca9d31 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 11 Dec 2025 01:34:19 +0400 Subject: [PATCH 19/36] app and api modified --- Airline.Api.Host/Airline.Api.Host.csproj | 5 +- .../Controllers/AnalyticController.cs | 123 ++++++++++++++ .../Controllers/BaseCrudController.cs | 160 ++++++++++++++++++ .../Controllers/FlightController.cs | 92 ++++++++++ .../Controllers/ModelFamilyController.cs | 40 +++++ .../Controllers/PassengerController.cs | 66 ++++++++ .../Controllers/PlaneModelController.cs | 40 +++++ .../Controllers/TicketController.cs | 67 ++++++++ Airline.Api.Host/Program.cs | 86 ++++++++-- Airline.AppHost/Airline.AppHost.csproj | 8 +- Airline.AppHost/AppHost.cs | 5 +- Airline.Application/AirlineMapperProfile.cs | 4 +- .../Services/PlaneModelService.cs | 1 - Airline.Application/Services/TicketService.cs | 1 - .../Airline.ServiceDefaults.csproj | 22 +++ Airline.ServiceDefaults/Extensions.cs | 127 ++++++++++++++ Airline.sln | 24 ++- 17 files changed, 847 insertions(+), 24 deletions(-) create mode 100644 Airline.Api.Host/Controllers/AnalyticController.cs create mode 100644 Airline.Api.Host/Controllers/BaseCrudController.cs create mode 100644 Airline.Api.Host/Controllers/FlightController.cs create mode 100644 Airline.Api.Host/Controllers/ModelFamilyController.cs create mode 100644 Airline.Api.Host/Controllers/PassengerController.cs create mode 100644 Airline.Api.Host/Controllers/PlaneModelController.cs create mode 100644 Airline.Api.Host/Controllers/TicketController.cs create mode 100644 Airline.ServiceDefaults/Airline.ServiceDefaults.csproj create mode 100644 Airline.ServiceDefaults/Extensions.cs diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj index 96c6c4d71..2e80d24c4 100644 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -11,6 +11,9 @@ + + + diff --git a/Airline.Api.Host/Controllers/AnalyticController.cs b/Airline.Api.Host/Controllers/AnalyticController.cs new file mode 100644 index 000000000..ecc60b60d --- /dev/null +++ b/Airline.Api.Host/Controllers/AnalyticController.cs @@ -0,0 +1,123 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + + +[ApiController] +[Route("api/[controller]")] +public class AnalyticsController( + IAnalyticsService analyticsService, + ILogger logger +) : ControllerBase +{ + + [HttpGet("top-flights-by-passenger-count")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTopFlightsByPassengerCount([FromQuery] int top = 5) + { + logger.LogInformation("Метод GetTopFlightsByPassengerCount вызван с top={Top}", top); + try + { + var flights = await analyticsService.GetTopFlightsByPassengerCountAsync(top); + logger.LogInformation("Метод GetTopFlightsByPassengerCount успешно выполнен"); + return Ok(flights); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetTopFlightsByPassengerCount"); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("flights-with-min-travel-time")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetFlightsWithMinTravelTime() + { + logger.LogInformation("Метод GetFlightsWithMinTravelTime вызван"); + try + { + var flights = await analyticsService.GetFlightsWithMinTravelTimeAsync(); + logger.LogInformation("Метод GetFlightsWithMinTravelTime успешно выполнен"); + return Ok(flights); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetFlightsWithMinTravelTime"); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + + [HttpGet("passengers-with-zero-baggage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPassengersWithZeroBaggage([FromQuery] int flightId) + { + logger.LogInformation("Метод GetPassengersWithZeroBaggage вызван с flightId={FlightId}", flightId); + try + { + var passengers = await analyticsService.GetPassengersWithZeroBaggageOnFlightAsync(flightId); + logger.LogInformation("Метод GetPassengersWithZeroBaggage успешно выполнен для flightId={FlightId}", flightId); + return Ok(passengers); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Рейс не найден для flightId={FlightId}", flightId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetPassengersWithZeroBaggage для flightId={FlightId}", flightId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("flights-by-model-in-period")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetFlightsByModelInPeriod( + [FromQuery] int modelId, + [FromQuery] DateTime from, + [FromQuery] DateTime to) + { + logger.LogInformation("Метод GetFlightsByModelInPeriod вызван с modelId={ModelId}, from={From}, to={To}", modelId, from, to); + try + { + var flights = await analyticsService.GetFlightsByModelInPeriodAsync(modelId, from, to); + logger.LogInformation("Метод GetFlightsByModelInPeriod успешно выполнен"); + return Ok(flights); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetFlightsByModelInPeriod"); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("flights-by-route")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetFlightsByRoute( + [FromQuery] string departure, + [FromQuery] string arrival) + { + logger.LogInformation("Метод GetFlightsByRoute вызван с departure={Departure}, arrival={Arrival}", departure, arrival); + try + { + var flights = await analyticsService.GetFlightsByRouteAsync(departure, arrival); + logger.LogInformation("Метод GetFlightsByRoute успешно выполнен"); + return Ok(flights); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetFlightsByRoute"); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/BaseCrudController.cs b/Airline.Api.Host/Controllers/BaseCrudController.cs new file mode 100644 index 000000000..f042bd0f1 --- /dev/null +++ b/Airline.Api.Host/Controllers/BaseCrudController.cs @@ -0,0 +1,160 @@ +// Airline.Api.Host/Controllers/CrudControllerBase.cs +using Airline.Application.Contracts; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + +/// +/// Базовый контроллер для стандартизированных CRUD-операций над сущностями. +/// Обеспечивает единообразную обработку запросов, логирование и возврат HTTP-статусов. +/// +/// DTO для операций чтения +/// DTO для создания и обновления +/// Тип идентификатора (например, int) +/// Сервис, реализующий IApplicationService +/// Экземпляр логгера +[ApiController] +[Route("api/[controller]")] +public abstract class CrudControllerBase( + IApplicationService service, + ILogger> logger +) : ControllerBase + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + /// + /// Создаёт новую сущность. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create(TCreateUpdateDto dto) + { + logger.LogInformation("Метод Create вызван в {Controller} с данными: {@Dto}", GetType().Name, dto); + try + { + var result = await service.CreateAsync(dto); + logger.LogInformation("Метод Create успешно выполнен в {Controller}", GetType().Name); + return CreatedAtAction(nameof(GetById), new { id = GetEntityId(result) }, result); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе Create контроллера {Controller}", GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + /// + /// Обновляет существующую сущность по идентификатору. + /// + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Update(TKey id, TCreateUpdateDto dto) + { + logger.LogInformation("Метод Update вызван в {Controller} с id={Id} и данными: {@Dto}", GetType().Name, id, dto); + try + { + var result = await service.UpdateAsync(dto, id); + logger.LogInformation("Метод Update успешно выполнен в {Controller}", GetType().Name); + return Ok(result); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Сущность с id={Id} не найдена в {Controller}", id, GetType().Name); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе Update контроллера {Controller}", GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + /// + /// Удаляет сущность по идентификатору. + /// + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task Delete(TKey id) + { + logger.LogInformation("Метод Delete вызван в {Controller} с id={Id}", GetType().Name, id); + try + { + var success = await service.DeleteAsync(id); + if (!success) + { + logger.LogWarning("Сущность с id={Id} не найдена при удалении в {Controller}", id, GetType().Name); + return NotFound(); + } + logger.LogInformation("Метод Delete успешно выполнен в {Controller}", GetType().Name); + return NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе Delete контроллера {Controller}", GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + /// + /// Возвращает все сущности. + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAll() + { + logger.LogInformation("Метод GetAll вызван в {Controller}", GetType().Name); + try + { + var result = await service.GetAllAsync(); + logger.LogInformation("Метод GetAll успешно выполнен в {Controller}", GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetAll контроллера {Controller}", GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + /// + /// Возвращает сущность по идентификатору. + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetById(TKey id) + { + logger.LogInformation("Метод GetById вызван в {Controller} с id={Id}", GetType().Name, id); + try + { + var result = await service.GetByIdAsync(id); + if (result == null) + { + logger.LogWarning("Сущность с id={Id} не найдена в {Controller}", id, GetType().Name); + return NotFound(); + } + logger.LogInformation("Метод GetById успешно выполнен в {Controller}", GetType().Name); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetById контроллера {Controller}", GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + /// + /// Извлекает идентификатор из DTO. + /// Должен быть переопределён в производных классах. + /// + protected abstract TKey GetEntityId(TDto dto); +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/FlightController.cs b/Airline.Api.Host/Controllers/FlightController.cs new file mode 100644 index 000000000..37c23452a --- /dev/null +++ b/Airline.Api.Host/Controllers/FlightController.cs @@ -0,0 +1,92 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.PlaneModel; +using Airline.Application.Contracts.Ticket; +using Microsoft.AspNetCore.Mvc; + +namespace Airline.Api.Host.Controllers; + +[Route("api/[controller]")] +public class FlightsController( + IFlightService flightService, + ILogger logger +) : CrudControllerBase(flightService, logger) +{ + + protected override int GetEntityId(FlightDto dto) => dto.Id; + + [HttpGet("{flightId}/model")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetPlaneModel(int flightId) + { + logger.LogInformation("Метод GetPlaneModel вызван с flightId={FlightId}", flightId); + try + { + var model = await flightService.GetPlaneModelAsync(flightId); + logger.LogInformation("Метод GetPlaneModel успешно выполнен для flightId={FlightId}", flightId); + return Ok(model); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Модель самолёта не найдена для flightId={FlightId}", flightId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetPlaneModel для flightId={FlightId}", flightId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("{flightId}/tickets")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTickets(int flightId) + { + logger.LogInformation("Метод GetTickets вызван с flightId={FlightId}", flightId); + try + { + var tickets = await flightService.GetTicketsAsync(flightId); + logger.LogInformation("Метод GetTickets успешно выполнен для flightId={FlightId}", flightId); + return Ok(tickets); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Билеты не найдены для flightId={FlightId}", flightId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetTickets для flightId={FlightId}", flightId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("{flightId}/passengers")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPassengers(int flightId) + { + logger.LogInformation("Метод GetPassengers вызван с flightId={FlightId}", flightId); + try + { + var passengers = await flightService.GetPassengersAsync(flightId); + logger.LogInformation("Метод GetPassengers успешно выполнен для flightId={FlightId}", flightId); + return Ok(passengers); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Пассажиры не найдены для flightId={FlightId}", flightId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetPassengers для flightId={FlightId}", flightId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/ModelFamilyController.cs b/Airline.Api.Host/Controllers/ModelFamilyController.cs new file mode 100644 index 000000000..ce129654a --- /dev/null +++ b/Airline.Api.Host/Controllers/ModelFamilyController.cs @@ -0,0 +1,40 @@ +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.PlaneModel; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + +[Route("api/[controller]")] +public class ModelFamiliesController( + IModelFamilyService modelFamilyService, + ILogger logger +) : CrudControllerBase(modelFamilyService, logger) +{ + protected override int GetEntityId(ModelFamilyDto dto) => dto.Id; + + [HttpGet("{familyId}/models")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPlaneModels(int familyId) + { + logger.LogInformation("Метод GetPlaneModels вызван с familyId={FamilyId}", familyId); + try + { + var models = await modelFamilyService.GetPlaneModelsAsync(familyId); + logger.LogInformation("Метод GetPlaneModels успешно выполнен для familyId={FamilyId}", familyId); + return Ok(models); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Модели самолётов не найдены для familyId={FamilyId}", familyId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetPlaneModels для familyId={FamilyId}", familyId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/PassengerController.cs b/Airline.Api.Host/Controllers/PassengerController.cs new file mode 100644 index 000000000..9a6785c3a --- /dev/null +++ b/Airline.Api.Host/Controllers/PassengerController.cs @@ -0,0 +1,66 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + +[Route("api/[controller]")] +public class PassengersController( + IPassengerService passengerService, + ILogger logger +) : CrudControllerBase(passengerService, logger) +{ + protected override int GetEntityId(PassengerDto dto) => dto.Id; + + [HttpGet("{passengerId}/tickets")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTickets(int passengerId) + { + logger.LogInformation("Метод GetTickets вызван с passengerId={PassengerId}", passengerId); + try + { + var tickets = await passengerService.GetTicketsAsync(passengerId); + logger.LogInformation("Метод GetTickets успешно выполнен для passengerId={PassengerId}", passengerId); + return Ok(tickets); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Билеты не найдены для passengerId={PassengerId}", passengerId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetTickets для passengerId={PassengerId}", passengerId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("{passengerId}/flights")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetFlights(int passengerId) + { + logger.LogInformation("Метод GetFlights вызван с passengerId={PassengerId}", passengerId); + try + { + var flights = await passengerService.GetFlightsAsync(passengerId); + logger.LogInformation("Метод GetFlights успешно выполнен для passengerId={PassengerId}", passengerId); + return Ok(flights); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Рейсы не найдены для passengerId={PassengerId}", passengerId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetFlights для passengerId={PassengerId}", passengerId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/PlaneModelController.cs b/Airline.Api.Host/Controllers/PlaneModelController.cs new file mode 100644 index 000000000..79e18a859 --- /dev/null +++ b/Airline.Api.Host/Controllers/PlaneModelController.cs @@ -0,0 +1,40 @@ +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.PlaneModel; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + +[Route("api/[controller]")] +public class PlaneModelsController( + IPlaneModelService planeModelService, + ILogger logger +) : CrudControllerBase(planeModelService, logger) +{ + protected override int GetEntityId(PlaneModelDto dto) => dto.Id; + + [HttpGet("{modelId}/family")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetModelFamily(int modelId) + { + logger.LogInformation("Метод GetModelFamily вызван с modelId={ModelId}", modelId); + try + { + var family = await planeModelService.GetModelFamilyAsync(modelId); + logger.LogInformation("Метод GetModelFamily успешно выполнен для modelId={ModelId}", modelId); + return Ok(family); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Семейство моделей не найдено для modelId={ModelId}", modelId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetModelFamily для modelId={ModelId}", modelId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/TicketController.cs b/Airline.Api.Host/Controllers/TicketController.cs new file mode 100644 index 000000000..1e6752320 --- /dev/null +++ b/Airline.Api.Host/Controllers/TicketController.cs @@ -0,0 +1,67 @@ +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.Ticket; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Airline.Api.Host.Controllers; + +[Route("api/[controller]")] +public class TicketsController( + ITicketService ticketService, + ILogger logger +) : CrudControllerBase(ticketService, logger) +{ + + protected override int GetEntityId(TicketDto dto) => dto.Id; + + [HttpGet("{ticketId}/flight")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetFlight(int ticketId) + { + logger.LogInformation("Метод GetFlight вызван с ticketId={TicketId}", ticketId); + try + { + var flight = await ticketService.GetFlightAsync(ticketId); + logger.LogInformation("Метод GetFlight успешно выполнен для ticketId={TicketId}", ticketId); + return Ok(flight); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Рейс не найден для ticketId={TicketId}", ticketId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetFlight для ticketId={TicketId}", ticketId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + [HttpGet("{ticketId}/passenger")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetPassenger(int ticketId) + { + logger.LogInformation("Метод GetPassenger вызван с ticketId={TicketId}", ticketId); + try + { + var passenger = await ticketService.GetPassengerAsync(ticketId); + logger.LogInformation("Метод GetPassenger успешно выполнен для ticketId={TicketId}", ticketId); + return Ok(passenger); + } + catch (KeyNotFoundException) + { + logger.LogWarning("Пассажир не найден для ticketId={TicketId}", ticketId); + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в методе GetPassenger для ticketId={TicketId}", ticketId); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index edda8a7f4..8519a5332 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -1,29 +1,95 @@ +using Airline.Api.Host; +using Airline.Application; +using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.ModelFamily; +using Airline.Application.Contracts.Passenger; +using Airline.Application.Contracts.PlaneModel; +using Airline.Application.Contracts.Ticket; +using Airline.Application.Services; +using Airline.Domain; +using Airline.Domain.DataSeed; +using Airline.Domain.Items; +using Airline.Infrastructure.EfCore; +using Airline.Infrastructure.EfCore.Repositories; +using Airline.ServiceDefaults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; + var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -// Add services to the container. -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddSingleton(); + +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new AirlineProfile()); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped, FlightRepository>(); +builder.Services.AddScoped, PassengerRepository>(); +builder.Services.AddScoped, TicketRepository>(); +builder.Services.AddScoped, PlaneModelRepository>(); +builder.Services.AddScoped, ModelFamilyRepository>(); + +builder.Services.AddDbContext(options => +{ + var connectionString = builder.Configuration.GetConnectionString("mongodb"); + if (string.IsNullOrEmpty(connectionString)) + connectionString = "mongodb://localhost:27017"; + + options.UseMongoDB(connectionString, "AirlineDb"); +}); + +// API +builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + +// Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "Airline API", + Version = "v1", + Description = "REST API " + }); +}); + +builder.Services.Configure(options => +{ + options.SuppressMapClientErrors = true; +}); var app = builder.Build(); app.MapDefaultEndpoints(); -// Configure the HTTP request pipeline. +// Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Airline API v1"); + }); } app.UseHttpsRedirection(); - -app.UseAuthorization(); - +app.UseRouting(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index c4fff901f..c7c4722e3 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,8 +8,14 @@ c5640d6f-84de-4448-bbef-056105f657a0 + + + + + + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index 5441193b0..b6705b23b 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -1,5 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); -builder.AddProject("airline-api-host"); +var db = builder.AddMongoDB("mongo").AddDatabase("db"); +builder.AddProject("airline-api-host") + .WithReference(db, "airlineClient") + .WaitFor(db); builder.Build().Run(); diff --git a/Airline.Application/AirlineMapperProfile.cs b/Airline.Application/AirlineMapperProfile.cs index ea77c9d09..f0e648897 100644 --- a/Airline.Application/AirlineMapperProfile.cs +++ b/Airline.Application/AirlineMapperProfile.cs @@ -10,9 +10,7 @@ namespace Airline.Application; public class AirlineProfile : Profile { - /// - /// Конструктор профиля, создающий связи между Entity и Dto классами - /// + // Конструктор профиля, создающий связи между Entity и Dto классами public AirlineProfile() { CreateMap(); diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs index 0399bb98f..bcf14e9b4 100644 --- a/Airline.Application/Services/PlaneModelService.cs +++ b/Airline.Application/Services/PlaneModelService.cs @@ -41,7 +41,6 @@ public async Task UpdateAsync(CreatePlaneModelDto dto, int id) public async Task DeleteAsync(int id) => await planeModelRepository.DeleteAsync(id); - // Уникальный метод public async Task GetModelFamilyAsync(int modelId) { var model = await planeModelRepository.GetAsync(modelId) diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs index ab17a91cd..40abeefd7 100644 --- a/Airline.Application/Services/TicketService.cs +++ b/Airline.Application/Services/TicketService.cs @@ -46,7 +46,6 @@ public async Task UpdateAsync(CreateTicketDto dto, int id) public async Task DeleteAsync(int id) => await ticketRepository.DeleteAsync(id); - // Уникальные методы public async Task GetFlightAsync(int ticketId) { var ticket = await ticketRepository.GetAsync(ticketId) diff --git a/Airline.ServiceDefaults/Airline.ServiceDefaults.csproj b/Airline.ServiceDefaults/Airline.ServiceDefaults.csproj new file mode 100644 index 000000000..915b00810 --- /dev/null +++ b/Airline.ServiceDefaults/Airline.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/Airline.ServiceDefaults/Extensions.cs b/Airline.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..93660f617 --- /dev/null +++ b/Airline.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Airline.ServiceDefaults; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static 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(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + 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(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Airline.sln b/Airline.sln index 782ccedae..0b2926726 100644 --- a/Airline.sln +++ b/Airline.sln @@ -11,10 +11,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Infrastructure.EfCo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Application", "Airline.Application\Airline.Application.csproj", "{92A1A31C-40CB-802C-CCC3-246E0C50E4E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Api.Host", "Airline.Api.Host\Airline.Api.Host.csproj", "{2E9E28E0-D750-4996-A32D-84171942C3DE}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Domain", "Airline.Domain\Airline.Domain.csproj", "{1A3ED493-FDE5-C37C-498C-1431A58B395A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Api.Host", "Airline.Api.Host\Airline.Api.Host.csproj", "{880A43A8-0013-9906-DB33-3DF18CDBA4EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.AppHost", "Airline.AppHost\Airline.AppHost.csproj", "{371AB662-A469-4684-8FC1-F5363996F00F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.ServiceDefaults", "Airline.ServiceDefaults\Airline.ServiceDefaults.csproj", "{AB4A2E61-005D-D6AF-C18F-732B6C13F746}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,14 +41,22 @@ Global {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {92A1A31C-40CB-802C-CCC3-246E0C50E4E1}.Release|Any CPU.Build.0 = Release|Any CPU - {2E9E28E0-D750-4996-A32D-84171942C3DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2E9E28E0-D750-4996-A32D-84171942C3DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E9E28E0-D750-4996-A32D-84171942C3DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2E9E28E0-D750-4996-A32D-84171942C3DE}.Release|Any CPU.Build.0 = Release|Any CPU {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A3ED493-FDE5-C37C-498C-1431A58B395A}.Release|Any CPU.Build.0 = Release|Any CPU + {880A43A8-0013-9906-DB33-3DF18CDBA4EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {880A43A8-0013-9906-DB33-3DF18CDBA4EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {880A43A8-0013-9906-DB33-3DF18CDBA4EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {880A43A8-0013-9906-DB33-3DF18CDBA4EE}.Release|Any CPU.Build.0 = Release|Any CPU + {371AB662-A469-4684-8FC1-F5363996F00F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {371AB662-A469-4684-8FC1-F5363996F00F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {371AB662-A469-4684-8FC1-F5363996F00F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {371AB662-A469-4684-8FC1-F5363996F00F}.Release|Any CPU.Build.0 = Release|Any CPU + {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 5323d40521ce1c3c5ef7cee9a9679b08e4ca766d Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 12 Dec 2025 03:08:12 +0400 Subject: [PATCH 20/36] csproj modified --- Airline.Api.Host/Airline.Api.Host.csproj | 3 +- Airline.Api.Host/Program.cs | 123 +++++++++++++----- Airline.AppHost/Airline.AppHost.csproj | 5 +- Airline.AppHost/AppHost.cs | 3 + .../Airline.Infrastructure.EfCore.csproj | 4 +- 5 files changed, 98 insertions(+), 40 deletions(-) diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj index 2e80d24c4..05b536f3a 100644 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -7,7 +7,8 @@ - + + diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index 8519a5332..a55c4711f 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -1,33 +1,43 @@ -using Airline.Api.Host; using Airline.Application; +using Airline.Application.Contracts; using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.Passenger; using Airline.Application.Contracts.PlaneModel; using Airline.Application.Contracts.Ticket; -using Airline.Application.Services; +using Airline.Application.Services; using Airline.Domain; using Airline.Domain.DataSeed; using Airline.Domain.Items; -using Airline.Infrastructure.EfCore; +using Airline.Infrastructure.EfCore; using Airline.Infrastructure.EfCore.Repositories; using Airline.ServiceDefaults; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); +// ============= Aspire Service Defaults ============= builder.AddServiceDefaults(); - +// ============= DataSeeder ============= builder.Services.AddSingleton(); +// ============= AutoMapper ============= builder.Services.AddAutoMapper(config => { config.AddProfile(new AirlineProfile()); }); +// ============= ============= +builder.Services.AddTransient, FlightRepository>(); +builder.Services.AddTransient, PassengerRepository>(); +builder.Services.AddTransient, TicketRepository>(); +builder.Services.AddTransient, PlaneModelRepository>(); +builder.Services.AddTransient, ModelFamilyRepository>(); + +// ============= ============= builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -35,61 +45,102 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped, FlightRepository>(); -builder.Services.AddScoped, PassengerRepository>(); -builder.Services.AddScoped, TicketRepository>(); -builder.Services.AddScoped, PlaneModelRepository>(); -builder.Services.AddScoped, ModelFamilyRepository>(); - -builder.Services.AddDbContext(options => -{ - var connectionString = builder.Configuration.GetConnectionString("mongodb"); - if (string.IsNullOrEmpty(connectionString)) - connectionString = "mongodb://localhost:27017"; - - options.UseMongoDB(connectionString, "AirlineDb"); -}); - -// API +// ============= API ============= builder.Services.AddControllers() - .ConfigureApiBehaviorOptions(options => + .AddJsonOptions(options => { - options.SuppressModelStateInvalidFilter = true; + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; }); -// Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => + +// ============= Swagger XML- ============= +builder.Services.AddSwaggerGen(c => { - options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + //c.SwaggerDoc("v1", new OpenApiInfo { Title = "Airline API", Version = "v1" }); + + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("Airline")) + .Distinct(); + + foreach (var assembly in assemblies) { - Title = "Airline API", - Version = "v1", - Description = "REST API " - }); + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } }); -builder.Services.Configure(options => +// ============= MongoDB Aspire ============= +//builder.AddMongoDBClient("airline"); + +//builder.Services.AddDbContext((services, options) => +//{ +// var db = services.GetRequiredService(); +//options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); +//}); +builder.AddMongoDBClient("airlineClient"); + +builder.Services.AddDbContext((services, o) => { - options.SuppressMapClientErrors = true; + var db = services.GetRequiredService(); + o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); }); + +// ============= ============= var app = builder.Build(); -app.MapDefaultEndpoints(); +app.MapDefaultEndpoints(); // Health checks Aspire // Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(options => + app.UseSwaggerUI(c => { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "Airline API v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Airline API v1"); }); } +// ============= ============= +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + var seed = scope.ServiceProvider.GetRequiredService(); + + // , Flights + var flightsExist = dbContext.Flights.Any(); + if (!flightsExist) + { + // ModelFamilies + foreach (var family in seed.ModelFamilies) + await dbContext.ModelFamilies.AddAsync(family); + + // PlaneModels + foreach (var model in seed.PlaneModels) + await dbContext.PlaneModels.AddAsync(model); + + // Passengers + foreach (var passenger in seed.Passengers) + await dbContext.Passengers.AddAsync(passenger); + + // Flights + foreach (var flight in seed.Flights) + await dbContext.Flights.AddAsync(flight); + + // Tickets + foreach (var ticket in seed.Tickets) + await dbContext.Tickets.AddAsync(ticket); + + await dbContext.SaveChangesAsync(); + app.Logger.LogInformation(" ."); + } +} + app.UseHttpsRedirection(); -app.UseRouting(); +app.UseAuthorization(); app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index c7c4722e3..52443a51e 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -1,4 +1,6 @@ - + + + Exe @@ -16,6 +18,7 @@ + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index b6705b23b..efeecae55 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -1,3 +1,5 @@ +using Aspire.Hosting; + var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddMongoDB("mongo").AddDatabase("db"); @@ -6,3 +8,4 @@ .WithReference(db, "airlineClient") .WaitFor(db); builder.Build().Run(); + diff --git a/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj index 70f5e304f..2dba4f0a5 100644 --- a/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj +++ b/Airline.Infrastructure.EfCore/Airline.Infrastructure.EfCore.csproj @@ -7,8 +7,8 @@ - - + + From e145b6855c328b481b3f1075ac1c668d3faacfe3 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 15 Dec 2025 00:48:02 +0400 Subject: [PATCH 21/36] some fix --- Airline.Application/Services/FlightService.cs | 14 ++++++++++++++ Airline.Application/Services/ModelFamilyService.cs | 11 +++++++++-- Airline.Application/Services/PassengerService.cs | 14 +++++++++++++- Airline.Application/Services/PlaneModelService.cs | 13 +++++++++++-- Airline.Application/Services/TicketService.cs | 7 +++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs index f8eb05120..4e1d2dd5c 100644 --- a/Airline.Application/Services/FlightService.cs +++ b/Airline.Application/Services/FlightService.cs @@ -20,7 +20,15 @@ public async Task CreateAsync(CreateFlightDto dto) { if (await planeModelRepository.GetAsync(dto.ModelId) == null) throw new KeyNotFoundException($"Plane model '{dto.ModelId}' not found."); + var flight = mapper.Map(dto); + var maxId = 0; + var last = await flightRepository.GetAllAsync(); + if (last.Any()) + { + maxId = last.Max(f => f.Id); + } + flight.Id = maxId + 1; var created = await flightRepository.CreateAsync(flight); return mapper.Map(created); } @@ -56,6 +64,9 @@ public async Task GetPlaneModelAsync(int flightId) public async Task> GetTicketsAsync(int flightId) { + var flight = await flightRepository.GetAsync(flightId) != null; + if (!flight) + throw new KeyNotFoundException($"Flight with ID '{flightId}' not found."); var all = await ticketRepository.GetAllAsync(); return all.Where(t => t.FlightId == flightId) .Select(mapper.Map).ToList(); @@ -63,6 +74,9 @@ public async Task> GetTicketsAsync(int flightId) public async Task> GetPassengersAsync(int flightId) { + var flight = await flightRepository.GetAsync(flightId) != null; + if (!flight) + throw new KeyNotFoundException($"Flight with ID '{flightId}' not found."); var ticketDtos = await GetTicketsAsync(flightId); var passengerIds = ticketDtos.Select(t => t.PassengerId).ToHashSet(); var allPassengers = await passengerRepository.GetAllAsync(); diff --git a/Airline.Application/Services/ModelFamilyService.cs b/Airline.Application/Services/ModelFamilyService.cs index fda2b34e1..d9833f1c0 100644 --- a/Airline.Application/Services/ModelFamilyService.cs +++ b/Airline.Application/Services/ModelFamilyService.cs @@ -14,8 +14,15 @@ IMapper mapper { public async Task CreateAsync(CreateModelFamilyDto dto) { - var family = mapper.Map(dto); - var created = await familyRepository.CreateAsync(family); + var modelFamily = mapper.Map(dto); + var maxId = 0; + var last = await familyRepository.GetAllAsync(); + if (last.Any()) + { + maxId = last.Max(f => f.Id); + } + modelFamily.Id = maxId + 1; + var created = await familyRepository.CreateAsync(modelFamily); return mapper.Map(created); } diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs index 9a3ab0acf..a21dfed88 100644 --- a/Airline.Application/Services/PassengerService.cs +++ b/Airline.Application/Services/PassengerService.cs @@ -17,10 +17,16 @@ IMapper mapper public async Task CreateAsync(CreatePassengerDto dto) { var passenger = mapper.Map(dto); + var maxId = 0; + var last = await passengerRepository.GetAllAsync(); + if (last.Any()) + { + maxId = last.Max(p => p.Id); + } + passenger.Id = maxId + 1; var created = await passengerRepository.CreateAsync(passenger); return mapper.Map(created); } - public async Task GetByIdAsync(int id) { var entity = await passengerRepository.GetAsync(id); @@ -43,6 +49,9 @@ public async Task UpdateAsync(CreatePassengerDto dto, int id) public async Task> GetTicketsAsync(int passengerId) { + var passengerExists = await passengerRepository.GetAsync(passengerId) != null; + if (!passengerExists) + throw new KeyNotFoundException($"Passenger with ID '{passengerId}' not found."); var all = await ticketRepository.GetAllAsync(); return all.Where(t => t.PassengerId == passengerId) .Select(mapper.Map).ToList(); @@ -50,6 +59,9 @@ public async Task> GetTicketsAsync(int passengerId) public async Task> GetFlightsAsync(int passengerId) { + var passengerExists = await passengerRepository.GetAsync(passengerId) != null; + if (!passengerExists) + throw new KeyNotFoundException($"Passenger with ID '{passengerId}' not found."); var ticketDtos = await GetTicketsAsync(passengerId); var flightIds = ticketDtos.Select(t => t.FlightId).ToHashSet(); var allFlights = await flightRepository.GetAllAsync(); diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs index bcf14e9b4..a0eb4992f 100644 --- a/Airline.Application/Services/PlaneModelService.cs +++ b/Airline.Application/Services/PlaneModelService.cs @@ -12,12 +12,21 @@ public class PlaneModelService( IMapper mapper ) : IPlaneModelService { + public async Task CreateAsync(CreatePlaneModelDto dto) { if (await familyRepository.GetAsync(dto.ModelFamilyId) == null) throw new KeyNotFoundException($"Model family '{dto.ModelFamilyId}' not found."); - var model = mapper.Map(dto); - var created = await planeModelRepository.CreateAsync(model); + + var planeModel = mapper.Map(dto); + var maxId = 0; + var last = await planeModelRepository.GetAllAsync(); + if (last.Any()) + { + maxId = last.Max(m => m.Id); + } + planeModel.Id = maxId + 1; + var created = await planeModelRepository.CreateAsync(planeModel); return mapper.Map(created); } diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs index 40abeefd7..57bc2defb 100644 --- a/Airline.Application/Services/TicketService.cs +++ b/Airline.Application/Services/TicketService.cs @@ -22,6 +22,13 @@ public async Task CreateAsync(CreateTicketDto dto) throw new KeyNotFoundException($"Passenger '{dto.PassengerId}' not found."); var ticket = mapper.Map(dto); + var maxId = 0; + var last = await ticketRepository.GetAllAsync(); + if (last.Any()) + { + maxId = last.Max(t => t.Id); + } + ticket.Id = maxId + 1; var created = await ticketRepository.CreateAsync(ticket); return mapper.Map(created); } From 53bfafb58355d1f995bce6e06e758c1ba931f80b Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 16 Dec 2025 03:04:00 +0400 Subject: [PATCH 22/36] some more fix --- .../Passenger/CreatePassengerDto.cs | 2 +- Airline.Application.Contracts/Passenger/PassengerDto.cs | 2 +- Airline.Application/Services/PassengerService.cs | 6 ++++++ Airline.Application/Services/PlaneModelService.cs | 4 ++++ Airline.Application/Services/TicketService.cs | 4 ++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs index 732cf4f1d..5f8f26e12 100644 --- a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs +++ b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs @@ -6,4 +6,4 @@ namespace Airline.Application.Contracts.Passenger; /// Passport number. /// Full name of the passenger. /// Date of birth (YYYY-MM-DD). -public record CreatePassengerDto(string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file +public record CreatePassengerDto(string Passport, string PassengerName, DateOnly DateOfBirth); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/PassengerDto.cs b/Airline.Application.Contracts/Passenger/PassengerDto.cs index a18429af7..b8e2949f3 100644 --- a/Airline.Application.Contracts/Passenger/PassengerDto.cs +++ b/Airline.Application.Contracts/Passenger/PassengerDto.cs @@ -7,4 +7,4 @@ namespace Airline.Application.Contracts.Passenger; /// Passport number. /// Full name of the passenger. /// Date of birth (YYYY-MM-DD). -public record PassengerDto(int Id, string Passport, string PassengerName, string DateOfBirth); \ No newline at end of file +public record PassengerDto(int Id, string Passport, string PassengerName, DateOnly DateOfBirth); \ No newline at end of file diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs index a21dfed88..0a12f4db7 100644 --- a/Airline.Application/Services/PassengerService.cs +++ b/Airline.Application/Services/PassengerService.cs @@ -4,6 +4,8 @@ using Airline.Domain; using Airline.Domain.Items; using AutoMapper; +using System.Text.RegularExpressions; +using System.Xml.Linq; namespace Airline.Application.Services; @@ -24,6 +26,8 @@ public async Task CreateAsync(CreatePassengerDto dto) maxId = last.Max(p => p.Id); } passenger.Id = maxId + 1; + if (string.IsNullOrWhiteSpace(dto.PassengerName) || Regex.IsMatch(dto.PassengerName, @"\d")) + throw new ArgumentException("Passenger name must not be empty or contain digits."); var created = await passengerRepository.CreateAsync(passenger); return mapper.Map(created); } @@ -41,6 +45,8 @@ public async Task UpdateAsync(CreatePassengerDto dto, int id) var existing = await passengerRepository.GetAsync(id) ?? throw new KeyNotFoundException($"Passenger '{id}' not found."); mapper.Map(dto, existing); + if (string.IsNullOrWhiteSpace(dto.PassengerName) || Regex.IsMatch(dto.PassengerName, @"\d")) + throw new ArgumentException("Passenger name must not be empty or contain digits."); var updated = await passengerRepository.UpdateAsync(existing); return mapper.Map(updated); } diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs index a0eb4992f..fa36f9d05 100644 --- a/Airline.Application/Services/PlaneModelService.cs +++ b/Airline.Application/Services/PlaneModelService.cs @@ -26,6 +26,8 @@ public async Task CreateAsync(CreatePlaneModelDto dto) maxId = last.Max(m => m.Id); } planeModel.Id = maxId + 1; + if (dto.PassengerCapacity < 0 || dto.CargoCapacity < 0) + throw new ArgumentException("Capacity values cannot be negative."); var created = await planeModelRepository.CreateAsync(planeModel); return mapper.Map(created); } @@ -44,6 +46,8 @@ public async Task UpdateAsync(CreatePlaneModelDto dto, int id) var existing = await planeModelRepository.GetAsync(id) ?? throw new KeyNotFoundException($"Plane model '{id}' not found."); mapper.Map(dto, existing); + if (dto.PassengerCapacity < 0 || dto.CargoCapacity < 0) + throw new ArgumentException("Capacity values cannot be negative."); var updated = await planeModelRepository.UpdateAsync(existing); return mapper.Map(updated); } diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs index 57bc2defb..4563ff802 100644 --- a/Airline.Application/Services/TicketService.cs +++ b/Airline.Application/Services/TicketService.cs @@ -29,6 +29,8 @@ public async Task CreateAsync(CreateTicketDto dto) maxId = last.Max(t => t.Id); } ticket.Id = maxId + 1; + if (dto.BaggageWeight < 0) + throw new ArgumentException("BaggageWeight cannot be negative."); var created = await ticketRepository.CreateAsync(ticket); return mapper.Map(created); } @@ -47,6 +49,8 @@ public async Task UpdateAsync(CreateTicketDto dto, int id) var existing = await ticketRepository.GetAsync(id) ?? throw new KeyNotFoundException($"Ticket '{id}' not found."); mapper.Map(dto, existing); + if (dto.BaggageWeight < 0) + throw new ArgumentException("BaggageWeight cannot be negative."); var updated = await ticketRepository.UpdateAsync(existing); return mapper.Map(updated); } From a6815a641a584996f06a07e3f18cd8c7a459b6a7 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 17 Dec 2025 05:07:23 +0400 Subject: [PATCH 23/36] final commit --- .../Controllers/AnalyticController.cs | 57 +++++++++++- .../Controllers/BaseCrudController.cs | 88 +++++++++++++++---- .../Controllers/FlightController.cs | 31 ++++++- .../Controllers/ModelFamilyController.cs | 15 +++- .../Controllers/PassengerController.cs | 23 ++++- .../Controllers/PlaneModelController.cs | 15 +++- .../Controllers/TicketController.cs | 24 ++++- Airline.Api.Host/Program.cs | 30 ++----- .../Flight/CreateFlightDto.cs | 18 ++-- .../Flight/FlightDto.cs | 20 +++-- .../Flight/IFlightService.cs | 24 ++++- .../IAnalyticService.cs | 45 ++++++++-- .../IApplicationService.cs | 50 ++++++++++- .../ModelFamily/CreateModelFamilyDto.cs | 12 ++- .../ModelFamily/IModelFamilyService.cs | 11 ++- .../ModelFamily/ModelFamilyDto.cs | 16 ++-- .../Passenger/CreatePassengerDto.cs | 15 ++-- .../Passenger/IPassengerService.cs | 15 ++++ .../Passenger/PassengerDto.cs | 18 ++-- .../PlaneModel/CreatePlaneModelDto.cs | 16 ++-- .../PlaneModel/IPlaneModelService.cs | 11 ++- .../PlaneModel/PlaneModelDto.cs | 18 ++-- .../Ticket/CreateTicketDto.cs | 18 ++-- .../Ticket/ITicketService.cs | 17 +++- .../Ticket/TicketDto.cs | 18 ++-- Airline.Application/AirlineMapperProfile.cs | 15 +++- .../Services/AnalyticService.cs | 38 +++++++- Airline.Application/Services/FlightService.cs | 64 ++++++++++++++ .../Services/ModelFamilyService.cs | 40 +++++++++ .../Services/PassengerService.cs | 59 ++++++++++++- .../Services/PlaneModelService.cs | 56 +++++++++++- Airline.Application/Services/TicketService.cs | 65 ++++++++++++++ Airline.Domain/IRepository.cs | 34 ++++--- Airline.Domain/Items/Flight.cs | 2 +- Airline.Domain/Items/Ticket.cs | 4 +- .../AirlineDbContext.cs | 31 +++---- .../Repositories/FlightRepository.cs | 33 +++++++ .../Repositories/ModelFamilyRepository.cs | 33 +++++++ .../Repositories/PassengerRepository.cs | 33 +++++++ .../Repositories/PlaneModelRepository.cs | 33 +++++++ .../Repositories/TicketRepository.cs | 33 +++++++ 41 files changed, 1023 insertions(+), 175 deletions(-) diff --git a/Airline.Api.Host/Controllers/AnalyticController.cs b/Airline.Api.Host/Controllers/AnalyticController.cs index ecc60b60d..f944c453a 100644 --- a/Airline.Api.Host/Controllers/AnalyticController.cs +++ b/Airline.Api.Host/Controllers/AnalyticController.cs @@ -1,11 +1,14 @@ using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.Passenger; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; - +/// +/// Controller for executing analytical queries over airline operational data. +/// Provides aggregated insights such as top flights, minimal travel time, passenger baggage analysis, +/// and route-based reporting. +/// [ApiController] [Route("api/[controller]")] public class AnalyticsController( @@ -13,7 +16,15 @@ public class AnalyticsController( ILogger logger ) : ControllerBase { - + /// + /// Retrieves the top N flights by number of passengers. + /// + /// The number of top flights to return (default: 5). + /// + /// A list of flight DTOs sorted by passenger count in descending order. + /// + /// Returns the list of top flights. + /// If an unexpected error occurs during processing. [HttpGet("top-flights-by-passenger-count")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -33,6 +44,14 @@ public async Task>> GetTopFlightsByPassengerCount([ } } + /// + /// Retrieves flights with the minimal travel time (duration). + /// + /// + /// A list of flight DTOs with the shortest duration. + /// + /// Returns the list of flights with minimal travel time. + /// If an unexpected error occurs during processing. [HttpGet("flights-with-min-travel-time")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -52,7 +71,16 @@ public async Task>> GetFlightsWithMinTravelTime() } } - + /// + /// Retrieves passengers with zero checked baggage for a specific flight. + /// + /// The unique identifier of the flight. + /// + /// A list of passenger DTOs who have no checked baggage on the specified flight. + /// + /// Returns the list of passengers with zero baggage. + /// If the specified flight does not exist. + /// If an unexpected error occurs during processing. [HttpGet("passengers-with-zero-baggage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -78,6 +106,17 @@ public async Task>> GetPassengersWithZeroBaggage } } + /// + /// Retrieves flights of a specific aircraft model within a given date period. + /// + /// The unique identifier of the aircraft model. + /// Start date of the period (inclusive). + /// End date of the period (inclusive). + /// + /// A list of flight DTOs matching the model and date range. + /// + /// Returns the list of flights matching the criteria. + /// If an unexpected error occurs during processing. [HttpGet("flights-by-model-in-period")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -100,6 +139,16 @@ public async Task>> GetFlightsByModelInPeriod( } } + /// + /// Retrieves flights by route (departure city → arrival city). + /// + /// The city of departure. + /// The city of arrival. + /// + /// A list of flight DTOs matching the specified route. + /// + /// Returns the list of flights matching the route. + /// If an unexpected error occurs during processing. [HttpGet("flights-by-route")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] diff --git a/Airline.Api.Host/Controllers/BaseCrudController.cs b/Airline.Api.Host/Controllers/BaseCrudController.cs index f042bd0f1..315501321 100644 --- a/Airline.Api.Host/Controllers/BaseCrudController.cs +++ b/Airline.Api.Host/Controllers/BaseCrudController.cs @@ -1,19 +1,28 @@ -// Airline.Api.Host/Controllers/CrudControllerBase.cs -using Airline.Application.Contracts; +using Airline.Application.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; /// -/// Базовый контроллер для стандартизированных CRUD-операций над сущностями. -/// Обеспечивает единообразную обработку запросов, логирование и возврат HTTP-статусов. +/// Base controller that provides standardized CRUD (Create, Read, Update, Delete) operations +/// for entities in the airline management system. +/// Implements common HTTP methods with unified error handling, logging, and response formatting. /// -/// DTO для операций чтения -/// DTO для создания и обновления -/// Тип идентификатора (например, int) -/// Сервис, реализующий IApplicationService -/// Экземпляр логгера +/// +/// The Data Transfer Object (DTO) type used for read operations. +/// Represents the shape of data exposed to API clients. +/// +/// +/// The Data Transfer Object (DTO) type used for create and update operations. +/// Contains only the fields required for mutation. +/// +/// +/// The type of the unique identifier for the entity (e.g., ). +/// Must be a value type (). +/// +/// The application service that implements business logic. +/// The logger instance for diagnostics and monitoring. [ApiController] [Route("api/[controller]")] public abstract class CrudControllerBase( @@ -22,11 +31,19 @@ ILogger> logger ) : ControllerBase where TDto : class where TCreateUpdateDto : class - where TKey : struct + where TKey : struct { /// - /// Создаёт новую сущность. + /// Creates a new entity. /// + /// The data transfer object containing creation data. + /// + /// An representing the result of the operation: + /// + /// 201 Created with the created entity if successful. + /// 500 Internal Server Error if an exception occurs. + /// + /// [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -47,8 +64,18 @@ public async Task> Create(TCreateUpdateDto dto) } /// - /// Обновляет существующую сущность по идентификатору. + /// Updates an existing entity by its unique identifier. /// + /// The unique identifier of the entity to update. + /// The data transfer object containing updated data. + /// + /// An representing the result of the operation: + /// + /// 200 OK with the updated entity if successful. + /// 404 Not Found if the entity does not exist. + /// 500 Internal Server Error if an exception occurs. + /// + /// [HttpPut("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -75,8 +102,17 @@ public async Task> Update(TKey id, TCreateUpdateDto dto) } /// - /// Удаляет сущность по идентификатору. + /// Deletes an entity by its unique identifier. /// + /// The unique identifier of the entity to delete. + /// + /// An representing the result of the operation: + /// + /// 204 No Content if the entity was successfully deleted. + /// 404 Not Found if the entity does not exist. + /// 500 Internal Server Error if an exception occurs. + /// + /// [HttpDelete("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -103,8 +139,15 @@ public async Task Delete(TKey id) } /// - /// Возвращает все сущности. + /// Retrieves all entities of the specified type. /// + /// + /// An representing the result of the operation: + /// + /// 200 OK with the list of entities if successful. + /// 500 Internal Server Error if an exception occurs. + /// + /// [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -125,8 +168,17 @@ public async Task>> GetAll() } /// - /// Возвращает сущность по идентификатору. + /// Retrieves an entity by its unique identifier. /// + /// The unique identifier of the entity to retrieve. + /// + /// An representing the result of the operation: + /// + /// 200 OK with the entity if found. + /// 404 Not Found if the entity does not exist. + /// 500 Internal Server Error if an exception occurs. + /// + /// [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -153,8 +205,10 @@ public async Task> GetById(TKey id) } /// - /// Извлекает идентификатор из DTO. - /// Должен быть переопределён в производных классах. + /// Extracts the unique identifier from a DTO. + /// Must be implemented by derived controllers to support routing. /// + /// The DTO from which to extract the identifier. + /// The unique identifier of the entity. protected abstract TKey GetEntityId(TDto dto); } \ No newline at end of file diff --git a/Airline.Api.Host/Controllers/FlightController.cs b/Airline.Api.Host/Controllers/FlightController.cs index 37c23452a..a2f615887 100644 --- a/Airline.Api.Host/Controllers/FlightController.cs +++ b/Airline.Api.Host/Controllers/FlightController.cs @@ -6,15 +6,28 @@ namespace Airline.Api.Host.Controllers; +/// +/// Controller for managing flights and retrieving associated data. +/// Inherits from +/// to provide standardized CRUD operations. +/// [Route("api/[controller]")] public class FlightsController( IFlightService flightService, ILogger logger ) : CrudControllerBase(flightService, logger) { - + /// protected override int GetEntityId(FlightDto dto) => dto.Id; + /// + /// Retrieves the aircraft model associated with a specific flight. + /// + /// The unique identifier of the flight. + /// The aircraft model DTO linked to the flight. + /// Returns the associated aircraft model. + /// If the flight or aircraft model is not found. + /// If an unexpected error occurs. [HttpGet("{flightId}/model")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -40,6 +53,14 @@ public async Task> GetPlaneModel(int flightId) } } + /// + /// Retrieves all tickets associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of ticket DTOs linked to the flight. + /// Returns the list of associated tickets. + /// If the flight is not found. + /// If an unexpected error occurs. [HttpGet("{flightId}/tickets")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -65,6 +86,14 @@ public async Task>> GetTickets(int flightId) } } + /// + /// Retrieves all passengers associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of passenger DTOs linked to the flight. + /// Returns the list of associated passengers. + /// If the flight is not found. + /// If an unexpected error occurs. [HttpGet("{flightId}/passengers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/Airline.Api.Host/Controllers/ModelFamilyController.cs b/Airline.Api.Host/Controllers/ModelFamilyController.cs index ce129654a..c74c3b85f 100644 --- a/Airline.Api.Host/Controllers/ModelFamilyController.cs +++ b/Airline.Api.Host/Controllers/ModelFamilyController.cs @@ -1,18 +1,31 @@ using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.PlaneModel; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; +/// +/// Controller for managing aircraft model families and retrieving associated data. +/// Inherits from +/// to provide standardized CRUD operations. +/// [Route("api/[controller]")] public class ModelFamiliesController( IModelFamilyService modelFamilyService, ILogger logger ) : CrudControllerBase(modelFamilyService, logger) { + /// protected override int GetEntityId(ModelFamilyDto dto) => dto.Id; + /// + /// Retrieves all aircraft models associated with a specific model family. + /// + /// The unique identifier of the model family. + /// A list of aircraft model DTOs linked to the family. + /// Returns the list of associated aircraft models. + /// If the model family is not found. + /// If an unexpected error occurs. [HttpGet("{familyId}/models")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/Airline.Api.Host/Controllers/PassengerController.cs b/Airline.Api.Host/Controllers/PassengerController.cs index 9a6785c3a..8bb169864 100644 --- a/Airline.Api.Host/Controllers/PassengerController.cs +++ b/Airline.Api.Host/Controllers/PassengerController.cs @@ -2,18 +2,31 @@ using Airline.Application.Contracts.Passenger; using Airline.Application.Contracts.Ticket; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; +/// +/// Controller for managing passengers and retrieving associated data. +/// Inherits from +/// to provide standardized CRUD operations. +/// [Route("api/[controller]")] public class PassengersController( IPassengerService passengerService, ILogger logger ) : CrudControllerBase(passengerService, logger) { + /// protected override int GetEntityId(PassengerDto dto) => dto.Id; + /// + /// Retrieves all tickets associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of ticket DTOs linked to the passenger. + /// Returns the list of associated tickets. + /// If the passenger is not found. + /// If an unexpected error occurs. [HttpGet("{passengerId}/tickets")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -39,6 +52,14 @@ public async Task>> GetTickets(int passengerId) } } + /// + /// Retrieves all flights associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of flight DTOs linked to the passenger. + /// Returns the list of associated flights. + /// If the passenger is not found. + /// If an unexpected error occurs. [HttpGet("{passengerId}/flights")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/Airline.Api.Host/Controllers/PlaneModelController.cs b/Airline.Api.Host/Controllers/PlaneModelController.cs index 79e18a859..6c02895d4 100644 --- a/Airline.Api.Host/Controllers/PlaneModelController.cs +++ b/Airline.Api.Host/Controllers/PlaneModelController.cs @@ -1,18 +1,31 @@ using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.PlaneModel; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; +/// +/// Controller for managing aircraft models and retrieving associated data. +/// Inherits from +/// to provide standardized CRUD operations. +/// [Route("api/[controller]")] public class PlaneModelsController( IPlaneModelService planeModelService, ILogger logger ) : CrudControllerBase(planeModelService, logger) { + /// protected override int GetEntityId(PlaneModelDto dto) => dto.Id; + /// + /// Retrieves the model family associated with a specific aircraft model. + /// + /// The unique identifier of the aircraft model. + /// The model family DTO linked to the aircraft model. + /// Returns the associated model family. + /// If the aircraft model or model family is not found. + /// If an unexpected error occurs. [HttpGet("{modelId}/family")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/Airline.Api.Host/Controllers/TicketController.cs b/Airline.Api.Host/Controllers/TicketController.cs index 1e6752320..e8fe9787c 100644 --- a/Airline.Api.Host/Controllers/TicketController.cs +++ b/Airline.Api.Host/Controllers/TicketController.cs @@ -2,19 +2,31 @@ using Airline.Application.Contracts.Passenger; using Airline.Application.Contracts.Ticket; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; +/// +/// Controller for managing tickets and retrieving associated data. +/// Inherits from +/// to provide standardized CRUD operations. +/// [Route("api/[controller]")] public class TicketsController( ITicketService ticketService, ILogger logger ) : CrudControllerBase(ticketService, logger) { - + /// protected override int GetEntityId(TicketDto dto) => dto.Id; + /// + /// Retrieves the flight associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The flight DTO linked to the ticket. + /// Returns the associated flight. + /// If the ticket or flight is not found. + /// If an unexpected error occurs. [HttpGet("{ticketId}/flight")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -40,6 +52,14 @@ public async Task> GetFlight(int ticketId) } } + /// + /// Retrieves the passenger associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The passenger DTO linked to the ticket. + /// Returns the associated passenger. + /// If the ticket or passenger is not found. + /// If an unexpected error occurs. [HttpGet("{ticketId}/passenger")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index a55c4711f..175375966 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -1,5 +1,4 @@ using Airline.Application; -using Airline.Application.Contracts; using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.Passenger; @@ -18,26 +17,23 @@ var builder = WebApplication.CreateBuilder(args); -// ============= Aspire Service Defaults ============= builder.AddServiceDefaults(); -// ============= DataSeeder ============= builder.Services.AddSingleton(); -// ============= AutoMapper ============= builder.Services.AddAutoMapper(config => { config.AddProfile(new AirlineProfile()); }); -// ============= ============= +// Repositories builder.Services.AddTransient, FlightRepository>(); builder.Services.AddTransient, PassengerRepository>(); builder.Services.AddTransient, TicketRepository>(); builder.Services.AddTransient, PlaneModelRepository>(); builder.Services.AddTransient, ModelFamilyRepository>(); -// ============= ============= +// Application Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -45,20 +41,18 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -// ============= API ============= +// Controllers builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; }); +// Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); -// ============= Swagger XML- ============= builder.Services.AddSwaggerGen(c => { - //c.SwaggerDoc("v1", new OpenApiInfo { Title = "Airline API", Version = "v1" }); - var assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(a => a.GetName().Name!.StartsWith("Airline")) .Distinct(); @@ -72,14 +66,7 @@ } }); -// ============= MongoDB Aspire ============= -//builder.AddMongoDBClient("airline"); - -//builder.Services.AddDbContext((services, options) => -//{ -// var db = services.GetRequiredService(); -//options.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); -//}); +// MongoDB builder.AddMongoDBClient("airlineClient"); builder.Services.AddDbContext((services, o) => @@ -88,13 +75,10 @@ o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); }); - -// ============= ============= var app = builder.Build(); -app.MapDefaultEndpoints(); // Health checks Aspire +app.MapDefaultEndpoints(); -// Swagger if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -104,13 +88,11 @@ }); } -// ============= ============= using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); var seed = scope.ServiceProvider.GetRequiredService(); - // , Flights var flightsExist = dbContext.Flights.Any(); if (!flightsExist) { diff --git a/Airline.Application.Contracts/Flight/CreateFlightDto.cs b/Airline.Application.Contracts/Flight/CreateFlightDto.cs index 55772b62d..8ac342923 100644 --- a/Airline.Application.Contracts/Flight/CreateFlightDto.cs +++ b/Airline.Application.Contracts/Flight/CreateFlightDto.cs @@ -1,18 +1,20 @@ namespace Airline.Application.Contracts.Flight; /// -/// DTO for creating a new flight. +/// Data Transfer Object (DTO) for creating a new flight. +/// Contains scheduling and routing information for an airline flight. /// -/// Flight code (e.g., SU101). -/// City of departure. -/// City of arrival. -/// Date and time of departure. -/// Date and time of arrival. -/// ID of the plane model used for the flight. +/// The flight code (e.g., "SU101"). +/// The city of departure. +/// The city of arrival. +/// The scheduled departure date and time (local time). +/// The scheduled arrival date and time (local time). +/// The unique identifier of the associated aircraft model. public record CreateFlightDto( string FlightCode, string DepartureCity, string ArrivalCity, DateTime DepartureDateTime, DateTime ArrivalDateTime, - int ModelId); \ No newline at end of file + int ModelId +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Flight/FlightDto.cs b/Airline.Application.Contracts/Flight/FlightDto.cs index f55b6430f..48f03809b 100644 --- a/Airline.Application.Contracts/Flight/FlightDto.cs +++ b/Airline.Application.Contracts/Flight/FlightDto.cs @@ -1,15 +1,16 @@ namespace Airline.Application.Contracts.Flight; /// -/// DTO representing a flight. +/// Data Transfer Object (DTO) representing a flight in the airline system. +/// Used for read operations and data exchange with clients. /// -/// Unique identifier of the flight. -/// Flight code (e.g., SU101). -/// City of departure. -/// City of arrival. -/// Date and time of departure. -/// Date and time of arrival. -/// ID of the plane model used for the flight. +/// The unique identifier of the flight. +/// The flight code (e.g., "SU101"). +/// The city of departure. +/// The city of arrival. +/// The scheduled departure date and time (local time). +/// The scheduled arrival date and time (local time). +/// The unique identifier of the associated aircraft model. public record FlightDto( int Id, string FlightCode, @@ -17,4 +18,5 @@ public record FlightDto( string ArrivalCity, DateTime DepartureDateTime, DateTime ArrivalDateTime, - int ModelId); \ No newline at end of file + int ModelId +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Flight/IFlightService.cs b/Airline.Application.Contracts/Flight/IFlightService.cs index 6634af432..e6cbf03e2 100644 --- a/Airline.Application.Contracts/Flight/IFlightService.cs +++ b/Airline.Application.Contracts/Flight/IFlightService.cs @@ -4,10 +4,30 @@ namespace Airline.Application.Contracts.Flight; +/// +/// Service contract for managing flights in the airline system. +/// Extends the generic CRUD interface with flight-specific analytical operations. +/// public interface IFlightService : IApplicationService { - + /// + /// Retrieves the aircraft model associated with a specific flight. + /// + /// The unique identifier of the flight. + /// The aircraft model DTO linked to the flight. public Task GetPlaneModelAsync(int flightId); + + /// + /// Retrieves all tickets associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of ticket DTOs linked to the flight. public Task> GetTicketsAsync(int flightId); + + /// + /// Retrieves all passengers associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of passenger DTOs linked to the flight. public Task> GetPassengersAsync(int flightId); -} +} \ No newline at end of file diff --git a/Airline.Application.Contracts/IAnalyticService.cs b/Airline.Application.Contracts/IAnalyticService.cs index ac966a6c3..a8020624e 100644 --- a/Airline.Application.Contracts/IAnalyticService.cs +++ b/Airline.Application.Contracts/IAnalyticService.cs @@ -1,11 +1,46 @@ using Airline.Application.Contracts.Flight; using Airline.Application.Contracts.Passenger; +/// +/// Defines a service contract for analytical and reporting operations over airline data. +/// Provides aggregated queries for business intelligence, operational insights, and passenger analytics. +/// public interface IAnalyticsService { - public Task> GetTopFlightsByPassengerCountAsync(int top = 5); - public Task> GetFlightsWithMinTravelTimeAsync(); - public Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId); - public Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to); - public Task> GetFlightsByRouteAsync(string departure, string arrival); + /// + /// Retrieves the top N flights by number of passengers. + /// + /// The number of top flights to return (default: 5). + /// A list of flight DTOs sorted by passenger count in descending order. + public Task> GetTopFlightsByPassengerCountAsync(int top = 5); + + /// + /// Retrieves flights with the minimal travel time (duration). + /// + /// A list of flight DTOs with the shortest duration. + public Task> GetFlightsWithMinTravelTimeAsync(); + + /// + /// Retrieves passengers with zero baggage (no checked luggage) for a specific flight. + /// + /// The unique identifier of the flight. + /// A list of passenger DTOs who have no baggage on the specified flight. + public Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId); + + /// + /// Retrieves flights of a specific aircraft model within a given date period. + /// + /// The unique identifier of the aircraft model. + /// Start date of the period (inclusive). + /// End date of the period (inclusive). + /// A list of flight DTOs matching the model and date range. + public Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to); + + /// + /// Retrieves flights by route (departure city to arrival city). + /// + /// The city of departure. + /// The city of arrival. + /// A list of flight DTOs matching the specified route. + public Task> GetFlightsByRouteAsync(string departure, string arrival); } \ No newline at end of file diff --git a/Airline.Application.Contracts/IApplicationService.cs b/Airline.Application.Contracts/IApplicationService.cs index c42571621..ebd069f93 100644 --- a/Airline.Application.Contracts/IApplicationService.cs +++ b/Airline.Application.Contracts/IApplicationService.cs @@ -1,14 +1,62 @@ namespace Airline.Application.Contracts; +/// +/// Defines application service interface that provides +/// standardized CRUD (Create, Read, Update, Delete) operations for domain entities. +/// Serves as a contract between the application layer and presentation layer. +/// +/// +/// The Data Transfer Object (DTO) type used for read operations. +/// Represents the shape of data exposed to clients. +/// +/// +/// The Data Transfer Object (DTO) type used for create and update operations. +/// Contains only the fields required for mutation. +/// +/// +/// The type of the unique identifier for the entity (e.g., , ). +/// Must be a value type (). +/// public interface IApplicationService where TDto : class where TCreateUpdateDto : class where TKey : struct { + /// + /// Creates a new entity in the system. + /// + /// The data transfer object containing creation data. + /// The created entity as a DTO, typically with an assigned identifier. public Task CreateAsync(TCreateUpdateDto dto); + + /// + /// Retrieves an entity by its unique identifier. + /// + /// The unique identifier of the entity to retrieve. + /// The entity as a DTO if found; otherwise, . public Task GetByIdAsync(TKey id); + + /// + /// Retrieves all entities of the specified type. + /// + /// A list of all entities as DTOs. public Task> GetAllAsync(); + + /// + /// Updates an existing entity with new data. + /// + /// The data transfer object containing updated data. + /// The unique identifier of the entity to update. + /// The updated entity as a DTO. public Task UpdateAsync(TCreateUpdateDto dto, TKey id); - public Task DeleteAsync(TKey id); + /// + /// Deletes an entity by its unique identifier. + /// + /// The unique identifier of the entity to delete. + /// + /// if the entity was successfully deleted; + /// otherwise, . + /// + public Task DeleteAsync(TKey id); } \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs index 03267e44d..c0503e648 100644 --- a/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs +++ b/Airline.Application.Contracts/ModelFamily/CreateModelFamilyDto.cs @@ -1,8 +1,12 @@ namespace Airline.Application.Contracts.ModelFamily; /// -/// DTO for creating a new model family. +/// Data Transfer Object (DTO) for creating a new aircraft model family. +/// Represents a group of aircraft models with common design features. /// -/// Name of the model family. -/// Manufacturer of the model family. -public record CreateModelFamilyDto(string NameOfFamily, string ManufacturerName); \ No newline at end of file +/// The name of the aircraft model family (e.g., "A320 Family"). +/// The name of the manufacturer (e.g., "Airbus", "Boeing"). +public record CreateModelFamilyDto( + string NameOfFamily, + string ManufacturerName +); \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs index 6a38d68ac..d90168ae0 100644 --- a/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs +++ b/Airline.Application.Contracts/ModelFamily/IModelFamilyService.cs @@ -2,7 +2,16 @@ namespace Airline.Application.Contracts.ModelFamily; +/// +/// Service contract for managing aircraft model families in the airline system. +/// Extends the generic CRUD interface with family-specific analytical operations. +/// public interface IModelFamilyService : IApplicationService { + /// + /// Retrieves all aircraft models associated with a specific model family. + /// + /// The unique identifier of the model family. + /// A list of aircraft model DTOs linked to the family. public Task> GetPlaneModelsAsync(int familyId); -} +} \ No newline at end of file diff --git a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs index 010eb3d28..930e74143 100644 --- a/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs +++ b/Airline.Application.Contracts/ModelFamily/ModelFamilyDto.cs @@ -1,10 +1,14 @@ namespace Airline.Application.Contracts.ModelFamily; /// -/// DTO representing a model family. -/// Contains basic information about the family. +/// Data Transfer Object (DTO) representing an aircraft model family in the airline system. +/// Used for read operations and data exchange with clients. /// -/// Unique identifier of the model family. -/// Name of the model family. -/// Manufacturer of the model family. -public record ModelFamilyDto(int Id, string NameOfFamily, string ManufacturerName); \ No newline at end of file +/// The unique identifier of the model family. +/// The name of the aircraft model family (e.g., "A320 Family"). +/// The name of the manufacturer (e.g., "Airbus", "Boeing"). +public record ModelFamilyDto( + int Id, + string NameOfFamily, + string ManufacturerName +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs index 5f8f26e12..5c4f25168 100644 --- a/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs +++ b/Airline.Application.Contracts/Passenger/CreatePassengerDto.cs @@ -1,9 +1,14 @@ namespace Airline.Application.Contracts.Passenger; /// -/// DTO for creating a new passenger. +/// Data Transfer Object (DTO) for creating a new passenger. +/// Contains personal identification and demographic data. /// -/// Passport number. -/// Full name of the passenger. -/// Date of birth (YYYY-MM-DD). -public record CreatePassengerDto(string Passport, string PassengerName, DateOnly DateOfBirth); \ No newline at end of file +/// The passport number of the passenger. +/// The full name of the passenger (must not contain digits). +/// The date of birth of the passenger. +public record CreatePassengerDto( + string Passport, + string PassengerName, + DateOnly DateOfBirth +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/IPassengerService.cs b/Airline.Application.Contracts/Passenger/IPassengerService.cs index e2553f44c..203cc53d0 100644 --- a/Airline.Application.Contracts/Passenger/IPassengerService.cs +++ b/Airline.Application.Contracts/Passenger/IPassengerService.cs @@ -3,8 +3,23 @@ namespace Airline.Application.Contracts.Passenger; +/// +/// Service contract for managing passengers in the airline system. +/// Extends the generic CRUD interface with passenger-specific analytical operations. +/// public interface IPassengerService : IApplicationService { + /// + /// Retrieves all tickets associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of ticket DTOs linked to the passenger. public Task> GetTicketsAsync(int passengerId); + + /// + /// Retrieves all flights associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of flight DTOs linked to the passenger. public Task> GetFlightsAsync(int passengerId); } \ No newline at end of file diff --git a/Airline.Application.Contracts/Passenger/PassengerDto.cs b/Airline.Application.Contracts/Passenger/PassengerDto.cs index b8e2949f3..abc7a6b23 100644 --- a/Airline.Application.Contracts/Passenger/PassengerDto.cs +++ b/Airline.Application.Contracts/Passenger/PassengerDto.cs @@ -1,10 +1,16 @@ namespace Airline.Application.Contracts.Passenger; /// -/// DTO representing a passenger. +/// Data Transfer Object (DTO) representing a passenger in the airline system. +/// Used for read operations and data exchange with clients. /// -/// Unique identifier of the passenger. -/// Passport number. -/// Full name of the passenger. -/// Date of birth (YYYY-MM-DD). -public record PassengerDto(int Id, string Passport, string PassengerName, DateOnly DateOfBirth); \ No newline at end of file +/// The unique identifier of the passenger. +/// The passport number of the passenger. +/// The full name of the passenger. +/// The date of birth of the passenger. +public record PassengerDto( + int Id, + string Passport, + string PassengerName, + DateOnly DateOfBirth +); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs index f0df96816..cf8f6a06a 100644 --- a/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs +++ b/Airline.Application.Contracts/PlaneModel/CreatePlaneModelDto.cs @@ -1,16 +1,18 @@ namespace Airline.Application.Contracts.PlaneModel; /// -/// DTO for creating a new plane model. +/// Data Transfer Object (DTO) for creating a new aircraft model. +/// Contains technical specifications and references to its model family. /// -/// Name of the plane model. -/// ID of the associated model family. -/// Maximum flight range (km). -/// Passenger capacity. -/// Cargo capacity (tons). +/// The name of the aircraft model (e.g., "A320"). +/// The unique identifier of the associated model family. +/// The maximum flight range in kilometers (must be non-negative). +/// The number of passenger seats (must be non-negative). +/// The cargo capacity in tons (must be non-negative). public record CreatePlaneModelDto( string ModelName, int ModelFamilyId, double MaxRange, double PassengerCapacity, - double CargoCapacity); \ No newline at end of file + double CargoCapacity +); \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs index 73150f6bb..3461b208e 100644 --- a/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs +++ b/Airline.Application.Contracts/PlaneModel/IPlaneModelService.cs @@ -2,7 +2,16 @@ namespace Airline.Application.Contracts.PlaneModel; +/// +/// Service contract for managing aircraft models in the airline system. +/// Extends the generic CRUD interface with model-specific analytical operations. +/// public interface IPlaneModelService : IApplicationService { + /// + /// Retrieves the model family associated with a specific aircraft model. + /// + /// The unique identifier of the aircraft model. + /// The model family DTO linked to the aircraft model. public Task GetModelFamilyAsync(int modelId); -} +} \ No newline at end of file diff --git a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs index b1592ec6e..5590a56d1 100644 --- a/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs +++ b/Airline.Application.Contracts/PlaneModel/PlaneModelDto.cs @@ -1,18 +1,20 @@ namespace Airline.Application.Contracts.PlaneModel; /// -/// DTO representing a plane model. +/// Data Transfer Object (DTO) representing an aircraft model in the airline system. +/// Used for read operations and data exchange with clients. /// -/// Unique identifier of the plane model. -/// Name of the plane model. -/// ID of the associated model family. -/// Maximum flight range (km). -/// Passenger capacity. -/// Cargo capacity (tons). +/// The unique identifier of the aircraft model. +/// The name of the aircraft model (e.g., "A320"). +/// The unique identifier of the associated model family. +/// The maximum flight range in kilometers. +/// The number of passenger seats. +/// The cargo capacity in tons. public record PlaneModelDto( int Id, string ModelName, int ModelFamilyId, double MaxRange, double PassengerCapacity, - double CargoCapacity); \ No newline at end of file + double CargoCapacity +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs index d2ebe2e9c..11ca5e2be 100644 --- a/Airline.Application.Contracts/Ticket/CreateTicketDto.cs +++ b/Airline.Application.Contracts/Ticket/CreateTicketDto.cs @@ -1,18 +1,18 @@ namespace Airline.Application.Contracts.Ticket; /// -/// DTO for creating a new ticket. +/// Data Transfer Object (DTO) for creating a new ticket. +/// Contains all required fields for ticket creation in the airline system. /// -/// ID of the associated flight. -/// ID of the associated passenger. -/// Seat number (e.g., 12A). -/// Indicates if hand luggage is present. -/// Total baggage weight in kilograms (null if no baggage). -public record CreateTicketDto -( +/// The unique identifier of the associated flight. +/// The unique identifier of the associated passenger. +/// The assigned seat number (e.g., "12A"). +/// Indicates whether hand luggage is carried. +/// The total checked baggage weight in kilograms (null if no baggage). +public record CreateTicketDto( int FlightId, int PassengerId, string SeatNumber, bool HandLuggage, double? BaggageWeight - ); \ No newline at end of file +); \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/ITicketService.cs b/Airline.Application.Contracts/Ticket/ITicketService.cs index e52fe3079..f57990240 100644 --- a/Airline.Application.Contracts/Ticket/ITicketService.cs +++ b/Airline.Application.Contracts/Ticket/ITicketService.cs @@ -3,8 +3,23 @@ namespace Airline.Application.Contracts.Ticket; +/// +/// Service contract for managing tickets in the airline system. +/// Extends the generic CRUD interface with ticket-specific analytical operations. +/// public interface ITicketService : IApplicationService { + /// + /// Retrieves the flight associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The flight DTO linked to the ticket. public Task GetFlightAsync(int ticketId); + + /// + /// Retrieves the passenger associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The passenger DTO linked to the ticket. public Task GetPassengerAsync(int ticketId); -} +} \ No newline at end of file diff --git a/Airline.Application.Contracts/Ticket/TicketDto.cs b/Airline.Application.Contracts/Ticket/TicketDto.cs index c5d413b3e..d71d3811a 100644 --- a/Airline.Application.Contracts/Ticket/TicketDto.cs +++ b/Airline.Application.Contracts/Ticket/TicketDto.cs @@ -1,18 +1,20 @@ namespace Airline.Application.Contracts.Ticket; /// -/// DTO representing a ticket. +/// Data Transfer Object (DTO) representing a ticket in the airline system. +/// Used for read operations and data exchange with clients. /// -/// Unique identifier of the ticket. -/// ID of the associated flight. -/// ID of the associated passenger. -/// Seat number (e.g., 12A). -/// Indicates if hand luggage is present. -/// Total baggage weight in kilograms (null if no baggage). +/// The unique identifier of the ticket. +/// The unique identifier of the associated flight. +/// The unique identifier of the associated passenger. +/// The assigned seat number (e.g., "12A"). +/// Indicates whether hand luggage is carried. +/// The total checked baggage weight in kilograms (null if no baggage). public record TicketDto( int Id, int FlightId, int PassengerId, string SeatNumber, bool HandLuggage, - double? BaggageWeight); \ No newline at end of file + double? BaggageWeight +); \ No newline at end of file diff --git a/Airline.Application/AirlineMapperProfile.cs b/Airline.Application/AirlineMapperProfile.cs index f0e648897..08ec8b2b1 100644 --- a/Airline.Application/AirlineMapperProfile.cs +++ b/Airline.Application/AirlineMapperProfile.cs @@ -8,23 +8,36 @@ namespace Airline.Application; +/// +/// AutoMapper profile that defines mapping configurations between +/// domain entities and Data Transfer Objects (DTOs). +/// Ensures consistent and safe conversion between layers of the application. +/// public class AirlineProfile : Profile { - // Конструктор профиля, создающий связи между Entity и Dto классами + /// + /// Initializes a new instance of the class + /// and configures all entity-to-DTO and DTO-to-entity mappings. + /// public AirlineProfile() { + // ModelFamily mappings CreateMap(); CreateMap(); + // PlaneModel mappings CreateMap(); CreateMap(); + // Flight mappings CreateMap(); CreateMap(); + // Passenger mappings CreateMap(); CreateMap(); + // Ticket mappings CreateMap(); CreateMap(); } diff --git a/Airline.Application/Services/AnalyticService.cs b/Airline.Application/Services/AnalyticService.cs index efcfd9ce9..ad0628637 100644 --- a/Airline.Application/Services/AnalyticService.cs +++ b/Airline.Application/Services/AnalyticService.cs @@ -6,6 +6,10 @@ namespace Airline.Application.Services; +/// +/// Provides analytical and reporting capabilities over airline data. +/// Implements aggregated queries for business intelligence and operational insights. +/// public class AnalyticsService( IRepository flightRepository, IRepository ticketRepository, @@ -13,12 +17,16 @@ public class AnalyticsService( IMapper mapper ) : IAnalyticsService { + /// + /// Retrieves the top N flights by number of passengers. + /// + /// The number of top flights to return (default: 5). + /// A list of flight DTOs sorted by passenger count in descending order. public async Task> GetTopFlightsByPassengerCountAsync(int top = 5) { var flights = await flightRepository.GetAllAsync(); var tickets = await ticketRepository.GetAllAsync(); - // Группируем билеты по FlightId и считаем количество var flightPassengerCounts = tickets .GroupBy(t => t.FlightId) .Select(g => new { FlightId = g.Key, Count = g.Count() }) @@ -27,12 +35,10 @@ public async Task> GetTopFlightsByPassengerCountAsync(int top = .Select(x => x.FlightId) .ToHashSet(); - // Получаем полные объекты рейсов var topFlights = flights .Where(f => flightPassengerCounts.Contains(f.Id)) .ToList(); - // Маппим в DTO и сортируем по убыванию числа пассажиров var flightCountMap = tickets .GroupBy(t => t.FlightId) .ToDictionary(g => g.Key, g => g.Count()); @@ -43,6 +49,10 @@ public async Task> GetTopFlightsByPassengerCountAsync(int top = .ToList(); } + /// + /// Retrieves flights with the minimal travel time. + /// + /// A list of flight DTOs with the shortest duration. public async Task> GetFlightsWithMinTravelTimeAsync() { var flights = await flightRepository.GetAllAsync(); @@ -55,9 +65,16 @@ public async Task> GetFlightsWithMinTravelTimeAsync() .ToList(); } + /// + /// Retrieves passengers with zero baggage for a specific flight. + /// + /// The unique identifier of the flight. + /// A list of passenger DTOs with no baggage, sorted by full name. + /// + /// Thrown if the specified flight does not exist. + /// public async Task> GetPassengersWithZeroBaggageOnFlightAsync(int flightId) { - // Убеждаемся, что рейс существует var flight = await flightRepository.GetAsync(flightId); if (flight == null) throw new KeyNotFoundException($"Flight with ID '{flightId}' not found."); @@ -78,6 +95,13 @@ public async Task> GetPassengersWithZeroBaggageOnFlightAsync( .ToList(); } + /// + /// Retrieves flights of a specific aircraft model within a date period. + /// + /// The unique identifier of the aircraft model. + /// Start date of the period (inclusive). + /// End date of the period (inclusive). + /// A list of flight DTOs matching the criteria. public async Task> GetFlightsByModelInPeriodAsync(int modelId, DateTime from, DateTime to) { var flights = await flightRepository.GetAllAsync(); @@ -89,6 +113,12 @@ public async Task> GetFlightsByModelInPeriodAsync(int modelId, D .ToList(); } + /// + /// Retrieves flights by route (departure city → arrival city). + /// + /// The city of departure. + /// The city of arrival. + /// A list of flight DTOs matching the route. public async Task> GetFlightsByRouteAsync(string departure, string arrival) { var flights = await flightRepository.GetAllAsync(); diff --git a/Airline.Application/Services/FlightService.cs b/Airline.Application/Services/FlightService.cs index 4e1d2dd5c..ff8b93de3 100644 --- a/Airline.Application/Services/FlightService.cs +++ b/Airline.Application/Services/FlightService.cs @@ -8,6 +8,11 @@ namespace Airline.Application.Services; +/// +/// Provides business logic for managing flights in the airline system. +/// Handles CRUD operations, validation of aircraft model existence, +/// and retrieval of associated tickets, passengers, and aircraft models. +/// public class FlightService( IRepository flightRepository, IRepository planeModelRepository, @@ -16,6 +21,15 @@ public class FlightService( IMapper mapper ) : IFlightService { + /// + /// Creates a new flight. + /// Validates that the associated aircraft model exists. + /// + /// The flight creation data transfer object. + /// The created flight DTO. + /// + /// Thrown if the specified aircraft model does not exist. + /// public async Task CreateAsync(CreateFlightDto dto) { if (await planeModelRepository.GetAsync(dto.ModelId) == null) @@ -33,15 +47,33 @@ public async Task CreateAsync(CreateFlightDto dto) return mapper.Map(created); } + /// + /// Retrieves a flight by its unique identifier. + /// + /// The unique identifier of the flight. + /// The flight DTO if found; otherwise, null. public async Task GetByIdAsync(int id) { var entity = await flightRepository.GetAsync(id); return entity == null ? null : mapper.Map(entity); } + /// + /// Retrieves all flights in the system. + /// + /// A list of all flight DTOs. public async Task> GetAllAsync() => (await flightRepository.GetAllAsync()).Select(mapper.Map).ToList(); + /// + /// Updates an existing flight with new data. + /// + /// The updated flight data transfer object. + /// The unique identifier of the flight to update. + /// The updated flight DTO. + /// + /// Thrown if the flight does not exist. + /// public async Task UpdateAsync(CreateFlightDto dto, int id) { var existing = await flightRepository.GetAsync(id) @@ -51,8 +83,24 @@ public async Task UpdateAsync(CreateFlightDto dto, int id) return mapper.Map(updated); } + /// + /// Deletes a flight by its unique identifier. + /// + /// The unique identifier of the flight to delete. + /// True if the flight was deleted; otherwise, false. public async Task DeleteAsync(int id) => await flightRepository.DeleteAsync(id); + /// + /// Retrieves the aircraft model associated with a specific flight. + /// + /// The unique identifier of the flight. + /// The aircraft model DTO associated with the flight. + /// + /// Thrown if the flight does not exist. + /// + /// + /// Thrown if the associated aircraft model is missing (data integrity issue). + /// public async Task GetPlaneModelAsync(int flightId) { var flight = await flightRepository.GetAsync(flightId) @@ -62,6 +110,14 @@ public async Task GetPlaneModelAsync(int flightId) return mapper.Map(model); } + /// + /// Retrieves all tickets associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of ticket DTOs linked to the flight. + /// + /// Thrown if the flight does not exist. + /// public async Task> GetTicketsAsync(int flightId) { var flight = await flightRepository.GetAsync(flightId) != null; @@ -72,6 +128,14 @@ public async Task> GetTicketsAsync(int flightId) .Select(mapper.Map).ToList(); } + /// + /// Retrieves all passengers associated with a specific flight. + /// + /// The unique identifier of the flight. + /// A list of passenger DTOs linked to the flight. + /// + /// Thrown if the flight does not exist. + /// public async Task> GetPassengersAsync(int flightId) { var flight = await flightRepository.GetAsync(flightId) != null; diff --git a/Airline.Application/Services/ModelFamilyService.cs b/Airline.Application/Services/ModelFamilyService.cs index d9833f1c0..4d26179c1 100644 --- a/Airline.Application/Services/ModelFamilyService.cs +++ b/Airline.Application/Services/ModelFamilyService.cs @@ -6,12 +6,21 @@ namespace Airline.Application.Services; +/// +/// Provides business logic for managing aircraft model families in the airline system. +/// Handles CRUD operations and retrieval of associated aircraft models. +/// public class ModelFamilyService( IRepository planeModelRepository, IRepository familyRepository, IMapper mapper ) : IModelFamilyService { + /// + /// Creates a new aircraft model family. + /// + /// The model family creation data transfer object. + /// The created model family DTO. public async Task CreateAsync(CreateModelFamilyDto dto) { var modelFamily = mapper.Map(dto); @@ -26,15 +35,33 @@ public async Task CreateAsync(CreateModelFamilyDto dto) return mapper.Map(created); } + /// + /// Retrieves a model family by its unique identifier. + /// + /// The unique identifier of the model family. + /// The model family DTO if found; otherwise, null. public async Task GetByIdAsync(int id) { var entity = await familyRepository.GetAsync(id); return entity == null ? null : mapper.Map(entity); } + /// + /// Retrieves all aircraft model families in the system. + /// + /// A list of all model family DTOs. public async Task> GetAllAsync() => (await familyRepository.GetAllAsync()).Select(mapper.Map).ToList(); + /// + /// Updates an existing model family with new data. + /// + /// The updated model family data transfer object. + /// The unique identifier of the model family to update. + /// The updated model family DTO. + /// + /// Thrown if the model family does not exist. + /// public async Task UpdateAsync(CreateModelFamilyDto dto, int id) { var existing = await familyRepository.GetAsync(id) @@ -44,8 +71,21 @@ public async Task UpdateAsync(CreateModelFamilyDto dto, int id) return mapper.Map(updated); } + /// + /// Deletes a model family by its unique identifier. + /// + /// The unique identifier of the model family to delete. + /// True if the model family was deleted; otherwise, false. public async Task DeleteAsync(int id) => await familyRepository.DeleteAsync(id); + /// + /// Retrieves all aircraft models associated with a specific model family. + /// + /// The unique identifier of the model family. + /// A list of aircraft model DTOs linked to the family. + /// + /// Thrown if the model family does not exist. + /// public async Task> GetPlaneModelsAsync(int familyId) { var family = await familyRepository.GetAsync(familyId); diff --git a/Airline.Application/Services/PassengerService.cs b/Airline.Application/Services/PassengerService.cs index 0a12f4db7..d11516cd7 100644 --- a/Airline.Application/Services/PassengerService.cs +++ b/Airline.Application/Services/PassengerService.cs @@ -5,10 +5,14 @@ using Airline.Domain.Items; using AutoMapper; using System.Text.RegularExpressions; -using System.Xml.Linq; namespace Airline.Application.Services; +/// +/// Provides business logic for managing passengers in the airline system. +/// Handles CRUD operations, validation of personal data (name must not contain digits), +/// and retrieval of associated tickets and flights. +/// public class PassengerService( IRepository passengerRepository, IRepository ticketRepository, @@ -16,6 +20,15 @@ public class PassengerService( IMapper mapper ) : IPassengerService { + /// + /// Creates a new passenger. + /// Validates that the passenger name is not empty and does not contain digits. + /// + /// The passenger creation data transfer object. + /// The created passenger DTO. + /// + /// Thrown if the passenger name is empty, whitespace, or contains digits. + /// public async Task CreateAsync(CreatePassengerDto dto) { var passenger = mapper.Map(dto); @@ -31,15 +44,38 @@ public async Task CreateAsync(CreatePassengerDto dto) var created = await passengerRepository.CreateAsync(passenger); return mapper.Map(created); } + + /// + /// Retrieves a passenger by its unique identifier. + /// + /// The unique identifier of the passenger. + /// The passenger DTO if found; otherwise, null. public async Task GetByIdAsync(int id) { var entity = await passengerRepository.GetAsync(id); return entity == null ? null : mapper.Map(entity); } + /// + /// Retrieves all passengers in the system. + /// + /// A list of all passenger DTOs. public async Task> GetAllAsync() => (await passengerRepository.GetAllAsync()).Select(mapper.Map).ToList(); + /// + /// Updates an existing passenger with new data. + /// Validates that the passenger name is not empty and does not contain digits. + /// + /// The updated passenger data transfer object. + /// The unique identifier of the passenger to update. + /// The updated passenger DTO. + /// + /// Thrown if the passenger does not exist. + /// + /// + /// Thrown if the passenger name is empty, whitespace, or contains digits. + /// public async Task UpdateAsync(CreatePassengerDto dto, int id) { var existing = await passengerRepository.GetAsync(id) @@ -51,8 +87,21 @@ public async Task UpdateAsync(CreatePassengerDto dto, int id) return mapper.Map(updated); } + /// + /// Deletes a passenger by its unique identifier. + /// + /// The unique identifier of the passenger to delete. + /// True if the passenger was deleted; otherwise, false. public async Task DeleteAsync(int id) => await passengerRepository.DeleteAsync(id); + /// + /// Retrieves all tickets associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of ticket DTOs linked to the passenger. + /// + /// Thrown if the passenger does not exist. + /// public async Task> GetTicketsAsync(int passengerId) { var passengerExists = await passengerRepository.GetAsync(passengerId) != null; @@ -63,6 +112,14 @@ public async Task> GetTicketsAsync(int passengerId) .Select(mapper.Map).ToList(); } + /// + /// Retrieves all flights associated with a specific passenger. + /// + /// The unique identifier of the passenger. + /// A list of flight DTOs linked to the passenger. + /// + /// Thrown if the passenger does not exist. + /// public async Task> GetFlightsAsync(int passengerId) { var passengerExists = await passengerRepository.GetAsync(passengerId) != null; diff --git a/Airline.Application/Services/PlaneModelService.cs b/Airline.Application/Services/PlaneModelService.cs index fa36f9d05..a4c665c97 100644 --- a/Airline.Application/Services/PlaneModelService.cs +++ b/Airline.Application/Services/PlaneModelService.cs @@ -6,13 +6,29 @@ namespace Airline.Application.Services; +/// +/// Provides business logic for managing aircraft models in the airline system. +/// Handles CRUD operations, validation of technical specifications, +/// and retrieval of associated model families. +/// public class PlaneModelService( IRepository planeModelRepository, IRepository familyRepository, IMapper mapper ) : IPlaneModelService { - + /// + /// Creates a new aircraft model. + /// Validates that the model family exists and that capacity values are non-negative. + /// + /// The aircraft model creation data transfer object. + /// The created aircraft model DTO. + /// + /// Thrown if the specified model family does not exist. + /// + /// + /// Thrown if passenger capacity or cargo capacity is negative. + /// public async Task CreateAsync(CreatePlaneModelDto dto) { if (await familyRepository.GetAsync(dto.ModelFamilyId) == null) @@ -32,15 +48,37 @@ public async Task CreateAsync(CreatePlaneModelDto dto) return mapper.Map(created); } + /// + /// Retrieves an aircraft model by its unique identifier. + /// + /// The unique identifier of the aircraft model. + /// The aircraft model DTO if found; otherwise, null. public async Task GetByIdAsync(int id) { var entity = await planeModelRepository.GetAsync(id); return entity == null ? null : mapper.Map(entity); } + /// + /// Retrieves all aircraft models in the system. + /// + /// A list of all aircraft model DTOs. public async Task> GetAllAsync() => (await planeModelRepository.GetAllAsync()).Select(mapper.Map).ToList(); + /// + /// Updates an existing aircraft model with new data. + /// Validates that capacity values are non-negative. + /// + /// The updated aircraft model data transfer object. + /// The unique identifier of the model to update. + /// The updated aircraft model DTO. + /// + /// Thrown if the aircraft model does not exist. + /// + /// + /// Thrown if passenger capacity or cargo capacity is negative. + /// public async Task UpdateAsync(CreatePlaneModelDto dto, int id) { var existing = await planeModelRepository.GetAsync(id) @@ -52,8 +90,24 @@ public async Task UpdateAsync(CreatePlaneModelDto dto, int id) return mapper.Map(updated); } + /// + /// Deletes an aircraft model by its unique identifier. + /// + /// The unique identifier of the model to delete. + /// True if the model was deleted; otherwise, false. public async Task DeleteAsync(int id) => await planeModelRepository.DeleteAsync(id); + /// + /// Retrieves the model family associated with a specific aircraft model. + /// + /// The unique identifier of the aircraft model. + /// The model family DTO associated with the model. + /// + /// Thrown if the aircraft model does not exist. + /// + /// + /// Thrown if the associated model family is missing (data integrity issue). + /// public async Task GetModelFamilyAsync(int modelId) { var model = await planeModelRepository.GetAsync(modelId) diff --git a/Airline.Application/Services/TicketService.cs b/Airline.Application/Services/TicketService.cs index 4563ff802..c74909faf 100644 --- a/Airline.Application/Services/TicketService.cs +++ b/Airline.Application/Services/TicketService.cs @@ -7,6 +7,10 @@ namespace Airline.Application.Services; +/// +/// Provides business logic for managing tickets in the airline system. +/// Handles CRUD operations, validation, and retrieval of related entities (flight, passenger). +/// public class TicketService( IRepository ticketRepository, IRepository flightRepository, @@ -14,6 +18,18 @@ public class TicketService( IMapper mapper ) : ITicketService { + /// + /// Creates a new ticket for a passenger on a specific flight. + /// Validates that the flight and passenger exist and that baggage weight is non-negative. + /// + /// The ticket creation data transfer object. + /// The created ticket DTO. + /// + /// Thrown if the specified flight or passenger does not exist. + /// + /// + /// Thrown if baggage weight is negative. + /// public async Task CreateAsync(CreateTicketDto dto) { if (await flightRepository.GetAsync(dto.FlightId) == null) @@ -35,15 +51,37 @@ public async Task CreateAsync(CreateTicketDto dto) return mapper.Map(created); } + /// + /// Retrieves a ticket by its unique identifier. + /// + /// The unique identifier of the ticket. + /// The ticket DTO if found; otherwise, null. public async Task GetByIdAsync(int id) { var entity = await ticketRepository.GetAsync(id); return entity == null ? null : mapper.Map(entity); } + /// + /// Retrieves all tickets in the system. + /// + /// A list of all ticket DTOs. public async Task> GetAllAsync() => (await ticketRepository.GetAllAsync()).Select(mapper.Map).ToList(); + /// + /// Updates an existing ticket with new data. + /// Validates that baggage weight is non-negative. + /// + /// The updated ticket data transfer object. + /// The unique identifier of the ticket to update. + /// The updated ticket DTO. + /// + /// Thrown if the ticket does not exist. + /// + /// + /// Thrown if baggage weight is negative. + /// public async Task UpdateAsync(CreateTicketDto dto, int id) { var existing = await ticketRepository.GetAsync(id) @@ -55,8 +93,24 @@ public async Task UpdateAsync(CreateTicketDto dto, int id) return mapper.Map(updated); } + /// + /// Deletes a ticket by its unique identifier. + /// + /// The unique identifier of the ticket to delete. + /// True if the ticket was deleted; otherwise, false. public async Task DeleteAsync(int id) => await ticketRepository.DeleteAsync(id); + /// + /// Retrieves the flight associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The flight DTO associated with the ticket. + /// + /// Thrown if the ticket does not exist. + /// + /// + /// Thrown if the associated flight is missing (data integrity issue). + /// public async Task GetFlightAsync(int ticketId) { var ticket = await ticketRepository.GetAsync(ticketId) @@ -66,6 +120,17 @@ public async Task GetFlightAsync(int ticketId) return mapper.Map(flight); } + /// + /// Retrieves the passenger associated with a specific ticket. + /// + /// The unique identifier of the ticket. + /// The passenger DTO associated with the ticket. + /// + /// Thrown if the ticket does not exist. + /// + /// + /// Thrown if the associated passenger is missing (data integrity issue). + /// public async Task GetPassengerAsync(int ticketId) { var ticket = await ticketRepository.GetAsync(ticketId) diff --git a/Airline.Domain/IRepository.cs b/Airline.Domain/IRepository.cs index a78822266..86d0cc24e 100644 --- a/Airline.Domain/IRepository.cs +++ b/Airline.Domain/IRepository.cs @@ -4,51 +4,49 @@ /// Defines a generic repository interface that provides /// basic CRUD (Create, Read, Update, Delete) operations. /// -/// -/// The type of the entity being managed by the repository. -/// -/// -/// The type of the entity's unique identifier (e.g., for MongoDB). -/// +/// The type of the entity being managed. +/// The type of the entity's unique identifier. public interface IRepository where TEntity : class { /// - /// Adds a new entity to the repository. + /// Creates a new entity in the data store. /// - /// The entity instance to add. - /// The created entity. + /// The entity instance to create. + /// The created entity, typically with an assigned identifier. public Task CreateAsync(TEntity entity); /// - /// Retrieves an entity from the repository by its identifier. + /// Retrieves an entity from the data store by its unique identifier. /// - /// The unique identifier of the entity. + /// The unique identifier of the entity to retrieve. /// - /// The entity with the specified identifier, or - /// if no entity with such an identifier exists. + /// The entity if found; otherwise, . /// public Task GetAsync(TKey id); /// - /// Retrieves all entities stored in the repository. + /// Retrieves all entities of the specified type from the data store. /// /// - /// A list containing all entities in the repository. + /// A list containing all entities of the specified type. /// public Task> GetAllAsync(); /// - /// Updates an existing entity in the repository. + /// Updates an existing entity in the data store. /// /// The entity instance containing updated data. /// The updated entity. public Task UpdateAsync(TEntity entity); /// - /// Removes an entity from the repository by its identifier. + /// Deletes an entity from the data store by its unique identifier. /// /// The unique identifier of the entity to delete. - /// if the entity was deleted; otherwise, . + /// + /// if the entity was successfully deleted; + /// otherwise, . + /// public Task DeleteAsync(TKey id); } \ No newline at end of file diff --git a/Airline.Domain/Items/Flight.cs b/Airline.Domain/Items/Flight.cs index 7f14fcd0b..fcf3b68e7 100644 --- a/Airline.Domain/Items/Flight.cs +++ b/Airline.Domain/Items/Flight.cs @@ -48,5 +48,5 @@ public class Flight /// /// The id of the model. /// - public int ModelId { get; set; } // ← ссылка на PlaneModel + public int ModelId { get; set; } } \ No newline at end of file diff --git a/Airline.Domain/Items/Ticket.cs b/Airline.Domain/Items/Ticket.cs index 300fb2ead..1d5bd0ff6 100644 --- a/Airline.Domain/Items/Ticket.cs +++ b/Airline.Domain/Items/Ticket.cs @@ -38,10 +38,10 @@ public class Ticket /// /// The id to connect between the ticket and the flight. /// - public int FlightId { get; set; } // ← ссылка на Flight + public int FlightId { get; set; } /// /// The id to connect between the ticket and the passenger. /// - public int PassengerId { get; set; } // ← ссылка на Passenger + public int PassengerId { get; set; } } \ No newline at end of file diff --git a/Airline.Infrastructure.EfCore/AirlineDbContext.cs b/Airline.Infrastructure.EfCore/AirlineDbContext.cs index 6b0d5215c..032c590e6 100644 --- a/Airline.Infrastructure.EfCore/AirlineDbContext.cs +++ b/Airline.Infrastructure.EfCore/AirlineDbContext.cs @@ -5,45 +5,50 @@ namespace Airline.Infrastructure.EfCore; /// -/// Database context for the airline management system. -/// Configures entity mappings and collections for MongoDB. +/// Represents the database context for the airline management system. +/// Configures entity-to-collection mappings and property name conventions for MongoDB. /// public class AirlineDbContext(DbContextOptions options) : DbContext(options) { /// - /// Collection of aircraft model families. + /// Gets or sets the collection of aircraft model families. + /// Mapped to the 'model_families' collection in MongoDB. /// public DbSet ModelFamilies { get; set; } /// - /// Collection of aircraft models. + /// Gets or sets the collection of aircraft models. + /// Mapped to the 'plane_models' collection in MongoDB. /// public DbSet PlaneModels { get; set; } /// - /// Collection of flights. + /// Gets or sets the collection of flights. + /// Mapped to the 'flights' collection in MongoDB. /// public DbSet Flights { get; set; } /// - /// Collection of passengers. + /// Gets or sets the collection of passengers. + /// Mapped to the 'passengers' collection in MongoDB. /// public DbSet Passengers { get; set; } /// - /// Collection of tickets. + /// Gets or sets the collection of tickets. + /// Mapped to the 'tickets' collection in MongoDB. /// public DbSet Tickets { get; set; } /// - /// Configures entity-to-collection mappings and property names for MongoDB. + /// Configures the model by mapping entities to MongoDB collections and customizing field names. + /// Disables automatic transaction behavior (MongoDB does not support transactions in this context). /// + /// The model builder used to configure entity mappings. protected override void OnModelCreating(ModelBuilder modelBuilder) { - Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - // ModelFamily → collection "model_families" modelBuilder.Entity(entity => { entity.ToCollection("model_families"); @@ -53,7 +58,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(f => f.ManufacturerName).HasElementName("manufacturer"); }); - // PlaneModel → collection "plane_models" modelBuilder.Entity(entity => { entity.ToCollection("plane_models"); @@ -66,7 +70,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(m => m.ModelFamilyId).HasElementName("family_id"); }); - // Passenger → collection "passengers" modelBuilder.Entity(entity => { entity.ToCollection("passengers"); @@ -77,7 +80,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(p => p.DateOfBirth).HasElementName("date_of_birth"); }); - // Flight → collection "flights" modelBuilder.Entity(entity => { entity.ToCollection("flights"); @@ -88,10 +90,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(f => f.ArrivalCity).HasElementName("arrival_city"); entity.Property(f => f.DepartureDateTime).HasElementName("departure_datetime"); entity.Property(f => f.ArrivalDateTime).HasElementName("arrival_datetime"); - entity.Property(f => f.ModelId).HasElementName("plane_model_id"); // ← ссылка на PlaneModel + entity.Property(f => f.ModelId).HasElementName("plane_model_id"); }); - // Ticket → collection "tickets" modelBuilder.Entity(entity => { entity.ToCollection("tickets"); diff --git a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs index 7a539ae55..7e490f888 100644 --- a/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/FlightRepository.cs @@ -4,8 +4,17 @@ namespace Airline.Infrastructure.EfCore.Repositories; +/// +/// Repository implementation for flight entities. +/// Provides data access operations for the Flight domain model using Entity Framework Core with MongoDB. +/// public class FlightRepository(AirlineDbContext context) : IRepository { + /// + /// Creates a new flight in the data store. + /// + /// The flight entity to create. + /// The created flight entity with assigned identifier. public async Task CreateAsync(Flight entity) { var entry = await context.Flights.AddAsync(entity); @@ -13,6 +22,14 @@ public async Task CreateAsync(Flight entity) return entry.Entity; } + /// + /// Deletes a flight from the data store by its unique identifier. + /// + /// The unique identifier of the flight to delete. + /// + /// if the flight was successfully deleted; + /// otherwise, if the flight was not found. + /// public async Task DeleteAsync(int id) { var entity = await context.Flights.FindAsync(id); @@ -23,12 +40,28 @@ public async Task DeleteAsync(int id) return true; } + /// + /// Retrieves a flight from the data store by its unique identifier. + /// + /// The unique identifier of the flight. + /// + /// The flight entity if found; otherwise, . + /// public async Task GetAsync(int id) => await context.Flights.FindAsync(id); + /// + /// Retrieves all flights from the data store. + /// + /// A list of all flight entities. public async Task> GetAllAsync() => await context.Flights.ToListAsync(); + /// + /// Updates an existing flight in the data store. + /// + /// The flight entity with updated data. + /// The updated flight entity. public async Task UpdateAsync(Flight entity) { context.Flights.Update(entity); diff --git a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs index f1e23a453..766c7ea8d 100644 --- a/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/ModelFamilyRepository.cs @@ -4,8 +4,17 @@ namespace Airline.Infrastructure.EfCore.Repositories; +/// +/// Repository implementation for aircraft model family entities. +/// Provides data access operations for the ModelFamily domain model using Entity Framework Core with MongoDB. +/// public class ModelFamilyRepository(AirlineDbContext context) : IRepository { + /// + /// Creates a new aircraft model family in the data store. + /// + /// The model family entity to create. + /// The created model family entity with assigned identifier. public async Task CreateAsync(ModelFamily entity) { var entry = await context.ModelFamilies.AddAsync(entity); @@ -13,6 +22,14 @@ public async Task CreateAsync(ModelFamily entity) return entry.Entity; } + /// + /// Deletes an aircraft model family from the data store by its unique identifier. + /// + /// The unique identifier of the model family to delete. + /// + /// if the model family was successfully deleted; + /// otherwise, if the family was not found. + /// public async Task DeleteAsync(int id) { var entity = await context.ModelFamilies.FindAsync(id); @@ -23,12 +40,28 @@ public async Task DeleteAsync(int id) return true; } + /// + /// Retrieves an aircraft model family from the data store by its unique identifier. + /// + /// The unique identifier of the model family. + /// + /// The model family entity if found; otherwise, . + /// public async Task GetAsync(int id) => await context.ModelFamilies.FindAsync(id); + /// + /// Retrieves all aircraft model families from the data store. + /// + /// A list of all model family entities. public async Task> GetAllAsync() => await context.ModelFamilies.ToListAsync(); + /// + /// Updates an existing aircraft model family in the data store. + /// + /// The model family entity with updated data. + /// The updated model family entity. public async Task UpdateAsync(ModelFamily entity) { context.ModelFamilies.Update(entity); diff --git a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs index 196885ade..7383c306d 100644 --- a/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/PassengerRepository.cs @@ -4,8 +4,17 @@ namespace Airline.Infrastructure.EfCore.Repositories; +/// +/// Repository implementation for passenger entities. +/// Provides data access operations for the Passenger domain model using Entity Framework Core with MongoDB. +/// public class PassengerRepository(AirlineDbContext context) : IRepository { + /// + /// Creates a new passenger in the data store. + /// + /// The passenger entity to create. + /// The created passenger entity with assigned identifier. public async Task CreateAsync(Passenger entity) { var entry = await context.Passengers.AddAsync(entity); @@ -13,6 +22,14 @@ public async Task CreateAsync(Passenger entity) return entry.Entity; } + /// + /// Deletes a passenger from the data store by its unique identifier. + /// + /// The unique identifier of the passenger to delete. + /// + /// if the passenger was successfully deleted; + /// otherwise, if the passenger was not found. + /// public async Task DeleteAsync(int id) { var entity = await context.Passengers.FindAsync(id); @@ -23,12 +40,28 @@ public async Task DeleteAsync(int id) return true; } + /// + /// Retrieves a passenger from the data store by its unique identifier. + /// + /// The unique identifier of the passenger. + /// + /// The passenger entity if found; otherwise, . + /// public async Task GetAsync(int id) => await context.Passengers.FindAsync(id); + /// + /// Retrieves all passengers from the data store. + /// + /// A list of all passenger entities. public async Task> GetAllAsync() => await context.Passengers.ToListAsync(); + /// + /// Updates an existing passenger in the data store. + /// + /// The passenger entity with updated data. + /// The updated passenger entity. public async Task UpdateAsync(Passenger entity) { context.Passengers.Update(entity); diff --git a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs index c34cf1073..ed1a8aebe 100644 --- a/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/PlaneModelRepository.cs @@ -4,8 +4,17 @@ namespace Airline.Infrastructure.EfCore.Repositories; +/// +/// Repository implementation for aircraft model entities. +/// Provides data access operations for the PlaneModel domain model using Entity Framework Core with MongoDB. +/// public class PlaneModelRepository(AirlineDbContext context) : IRepository { + /// + /// Creates a new aircraft model in the data store. + /// + /// The aircraft model entity to create. + /// The created aircraft model entity with assigned identifier. public async Task CreateAsync(PlaneModel entity) { var entry = await context.PlaneModels.AddAsync(entity); @@ -13,6 +22,14 @@ public async Task CreateAsync(PlaneModel entity) return entry.Entity; } + /// + /// Deletes an aircraft model from the data store by its unique identifier. + /// + /// The unique identifier of the aircraft model to delete. + /// + /// if the aircraft model was successfully deleted; + /// otherwise, if the model was not found. + /// public async Task DeleteAsync(int id) { var entity = await context.PlaneModels.FindAsync(id); @@ -23,12 +40,28 @@ public async Task DeleteAsync(int id) return true; } + /// + /// Retrieves an aircraft model from the data store by its unique identifier. + /// + /// The unique identifier of the aircraft model. + /// + /// The aircraft model entity if found; otherwise, . + /// public async Task GetAsync(int id) => await context.PlaneModels.FindAsync(id); + /// + /// Retrieves all aircraft models from the data store. + /// + /// A list of all aircraft model entities. public async Task> GetAllAsync() => await context.PlaneModels.ToListAsync(); + /// + /// Updates an existing aircraft model in the data store. + /// + /// The aircraft model entity with updated data. + /// The updated aircraft model entity. public async Task UpdateAsync(PlaneModel entity) { context.PlaneModels.Update(entity); diff --git a/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs index 6d950af8c..c7f2925b1 100644 --- a/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs +++ b/Airline.Infrastructure.EfCore/Repositories/TicketRepository.cs @@ -4,8 +4,17 @@ namespace Airline.Infrastructure.EfCore.Repositories; +/// +/// Repository implementation for ticket entities. +/// Provides data access operations for the Ticket domain model using Entity Framework Core with MongoDB. +/// public class TicketRepository(AirlineDbContext context) : IRepository { + /// + /// Creates a new ticket in the data store. + /// + /// The ticket entity to create. + /// The created ticket entity with assigned identifier. public async Task CreateAsync(Ticket entity) { var entry = await context.Tickets.AddAsync(entity); @@ -13,6 +22,14 @@ public async Task CreateAsync(Ticket entity) return entry.Entity; } + /// + /// Deletes a ticket from the data store by its unique identifier. + /// + /// The unique identifier of the ticket to delete. + /// + /// if the ticket was successfully deleted; + /// otherwise, if the ticket was not found. + /// public async Task DeleteAsync(int id) { var entity = await context.Tickets.FindAsync(id); @@ -23,12 +40,28 @@ public async Task DeleteAsync(int id) return true; } + /// + /// Retrieves a ticket from the data store by its unique identifier. + /// + /// The unique identifier of the ticket. + /// + /// The ticket entity if found; otherwise, . + /// public async Task GetAsync(int id) => await context.Tickets.FindAsync(id); + /// + /// Retrieves all tickets from the data store. + /// + /// A list of all ticket entities. public async Task> GetAllAsync() => await context.Tickets.ToListAsync(); + /// + /// Updates an existing ticket in the data store. + /// + /// The ticket entity with updated data. + /// The updated ticket entity. public async Task UpdateAsync(Ticket entity) { context.Tickets.Update(entity); From 5215cd84ed4bb8abcc7b5d37444a88c69b0461cf Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 17 Dec 2025 05:24:47 +0400 Subject: [PATCH 24/36] fix --- .../Controllers/WeatherForecastController.cs | 32 ----------- Airline.Api.Host/WeatherForecast.cs | 12 ----- .../PlaneModel/IPlaneModel.cs | 10 ---- Airline.Domain/Repository/IRepository.cs | 54 ------------------- 4 files changed, 108 deletions(-) delete mode 100644 Airline.Api.Host/Controllers/WeatherForecastController.cs delete mode 100644 Airline.Api.Host/WeatherForecast.cs delete mode 100644 Airline.Application.Contracts/PlaneModel/IPlaneModel.cs delete mode 100644 Airline.Domain/Repository/IRepository.cs diff --git a/Airline.Api.Host/Controllers/WeatherForecastController.cs b/Airline.Api.Host/Controllers/WeatherForecastController.cs deleted file mode 100644 index a91fa8982..000000000 --- a/Airline.Api.Host/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Airline.Api.Host.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/Airline.Api.Host/WeatherForecast.cs b/Airline.Api.Host/WeatherForecast.cs deleted file mode 100644 index e6ebe52c5..000000000 --- a/Airline.Api.Host/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Airline.Api.Host; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs b/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs deleted file mode 100644 index bade458ca..000000000 --- a/Airline.Application.Contracts/PlaneModel/IPlaneModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Airline.Application.Contracts.PlaneModel; - -public interface IPlaneModelService -{ - public Task> GetAllAsync(); - public Task GetByIdAsync(string id); - public Task CreateAsync(PlaneModelDto model); - public Task UpdateAsync(PlaneModelDto model); - public Task DeleteAsync(string id); -} \ No newline at end of file diff --git a/Airline.Domain/Repository/IRepository.cs b/Airline.Domain/Repository/IRepository.cs deleted file mode 100644 index 4aaec24ef..000000000 --- a/Airline.Domain/Repository/IRepository.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Airline.Domain.Repository; - -/// -/// Defines a generic repository interface that provides -/// basic CRUD (Create, Read, Update, Delete) operations. -/// -/// -/// The type of the entity being managed by the repository. -/// -/// -/// The type of the entity's unique identifier (e.g., for MongoDB). -/// -public interface IRepository - where TEntity : class -{ - /// - /// Adds a new entity to the repository. - /// - /// The entity instance to add. - /// The created entity. - public Task CreateAsync(TEntity entity); - - /// - /// Retrieves an entity from the repository by its identifier. - /// - /// The unique identifier of the entity. - /// - /// The entity with the specified identifier, or - /// if no entity with such an identifier exists. - /// - public Task GetAsync(TKey id); - - /// - /// Retrieves all entities stored in the repository. - /// - /// - /// A list containing all entities in the repository. - /// - public Task> GetAllAsync(); - - /// - /// Updates an existing entity in the repository. - /// - /// The entity instance containing updated data. - /// The updated entity. - public Task UpdateAsync(TEntity entity); - - /// - /// Removes an entity from the repository by its identifier. - /// - /// The unique identifier of the entity to delete. - /// if the entity was deleted; otherwise, . - public Task DeleteAsync(TKey id); -} \ No newline at end of file From 3070a1786e1e47e5f5e4106e4266dda2aef7da23 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 17 Dec 2025 15:29:24 +0400 Subject: [PATCH 25/36] utf-8 and documentation fix --- Airline.Api.Host/Airline.Api.Host.csproj | 1 + .../Controllers/AnalyticController.cs | 32 ++++++++--------- .../Controllers/BaseCrudController.cs | 36 +++++++++---------- .../Controllers/FlightController.cs | 25 ++++++------- .../Controllers/ModelFamilyController.cs | 9 ++--- .../Controllers/PassengerController.cs | 17 ++++----- .../Controllers/PlaneModelController.cs | 9 ++--- .../Controllers/TicketController.cs | 17 ++++----- Airline.Api.Host/Program.cs | 2 +- .../Airline.Application.Contracts.csproj | 1 + 10 files changed, 78 insertions(+), 71 deletions(-) diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj index 05b536f3a..bbd0934d2 100644 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/Airline.Api.Host/Controllers/AnalyticController.cs b/Airline.Api.Host/Controllers/AnalyticController.cs index f944c453a..8522c52c6 100644 --- a/Airline.Api.Host/Controllers/AnalyticController.cs +++ b/Airline.Api.Host/Controllers/AnalyticController.cs @@ -30,16 +30,16 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetTopFlightsByPassengerCount([FromQuery] int top = 5) { - logger.LogInformation("Метод GetTopFlightsByPassengerCount вызван с top={Top}", top); + logger.LogInformation("GetTopFlightsByPassengerCount method called with top={Top}", top); try { var flights = await analyticsService.GetTopFlightsByPassengerCountAsync(top); - logger.LogInformation("Метод GetTopFlightsByPassengerCount успешно выполнен"); + logger.LogInformation("GetTopFlightsByPassengerCount method completed successfully"); return Ok(flights); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetTopFlightsByPassengerCount"); + logger.LogError(ex, "Error in GetTopFlightsByPassengerCount method"); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -57,16 +57,16 @@ public async Task>> GetTopFlightsByPassengerCount([ [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetFlightsWithMinTravelTime() { - logger.LogInformation("Метод GetFlightsWithMinTravelTime вызван"); + logger.LogInformation("GetFlightsWithMinTravelTime method called"); try { var flights = await analyticsService.GetFlightsWithMinTravelTimeAsync(); - logger.LogInformation("Метод GetFlightsWithMinTravelTime успешно выполнен"); + logger.LogInformation("GetFlightsWithMinTravelTime method completed successfully"); return Ok(flights); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetFlightsWithMinTravelTime"); + logger.LogError(ex, "Error in GetFlightsWithMinTravelTime method"); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -87,21 +87,21 @@ public async Task>> GetFlightsWithMinTravelTime() [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetPassengersWithZeroBaggage([FromQuery] int flightId) { - logger.LogInformation("Метод GetPassengersWithZeroBaggage вызван с flightId={FlightId}", flightId); + logger.LogInformation("GetPassengersWithZeroBaggage method called with flightId={FlightId}", flightId); try { var passengers = await analyticsService.GetPassengersWithZeroBaggageOnFlightAsync(flightId); - logger.LogInformation("Метод GetPassengersWithZeroBaggage успешно выполнен для flightId={FlightId}", flightId); + logger.LogInformation("GetPassengersWithZeroBaggage method completed successfully for flightId={FlightId}", flightId); return Ok(passengers); } catch (KeyNotFoundException) { - logger.LogWarning("Рейс не найден для flightId={FlightId}", flightId); + logger.LogWarning("Flight not found for flightId={FlightId}", flightId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetPassengersWithZeroBaggage для flightId={FlightId}", flightId); + logger.LogError(ex, "Error in GetPassengersWithZeroBaggage method for flightId={FlightId}", flightId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -125,16 +125,16 @@ public async Task>> GetFlightsByModelInPeriod( [FromQuery] DateTime from, [FromQuery] DateTime to) { - logger.LogInformation("Метод GetFlightsByModelInPeriod вызван с modelId={ModelId}, from={From}, to={To}", modelId, from, to); + logger.LogInformation("GetFlightsByModelInPeriod method called with modelId={ModelId}, from={From}, to={To}", modelId, from, to); try { var flights = await analyticsService.GetFlightsByModelInPeriodAsync(modelId, from, to); - logger.LogInformation("Метод GetFlightsByModelInPeriod успешно выполнен"); + logger.LogInformation("GetFlightsByModelInPeriod method completed successfully"); return Ok(flights); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetFlightsByModelInPeriod"); + logger.LogError(ex, "Error in GetFlightsByModelInPeriod method"); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -156,16 +156,16 @@ public async Task>> GetFlightsByRoute( [FromQuery] string departure, [FromQuery] string arrival) { - logger.LogInformation("Метод GetFlightsByRoute вызван с departure={Departure}, arrival={Arrival}", departure, arrival); + logger.LogInformation("GetFlightsByRoute method called with departure={Departure}, arrival={Arrival}", departure, arrival); try { var flights = await analyticsService.GetFlightsByRouteAsync(departure, arrival); - logger.LogInformation("Метод GetFlightsByRoute успешно выполнен"); + logger.LogInformation("GetFlightsByRoute method completed successfully"); return Ok(flights); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetFlightsByRoute"); + logger.LogError(ex, "Error in GetFlightsByRoute method"); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/BaseCrudController.cs b/Airline.Api.Host/Controllers/BaseCrudController.cs index 315501321..c156df593 100644 --- a/Airline.Api.Host/Controllers/BaseCrudController.cs +++ b/Airline.Api.Host/Controllers/BaseCrudController.cs @@ -49,16 +49,16 @@ ILogger> logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> Create(TCreateUpdateDto dto) { - logger.LogInformation("Метод Create вызван в {Controller} с данными: {@Dto}", GetType().Name, dto); + logger.LogInformation("Create method called in {Controller} with data: {@Dto}", GetType().Name, dto); try { var result = await service.CreateAsync(dto); - logger.LogInformation("Метод Create успешно выполнен в {Controller}", GetType().Name); + logger.LogInformation("Create method completed successfully in {Controller}", GetType().Name); return CreatedAtAction(nameof(GetById), new { id = GetEntityId(result) }, result); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе Create контроллера {Controller}", GetType().Name); + logger.LogError(ex, "Error in Create method of controller {Controller}", GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -82,21 +82,21 @@ public async Task> Create(TCreateUpdateDto dto) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> Update(TKey id, TCreateUpdateDto dto) { - logger.LogInformation("Метод Update вызван в {Controller} с id={Id} и данными: {@Dto}", GetType().Name, id, dto); + logger.LogInformation("Update method called in {Controller} with id={Id} and data: {@Dto}", GetType().Name, id, dto); try { var result = await service.UpdateAsync(dto, id); - logger.LogInformation("Метод Update успешно выполнен в {Controller}", GetType().Name); + logger.LogInformation("Update method completed successfully in {Controller}", GetType().Name); return Ok(result); } catch (KeyNotFoundException) { - logger.LogWarning("Сущность с id={Id} не найдена в {Controller}", id, GetType().Name); + logger.LogWarning("Entity with id={Id} not found in {Controller}", id, GetType().Name); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе Update контроллера {Controller}", GetType().Name); + logger.LogError(ex, "Error in Update method of controller {Controller}", GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -119,21 +119,21 @@ public async Task> Update(TKey id, TCreateUpdateDto dto) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task Delete(TKey id) { - logger.LogInformation("Метод Delete вызван в {Controller} с id={Id}", GetType().Name, id); + logger.LogInformation("Delete method called in {Controller} with id={Id}", GetType().Name, id); try { var success = await service.DeleteAsync(id); if (!success) { - logger.LogWarning("Сущность с id={Id} не найдена при удалении в {Controller}", id, GetType().Name); + logger.LogWarning("Entity with id={Id} not found during deletion in {Controller}", id, GetType().Name); return NotFound(); } - logger.LogInformation("Метод Delete успешно выполнен в {Controller}", GetType().Name); + logger.LogInformation("Delete method completed successfully in {Controller}", GetType().Name); return NoContent(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе Delete контроллера {Controller}", GetType().Name); + logger.LogError(ex, "Error in Delete method of controller {Controller}", GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -153,16 +153,16 @@ public async Task Delete(TKey id) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetAll() { - logger.LogInformation("Метод GetAll вызван в {Controller}", GetType().Name); + logger.LogInformation("GetAll method called in {Controller}", GetType().Name); try { var result = await service.GetAllAsync(); - logger.LogInformation("Метод GetAll успешно выполнен в {Controller}", GetType().Name); + logger.LogInformation("GetAll method completed successfully in {Controller}", GetType().Name); return Ok(result); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetAll контроллера {Controller}", GetType().Name); + logger.LogError(ex, "Error in GetAll method of controller {Controller}", GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -185,21 +185,21 @@ public async Task>> GetAll() [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetById(TKey id) { - logger.LogInformation("Метод GetById вызван в {Controller} с id={Id}", GetType().Name, id); + logger.LogInformation("GetById method called in {Controller} with id={Id}", GetType().Name, id); try { var result = await service.GetByIdAsync(id); if (result == null) { - logger.LogWarning("Сущность с id={Id} не найдена в {Controller}", id, GetType().Name); + logger.LogWarning("Entity with id={Id} not found in {Controller}", id, GetType().Name); return NotFound(); } - logger.LogInformation("Метод GetById успешно выполнен в {Controller}", GetType().Name); + logger.LogInformation("GetById method completed successfully in {Controller}", GetType().Name); return Ok(result); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetById контроллера {Controller}", GetType().Name); + logger.LogError(ex, "Error in GetById method of controller {Controller}", GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/FlightController.cs b/Airline.Api.Host/Controllers/FlightController.cs index a2f615887..eeb56a7d9 100644 --- a/Airline.Api.Host/Controllers/FlightController.cs +++ b/Airline.Api.Host/Controllers/FlightController.cs @@ -3,6 +3,7 @@ using Airline.Application.Contracts.PlaneModel; using Airline.Application.Contracts.Ticket; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; @@ -34,21 +35,21 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetPlaneModel(int flightId) { - logger.LogInformation("Метод GetPlaneModel вызван с flightId={FlightId}", flightId); + logger.LogInformation("GetPlaneModel method called with flightId={FlightId}", flightId); try { var model = await flightService.GetPlaneModelAsync(flightId); - logger.LogInformation("Метод GetPlaneModel успешно выполнен для flightId={FlightId}", flightId); + logger.LogInformation("GetPlaneModel method completed successfully for flightId={FlightId}", flightId); return Ok(model); } catch (KeyNotFoundException) { - logger.LogWarning("Модель самолёта не найдена для flightId={FlightId}", flightId); + logger.LogWarning("Aircraft model not found for flightId={FlightId}", flightId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetPlaneModel для flightId={FlightId}", flightId); + logger.LogError(ex, "Error in GetPlaneModel method for flightId={FlightId}", flightId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -67,21 +68,21 @@ public async Task> GetPlaneModel(int flightId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetTickets(int flightId) { - logger.LogInformation("Метод GetTickets вызван с flightId={FlightId}", flightId); + logger.LogInformation("GetTickets method called with flightId={FlightId}", flightId); try { var tickets = await flightService.GetTicketsAsync(flightId); - logger.LogInformation("Метод GetTickets успешно выполнен для flightId={FlightId}", flightId); + logger.LogInformation("GetTickets method completed successfully for flightId={FlightId}", flightId); return Ok(tickets); } catch (KeyNotFoundException) { - logger.LogWarning("Билеты не найдены для flightId={FlightId}", flightId); + logger.LogWarning("Tickets not found for flightId={FlightId}", flightId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetTickets для flightId={FlightId}", flightId); + logger.LogError(ex, "Error in GetTickets method for flightId={FlightId}", flightId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -100,21 +101,21 @@ public async Task>> GetTickets(int flightId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetPassengers(int flightId) { - logger.LogInformation("Метод GetPassengers вызван с flightId={FlightId}", flightId); + logger.LogInformation("GetPassengers method called with flightId={FlightId}", flightId); try { var passengers = await flightService.GetPassengersAsync(flightId); - logger.LogInformation("Метод GetPassengers успешно выполнен для flightId={FlightId}", flightId); + logger.LogInformation("GetPassengers method completed successfully for flightId={FlightId}", flightId); return Ok(passengers); } catch (KeyNotFoundException) { - logger.LogWarning("Пассажиры не найдены для flightId={FlightId}", flightId); + logger.LogWarning("Passengers not found for flightId={FlightId}", flightId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetPassengers для flightId={FlightId}", flightId); + logger.LogError(ex, "Error in GetPassengers method for flightId={FlightId}", flightId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/ModelFamilyController.cs b/Airline.Api.Host/Controllers/ModelFamilyController.cs index c74c3b85f..b5fcd8ff5 100644 --- a/Airline.Api.Host/Controllers/ModelFamilyController.cs +++ b/Airline.Api.Host/Controllers/ModelFamilyController.cs @@ -1,6 +1,7 @@ using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.PlaneModel; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; @@ -32,21 +33,21 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetPlaneModels(int familyId) { - logger.LogInformation("Метод GetPlaneModels вызван с familyId={FamilyId}", familyId); + logger.LogInformation("GetPlaneModels method called with familyId={FamilyId}", familyId); try { var models = await modelFamilyService.GetPlaneModelsAsync(familyId); - logger.LogInformation("Метод GetPlaneModels успешно выполнен для familyId={FamilyId}", familyId); + logger.LogInformation("GetPlaneModels method completed successfully for familyId={FamilyId}", familyId); return Ok(models); } catch (KeyNotFoundException) { - logger.LogWarning("Модели самолётов не найдены для familyId={FamilyId}", familyId); + logger.LogWarning("Aircraft models not found for familyId={FamilyId}", familyId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetPlaneModels для familyId={FamilyId}", familyId); + logger.LogError(ex, "Error in GetPlaneModels method for familyId={FamilyId}", familyId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/PassengerController.cs b/Airline.Api.Host/Controllers/PassengerController.cs index 8bb169864..e6065d7df 100644 --- a/Airline.Api.Host/Controllers/PassengerController.cs +++ b/Airline.Api.Host/Controllers/PassengerController.cs @@ -2,6 +2,7 @@ using Airline.Application.Contracts.Passenger; using Airline.Application.Contracts.Ticket; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; @@ -33,21 +34,21 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetTickets(int passengerId) { - logger.LogInformation("Метод GetTickets вызван с passengerId={PassengerId}", passengerId); + logger.LogInformation("GetTickets method called with passengerId={PassengerId}", passengerId); try { var tickets = await passengerService.GetTicketsAsync(passengerId); - logger.LogInformation("Метод GetTickets успешно выполнен для passengerId={PassengerId}", passengerId); + logger.LogInformation("GetTickets method completed successfully for passengerId={PassengerId}", passengerId); return Ok(tickets); } catch (KeyNotFoundException) { - logger.LogWarning("Билеты не найдены для passengerId={PassengerId}", passengerId); + logger.LogWarning("Tickets not found for passengerId={PassengerId}", passengerId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetTickets для passengerId={PassengerId}", passengerId); + logger.LogError(ex, "Error in GetTickets method for passengerId={PassengerId}", passengerId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -66,21 +67,21 @@ public async Task>> GetTickets(int passengerId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetFlights(int passengerId) { - logger.LogInformation("Метод GetFlights вызван с passengerId={PassengerId}", passengerId); + logger.LogInformation("GetFlights method called with passengerId={PassengerId}", passengerId); try { var flights = await passengerService.GetFlightsAsync(passengerId); - logger.LogInformation("Метод GetFlights успешно выполнен для passengerId={PassengerId}", passengerId); + logger.LogInformation("GetFlights method completed successfully for passengerId={PassengerId}", passengerId); return Ok(flights); } catch (KeyNotFoundException) { - logger.LogWarning("Рейсы не найдены для passengerId={PassengerId}", passengerId); + logger.LogWarning("Flights not found for passengerId={PassengerId}", passengerId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetFlights для passengerId={PassengerId}", passengerId); + logger.LogError(ex, "Error in GetFlights method for passengerId={PassengerId}", passengerId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/PlaneModelController.cs b/Airline.Api.Host/Controllers/PlaneModelController.cs index 6c02895d4..2e4e0d2dc 100644 --- a/Airline.Api.Host/Controllers/PlaneModelController.cs +++ b/Airline.Api.Host/Controllers/PlaneModelController.cs @@ -1,6 +1,7 @@ using Airline.Application.Contracts.ModelFamily; using Airline.Application.Contracts.PlaneModel; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; @@ -32,21 +33,21 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetModelFamily(int modelId) { - logger.LogInformation("Метод GetModelFamily вызван с modelId={ModelId}", modelId); + logger.LogInformation("GetModelFamily method called with modelId={ModelId}", modelId); try { var family = await planeModelService.GetModelFamilyAsync(modelId); - logger.LogInformation("Метод GetModelFamily успешно выполнен для modelId={ModelId}", modelId); + logger.LogInformation("GetModelFamily method completed successfully for modelId={ModelId}", modelId); return Ok(family); } catch (KeyNotFoundException) { - logger.LogWarning("Семейство моделей не найдено для modelId={ModelId}", modelId); + logger.LogWarning("Model family not found for modelId={ModelId}", modelId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetModelFamily для modelId={ModelId}", modelId); + logger.LogError(ex, "Error in GetModelFamily method for modelId={ModelId}", modelId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Controllers/TicketController.cs b/Airline.Api.Host/Controllers/TicketController.cs index e8fe9787c..2fea0bf3f 100644 --- a/Airline.Api.Host/Controllers/TicketController.cs +++ b/Airline.Api.Host/Controllers/TicketController.cs @@ -2,6 +2,7 @@ using Airline.Application.Contracts.Passenger; using Airline.Application.Contracts.Ticket; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Airline.Api.Host.Controllers; @@ -33,21 +34,21 @@ ILogger logger [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetFlight(int ticketId) { - logger.LogInformation("Метод GetFlight вызван с ticketId={TicketId}", ticketId); + logger.LogInformation("GetFlight method called with ticketId={TicketId}", ticketId); try { var flight = await ticketService.GetFlightAsync(ticketId); - logger.LogInformation("Метод GetFlight успешно выполнен для ticketId={TicketId}", ticketId); + logger.LogInformation("GetFlight method completed successfully for ticketId={TicketId}", ticketId); return Ok(flight); } catch (KeyNotFoundException) { - logger.LogWarning("Рейс не найден для ticketId={TicketId}", ticketId); + logger.LogWarning("Flight not found for ticketId={TicketId}", ticketId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetFlight для ticketId={TicketId}", ticketId); + logger.LogError(ex, "Error in GetFlight method for ticketId={TicketId}", ticketId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } @@ -66,21 +67,21 @@ public async Task> GetFlight(int ticketId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetPassenger(int ticketId) { - logger.LogInformation("Метод GetPassenger вызван с ticketId={TicketId}", ticketId); + logger.LogInformation("GetPassenger method called with ticketId={TicketId}", ticketId); try { var passenger = await ticketService.GetPassengerAsync(ticketId); - logger.LogInformation("Метод GetPassenger успешно выполнен для ticketId={TicketId}", ticketId); + logger.LogInformation("GetPassenger method completed successfully for ticketId={TicketId}", ticketId); return Ok(passenger); } catch (KeyNotFoundException) { - logger.LogWarning("Пассажир не найден для ticketId={TicketId}", ticketId); + logger.LogWarning("Passenger not found for ticketId={TicketId}", ticketId); return NotFound(); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в методе GetPassenger для ticketId={TicketId}", ticketId); + logger.LogError(ex, "Error in GetPassenger method for ticketId={TicketId}", ticketId); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index 175375966..9220937f6 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -117,7 +117,7 @@ await dbContext.Tickets.AddAsync(ticket); await dbContext.SaveChangesAsync(); - app.Logger.LogInformation(" ."); + app.Logger.LogInformation("Database successfully populated with test data."); } } diff --git a/Airline.Application.Contracts/Airline.Application.Contracts.csproj b/Airline.Application.Contracts/Airline.Application.Contracts.csproj index be813cb62..90b8a4194 100644 --- a/Airline.Application.Contracts/Airline.Application.Contracts.csproj +++ b/Airline.Application.Contracts/Airline.Application.Contracts.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true From 7a4b78e20e5f90b9ad8909f1c5763f32c107ecb5 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 22 Dec 2025 00:23:31 +0400 Subject: [PATCH 26/36] save me please kafka --- Airline.AppHost/Airline.AppHost.csproj | 4 +- Airline.AppHost/AppHost.cs | 9 ++- .../Airline.Generator.Kafka.Host.csproj | 22 ++++++ .../AirlineKafkaProducer.cs | 47 +++++++++++ .../Controllers/GeneratorController.cs | 79 +++++++++++++++++++ .../Generator/FlightGenerator.cs | 28 +++++++ .../Interface/IProducerService.cs | 16 ++++ Airline.Generator.Kafka.Host/Program.cs | 31 ++++++++ .../Properties/launchSettings.json | 12 +++ .../Serializers/AirlineKeySerializer.cs | 19 +++++ .../Serializers/AirlineValueSerializer.cs | 20 +++++ .../appsettings.Development.json | 8 ++ Airline.Generator.Kafka.Host/appsettings.json | 8 ++ Airline.sln | 6 ++ 14 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj create mode 100644 Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs create mode 100644 Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs create mode 100644 Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs create mode 100644 Airline.Generator.Kafka.Host/Interface/IProducerService.cs create mode 100644 Airline.Generator.Kafka.Host/Program.cs create mode 100644 Airline.Generator.Kafka.Host/Properties/launchSettings.json create mode 100644 Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs create mode 100644 Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs create mode 100644 Airline.Generator.Kafka.Host/appsettings.Development.json create mode 100644 Airline.Generator.Kafka.Host/appsettings.json diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index 52443a51e..c849204bf 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -13,10 +13,12 @@ + + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index efeecae55..b1a09ba33 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -1,11 +1,12 @@ using Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); +var kafka = builder.AddKafka("kafka"); -var db = builder.AddMongoDB("mongo").AddDatabase("db"); +builder.AddProject("airline-generator") + .WithReference(kafka); builder.AddProject("airline-api-host") - .WithReference(db, "airlineClient") - .WaitFor(db); -builder.Build().Run(); + .WithReference(kafka); +builder.Build().Run(); diff --git a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj new file mode 100644 index 000000000..6ac012c02 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs new file mode 100644 index 000000000..546268b8d --- /dev/null +++ b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs @@ -0,0 +1,47 @@ +using Airline.Application.Contracts.Flight; +using Airline.Generator.Kafka.Host.Interface; +using Confluent.Kafka; + +namespace Airline.Generator.Kafka.Host; + +/// +/// Kafka producer service that publishes batches of flight create contracts to a configured topic +/// +/// Application configuration used to resolve Kafka topic name +/// Kafka producer used to send messages +/// Logger instance +public sealed class AirlineKafkaProducer( + IConfiguration configuration, + IProducer> producer, + ILogger logger) : IProducerService +{ + private readonly string _topicName = + configuration.GetSection("Kafka")["TopicName"] ?? throw new KeyNotFoundException("TopicName section of Kafka is missing"); + + /// + /// Sends a batch of flight contracts to Kafka topic using flight code as message key + /// + /// Batch of flight create contracts + public async Task SendAsync(IList batch) + { + try + { + logger.LogInformation("Sending a batch of {count} flight contracts to {topic}", batch.Count, _topicName); + + // FlightCode ( ) + var key = batch.FirstOrDefault()?.FlightCode ?? Guid.NewGuid().ToString(); + + var message = new Message> + { + Key = key, + Value = batch + }; + + await producer.ProduceAsync(_topicName, message); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occurred during sending a batch of {count} flight contracts to {topic}", batch.Count, _topicName); + } + } +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs new file mode 100644 index 000000000..4203a318b --- /dev/null +++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs @@ -0,0 +1,79 @@ +using Airline.Application.Contracts.Flight; +using Airline.Generator.Kafka.Host.Generator; +using Airline.Generator.Kafka.Host.Interface; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Airline.Generator.Kafka.Host.Controllers; + +/// +/// Controller used to generate flight contracts and publish them via Kafka message broker +/// +/// Logger instance +/// Producer service used to send contracts +/// Configuration instance used to read generator settings +[Route("api/[controller]")] +[ApiController] +public sealed class GeneratorController( + ILogger logger, + IProducerService producerService, + IConfiguration configuration) : ControllerBase +{ + /// + /// Generates flight contracts and sends them via Kafka using batches and delay between sends + /// + /// Number of contracts in each batch + /// Total number of contracts to generate + /// Delay in seconds between batches + /// List of generated flight contracts + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> Get( + [FromQuery] int batchSize, + [FromQuery] int payloadLimit, + [FromQuery] int waitTime) + { + logger.LogInformation("Generating {limit} flight contracts via {batchSize} batches with {waitTime}s delay", + payloadLimit, batchSize, waitTime); + + try + { + var list = new List(payloadLimit); + var counter = 0; + + // Чтение пула существующих ModelId из конфигурации + var modelIds = configuration.GetSection("Generator:SeedModelIds") + .Get>() ?? []; + + if (modelIds.Count == 0) + return StatusCode(StatusCodes.Status500InternalServerError, + "SeedModelIds configuration is empty or missing"); + + while (counter < payloadLimit) + { + var currentBatchSize = Math.Min(batchSize, payloadLimit - counter); + + var batch = FlightGenerator.GenerateContracts(currentBatchSize, modelIds); + + await producerService.SendAsync(batch); + + logger.LogInformation("Batch of {batchSize} flight contracts has been sent to Kafka", currentBatchSize); + + counter += currentBatchSize; + list.AddRange(batch); + + if (counter < payloadLimit && waitTime > 0) + await Task.Delay(waitTime * 1000); + } + + logger.LogInformation("{Method} method of {Controller} executed successfully", nameof(Get), GetType().Name); + return Ok(list); + } + catch (Exception ex) + { + logger.LogError(ex, "An exception occurred during {Method} method of {Controller}", nameof(Get), GetType().Name); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs new file mode 100644 index 000000000..685c2c2d7 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs @@ -0,0 +1,28 @@ +using Bogus; +using Airline.Application.Contracts.Flight; + +namespace Airline.Generator.Kafka.Host.Generator; + +/// +/// Generates random flight contracts to emulate an external system sending data +/// +public static class FlightGenerator +{ + /// + /// Generates a list of flight create contracts + /// + /// Number of contracts to generate + /// Pool of existing aircraft model identifiers + /// Generated list of flight contracts + public static List GenerateContracts(int count, IList modelIds) => + new Faker() + .CustomInstantiator(f => new CreateFlightDto( + FlightCode: f.Random.String2(2, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + f.Random.Number(100, 999), + DepartureCity: f.Address.City(), + ArrivalCity: f.Address.City(), + DepartureDateTime: f.Date.Future(), + ArrivalDateTime: f.Date.Future().AddHours(f.Random.Number(1, 24)), + ModelId: f.PickRandom(modelIds) + )) + .Generate(count); +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Interface/IProducerService.cs b/Airline.Generator.Kafka.Host/Interface/IProducerService.cs new file mode 100644 index 000000000..57c3795b1 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Interface/IProducerService.cs @@ -0,0 +1,16 @@ +using Airline.Application.Contracts.Flight; + +namespace Airline.Generator.Kafka.Host.Interface; + +/// +/// Abstraction for producing batches of flight create contracts to a Kafka message broker +/// +public interface IProducerService +{ + /// + /// Sends a batch of flight create contracts to Kafka + /// + /// Batch of flight create contracts to send + /// Task representing asynchronous send operation + public Task SendAsync(IList batch); +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Program.cs b/Airline.Generator.Kafka.Host/Program.cs new file mode 100644 index 000000000..fbf034650 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Program.cs @@ -0,0 +1,31 @@ +using Airline.Application.Contracts.Flight; +using Airline.Generator.Kafka.Host; +using Airline.Generator.Kafka.Host.Interface; +using Airline.Generator.Kafka.Host.Serializers; +using Airline.ServiceDefaults; +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +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/Airline.Generator.Kafka.Host/Properties/launchSettings.json b/Airline.Generator.Kafka.Host/Properties/launchSettings.json new file mode 100644 index 000000000..2e87d0bf4 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Airline.Generator.Kafka.Host": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs b/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs new file mode 100644 index 000000000..409614d06 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Serializers/AirlineKeySerializer.cs @@ -0,0 +1,19 @@ +using Confluent.Kafka; +using System.Text.Json; + +namespace Airline.Generator.Kafka.Host.Serializers; + +/// +/// Kafka serializer that converts a string key into UTF8 JSON bytes +/// +public sealed class AirlineKeySerializer : ISerializer +{ + /// + /// Serializes string key into JSON byte array representation + /// + /// Key value to serialize + /// Serialization context provided by Kafka client + /// UTF8 JSON byte array + public byte[] Serialize(string data, SerializationContext context) => + JsonSerializer.SerializeToUtf8Bytes(data); +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs b/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs new file mode 100644 index 000000000..8b45aa987 --- /dev/null +++ b/Airline.Generator.Kafka.Host/Serializers/AirlineValueSerializer.cs @@ -0,0 +1,20 @@ +using Confluent.Kafka; +using Airline.Application.Contracts.Flight; +using System.Text.Json; + +namespace Airline.Generator.Kafka.Host.Serializers; + +/// +/// Kafka serializer that converts a list of flight create contracts into UTF8 JSON bytes +/// +public sealed class AirlineValueSerializer : ISerializer> +{ + /// + /// Serializes contract batch into JSON byte array representation + /// + /// Batch of flight contracts to serialize + /// Serialization context provided by Kafka client + /// UTF8 JSON byte array + public byte[] Serialize(IList data, SerializationContext context) => + JsonSerializer.SerializeToUtf8Bytes(data); +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/appsettings.Development.json b/Airline.Generator.Kafka.Host/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/Airline.Generator.Kafka.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Airline.Generator.Kafka.Host/appsettings.json b/Airline.Generator.Kafka.Host/appsettings.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/Airline.Generator.Kafka.Host/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Airline.sln b/Airline.sln index 0b2926726..9bb66297b 100644 --- a/Airline.sln +++ b/Airline.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.AppHost", "Airline. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.ServiceDefaults", "Airline.ServiceDefaults\Airline.ServiceDefaults.csproj", "{AB4A2E61-005D-D6AF-C18F-732B6C13F746}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Generator.Kafka.Host", "Airline.Generator.Kafka.Host\Airline.Generator.Kafka.Host.csproj", "{21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB4A2E61-005D-D6AF-C18F-732B6C13F746}.Release|Any CPU.Build.0 = Release|Any CPU + {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 7be5733cbb397fff144cce6695e905e8d0c395ab Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 23 Dec 2025 19:01:11 +0400 Subject: [PATCH 27/36] brand new start. kafka host is done --- Airline.AppHost/Airline.AppHost.csproj | 1 - Airline.AppHost/AppHost.cs | 10 +++--- .../Airline.Generator.Kafka.Host.csproj | 33 +++++++++---------- .../Airline.Generator.Kafka.Host.http | 6 ++++ .../AirlineKafkaProducer.cs | 9 +++-- .../Controllers/GeneratorController.cs | 24 ++++++-------- .../Interfaces/IProducerService.cs | 16 +++++++++ Airline.Generator.Kafka.Host/Program.cs | 27 +++++++++++++-- .../Properties/launchSettings.json | 33 +++++++++++++++++-- .../appsettings.Development.json | 2 +- Airline.Generator.Kafka.Host/appsettings.json | 5 +-- 11 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http create mode 100644 Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index c849204bf..7f90e0e9f 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -18,7 +18,6 @@ - diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index b1a09ba33..89e9a0a7f 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -1,12 +1,12 @@ using Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); -var kafka = builder.AddKafka("kafka"); -builder.AddProject("airline-generator") - .WithReference(kafka); +var db = builder.AddMongoDB("mongo").AddDatabase("db"); builder.AddProject("airline-api-host") - .WithReference(kafka); - + .WithReference(db, "airlineClient") + .WaitFor(db); +builder.AddProject("airline-generator-kafka-host"); builder.Build().Run(); + diff --git a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj index 6ac012c02..c5b6e15d5 100644 --- a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj +++ b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.csproj @@ -1,22 +1,21 @@  - - net8.0 - enable - enable - true - + + net8.0 + enable + enable + true + - - - - - - + + + + + - - - - + + + + - \ No newline at end of file + diff --git a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http new file mode 100644 index 000000000..ce1e47ffd --- /dev/null +++ b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http @@ -0,0 +1,6 @@ +@Airline.Generator.Kafka.Host_HostAddress = http://localhost:5206 + +GET {{Airline.Generator.Kafka.Host_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs index 546268b8d..cfdd7d8e4 100644 --- a/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs +++ b/Airline.Generator.Kafka.Host/AirlineKafkaProducer.cs @@ -1,6 +1,6 @@ +using Confluent.Kafka; using Airline.Application.Contracts.Flight; -using Airline.Generator.Kafka.Host.Interface; -using Confluent.Kafka; +using Airline.Generator.Kafka.Host.Interfaces; namespace Airline.Generator.Kafka.Host; @@ -26,9 +26,8 @@ public async Task SendAsync(IList batch) { try { - logger.LogInformation("Sending a batch of {count} flight contracts to {topic}", batch.Count, _topicName); + logger.LogInformation("Sending a batch of {count} contracts to {topic}", batch.Count, _topicName); - // FlightCode ( ) var key = batch.FirstOrDefault()?.FlightCode ?? Guid.NewGuid().ToString(); var message = new Message> @@ -41,7 +40,7 @@ public async Task SendAsync(IList batch) } catch (Exception ex) { - logger.LogError(ex, "Exception occurred during sending a batch of {count} flight contracts to {topic}", batch.Count, _topicName); + logger.LogError(ex, "Exception occurred during sending a batch of {count} contracts to {topic}", batch.Count, _topicName); } } } \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs index 4203a318b..9922f3c67 100644 --- a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs +++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs @@ -1,7 +1,6 @@ -using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Flight; using Airline.Generator.Kafka.Host.Generator; -using Airline.Generator.Kafka.Host.Interface; -using Microsoft.AspNetCore.Http; +using Airline.Generator.Kafka.Host.Interfaces; using Microsoft.AspNetCore.Mvc; namespace Airline.Generator.Kafka.Host.Controllers; @@ -22,10 +21,10 @@ public sealed class GeneratorController( /// /// Generates flight contracts and sends them via Kafka using batches and delay between sends /// - /// Number of contracts in each batch - /// Total number of contracts to generate + /// Batch size + /// Total number of contracts to send /// Delay in seconds between batches - /// List of generated flight contracts + /// List of generated contracts [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -34,21 +33,18 @@ public async Task>> Get( [FromQuery] int payloadLimit, [FromQuery] int waitTime) { - logger.LogInformation("Generating {limit} flight contracts via {batchSize} batches with {waitTime}s delay", - payloadLimit, batchSize, waitTime); + logger.LogInformation("Generating {limit} contracts via {batchSize} batches and {waitTime}s delay", payloadLimit, batchSize, waitTime); try { var list = new List(payloadLimit); var counter = 0; - // Чтение пула существующих ModelId из конфигурации var modelIds = configuration.GetSection("Generator:SeedModelIds") .Get>() ?? []; if (modelIds.Count == 0) - return StatusCode(StatusCodes.Status500InternalServerError, - "SeedModelIds configuration is empty or missing"); + return StatusCode(StatusCodes.Status500InternalServerError, "SeedModelIds is empty"); while (counter < payloadLimit) { @@ -58,7 +54,7 @@ public async Task>> Get( await producerService.SendAsync(batch); - logger.LogInformation("Batch of {batchSize} flight contracts has been sent to Kafka", currentBatchSize); + logger.LogInformation("Batch of {batchSize} items has been sent", currentBatchSize); counter += currentBatchSize; list.AddRange(batch); @@ -67,12 +63,12 @@ public async Task>> Get( await Task.Delay(waitTime * 1000); } - logger.LogInformation("{Method} method of {Controller} executed successfully", nameof(Get), GetType().Name); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); return Ok(list); } catch (Exception ex) { - logger.LogError(ex, "An exception occurred during {Method} method of {Controller}", nameof(Get), GetType().Name); + logger.LogError(ex, "An exception happened during {method} method of {controller}", nameof(Get), GetType().Name); return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); } } diff --git a/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs b/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs new file mode 100644 index 000000000..37a66623e --- /dev/null +++ b/Airline.Generator.Kafka.Host/Interfaces/IProducerService.cs @@ -0,0 +1,16 @@ +using Airline.Application.Contracts.Flight; + +namespace Airline.Generator.Kafka.Host.Interfaces; + +/// +/// Abstraction for producing batches of flight create contracts to a Kafka message broker +/// +public interface IProducerService +{ + /// + /// Sends a batch of flight create contracts + /// + /// Batch of flight create contracts to send + /// Task representing asynchronous send operation + public Task SendAsync(IList batch); +} \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Program.cs b/Airline.Generator.Kafka.Host/Program.cs index fbf034650..589fdbc12 100644 --- a/Airline.Generator.Kafka.Host/Program.cs +++ b/Airline.Generator.Kafka.Host/Program.cs @@ -1,18 +1,39 @@ using Airline.Application.Contracts.Flight; using Airline.Generator.Kafka.Host; -using Airline.Generator.Kafka.Host.Interface; +using Airline.Generator.Kafka.Host.Interfaces; using Airline.Generator.Kafka.Host.Serializers; using Airline.ServiceDefaults; -using Microsoft.AspNetCore.Builder; + var builder = WebApplication.CreateBuilder(args); +builder.AddKafkaProducer>( + "airline-kafka", + kafkaBuilder => + { + kafkaBuilder.SetKeySerializer(new AirlineKeySerializer()); + kafkaBuilder.SetValueSerializer(new AirlineValueSerializer()); + }); + builder.AddServiceDefaults(); builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("Airline")) + .Distinct(); + + foreach (var assembly in assemblies) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + options.IncludeXmlComments(xmlPath); + } +}); var app = builder.Build(); diff --git a/Airline.Generator.Kafka.Host/Properties/launchSettings.json b/Airline.Generator.Kafka.Host/Properties/launchSettings.json index 2e87d0bf4..e99c7bec6 100644 --- a/Airline.Generator.Kafka.Host/Properties/launchSettings.json +++ b/Airline.Generator.Kafka.Host/Properties/launchSettings.json @@ -1,11 +1,40 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60399", + "sslPort": 44344 + } + }, "profiles": { - "Airline.Generator.Kafka.Host": { + "http": { "commandName": "Project", "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7260;http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/Airline.Generator.Kafka.Host/appsettings.Development.json b/Airline.Generator.Kafka.Host/appsettings.Development.json index b2dcdb674..0c208ae91 100644 --- a/Airline.Generator.Kafka.Host/appsettings.Development.json +++ b/Airline.Generator.Kafka.Host/appsettings.Development.json @@ -2,7 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } } } diff --git a/Airline.Generator.Kafka.Host/appsettings.json b/Airline.Generator.Kafka.Host/appsettings.json index b2dcdb674..10f68b8c8 100644 --- a/Airline.Generator.Kafka.Host/appsettings.json +++ b/Airline.Generator.Kafka.Host/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } - } + }, + "AllowedHosts": "*" } From ccec9e9684822f037917e2a99adec5ea88be202d Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 23 Dec 2025 20:45:17 +0400 Subject: [PATCH 28/36] new kafka infrastructure --- .../Airline.Infrastructure.Kafka.csproj | 20 +++ .../AirlineKafkaConsumer.cs | 121 ++++++++++++++++++ .../Deserializers/AirlineKeyDeserializer.cs | 25 ++++ .../Deserializers/AirlineValueDeserializer.cs | 26 ++++ Airline.sln | 6 + 5 files changed, 198 insertions(+) create mode 100644 Airline.Infrastructure.Kafka/Airline.Infrastructure.Kafka.csproj create mode 100644 Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs create mode 100644 Airline.Infrastructure.Kafka/Deserializers/AirlineKeyDeserializer.cs create mode 100644 Airline.Infrastructure.Kafka/Deserializers/AirlineValueDeserializer.cs diff --git a/Airline.Infrastructure.Kafka/Airline.Infrastructure.Kafka.csproj b/Airline.Infrastructure.Kafka/Airline.Infrastructure.Kafka.csproj new file mode 100644 index 000000000..09fa9184d --- /dev/null +++ b/Airline.Infrastructure.Kafka/Airline.Infrastructure.Kafka.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs new file mode 100644 index 000000000..b573c1c1b --- /dev/null +++ b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs @@ -0,0 +1,121 @@ +using Confluent.Kafka; +using Airline.Application.Contracts.Flight; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Airline.Infrastructure.Kafka; + +/// +/// Kafka background consumer that subscribes to configured topic and creates flights from received contracts +/// +/// Kafka consumer instance +/// Service scope factory used to resolve scoped services +/// Application configuration used to read Kafka settings +/// Logger instance +public sealed class KafkaConsumer( + IConsumer> consumer, + IServiceScopeFactory scopeFactory, + IConfiguration configuration, + ILogger logger) : BackgroundService +{ + private readonly string _topicName = + configuration.GetSection("Kafka")["TopicName"] ?? throw new KeyNotFoundException("TopicName section of Kafka is missing"); + + /// + /// Starts consumer execution loop + /// + /// Cancellation token + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + + try + { + consumer.Subscribe(_topicName); + logger.LogInformation("Consumer successfully subscribed to topic {topic}", _topicName); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to subscribe consumer {consumer} to topic {topic}", consumer.Name, _topicName); + return; + } + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var consumeResult = consumer.Consume(stoppingToken); + + if (consumeResult?.Message?.Value is null || consumeResult.Message.Value.Count == 0) + continue; + + logger.LogInformation( + "Consumed message {key} from topic {topic} via consumer {consumer}", + consumeResult.Message.Key, _topicName, consumer.Name); + + using var scope = scopeFactory.CreateScope(); + var flightService = scope.ServiceProvider.GetRequiredService(); + + foreach (var contract in consumeResult.Message.Value) + { + try + { + await flightService.CreateAsync(contract); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning(ex, "Skipping invalid flight contract ModelId={modelId}", contract.ModelId); + } + } + + consumer.Commit(consumeResult); + + logger.LogInformation( + "Successfully processed and committed message {key} from topic {topic} via consumer {consumer}", + consumeResult.Message.Key, _topicName, consumer.Name); + } + catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) + { + logger.LogWarning("Topic {topic} is not available yet, waiting...", _topicName); + await Task.Delay(2000, stoppingToken); + } + catch (OperationCanceledException) + { + // Токен отмены — выходим из цикла + logger.LogInformation("Kafka consumer stopping due to cancellation token"); + break; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to consume or process message from topic {topic}", _topicName); + await Task.Delay(1000, stoppingToken); + } + } + + // Закрытие потребителя автоматически происходит при Dispose + logger.LogInformation("Kafka consumer stopped"); + } + + /// + /// Automatically disposes the Kafka consumer when the service is disposed + /// + /// + public override void Dispose() + { + try + { + consumer?.Close(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Error during consumer close"); + } + finally + { + consumer?.Dispose(); + } + base.Dispose(); + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.Kafka/Deserializers/AirlineKeyDeserializer.cs b/Airline.Infrastructure.Kafka/Deserializers/AirlineKeyDeserializer.cs new file mode 100644 index 000000000..783109e07 --- /dev/null +++ b/Airline.Infrastructure.Kafka/Deserializers/AirlineKeyDeserializer.cs @@ -0,0 +1,25 @@ +using Confluent.Kafka; +using System.Text.Json; + +namespace Airline.Infrastructure.Kafka.Deserializers; + +/// +/// Kafka deserializer for message key represented as string encoded in JSON +/// +public class AirlineKeyDeserializer : IDeserializer +{ + /// + /// Deserializes Kafka message key payload into string value + /// + /// Raw message key bytes + /// Indicates that the key is null + /// Serialization context + /// Deserialized string key + public string Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) + { + if (isNull) + return null; + + return JsonSerializer.Deserialize(data); + } +} \ No newline at end of file diff --git a/Airline.Infrastructure.Kafka/Deserializers/AirlineValueDeserializer.cs b/Airline.Infrastructure.Kafka/Deserializers/AirlineValueDeserializer.cs new file mode 100644 index 000000000..d1e121d26 --- /dev/null +++ b/Airline.Infrastructure.Kafka/Deserializers/AirlineValueDeserializer.cs @@ -0,0 +1,26 @@ +using Confluent.Kafka; +using Airline.Application.Contracts.Flight; +using System.Text.Json; + +namespace Airline.Infrastructure.Kafka.Deserializers; + +/// +/// Kafka deserializer for message value represented as JSON array of CreateFlightDto contracts +/// +public sealed class AirlineValueDeserializer : IDeserializer> +{ + /// + /// Deserializes Kafka message value payload into list of CreateFlightDto contracts + /// + /// Raw message value bytes + /// Indicates that the value is null + /// Serialization context + /// Deserialized list of contracts or empty list when value is null + public IList Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) + { + if (isNull) + return []; + + return JsonSerializer.Deserialize>(data) ?? []; + } +} \ No newline at end of file diff --git a/Airline.sln b/Airline.sln index 9bb66297b..05c3d3d28 100644 --- a/Airline.sln +++ b/Airline.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.ServiceDefaults", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Generator.Kafka.Host", "Airline.Generator.Kafka.Host\Airline.Generator.Kafka.Host.csproj", "{21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Airline.Infrastructure.Kafka", "Airline.Infrastructure.Kafka\Airline.Infrastructure.Kafka.csproj", "{83DE4DA4-488F-44FB-BD92-7D0462CD934B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {21DA2267-8FDD-9E3B-3B8F-DE4339C512AB}.Release|Any CPU.Build.0 = Release|Any CPU + {83DE4DA4-488F-44FB-BD92-7D0462CD934B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83DE4DA4-488F-44FB-BD92-7D0462CD934B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83DE4DA4-488F-44FB-BD92-7D0462CD934B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83DE4DA4-488F-44FB-BD92-7D0462CD934B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 86d89684010e9c65784d52cc3b13ea9b076b2cb7 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 00:37:48 +0400 Subject: [PATCH 29/36] the end is near --- Airline.Api.Host/Airline.Api.Host.csproj | 1 + Airline.Api.Host/Program.cs | 16 +++ Airline.AppHost/Airline.AppHost.csproj | 1 + Airline.AppHost/AppHost.cs | 26 +++-- .../AirlineKafkaConsumer.cs | 98 +++++-------------- 5 files changed, 62 insertions(+), 80 deletions(-) diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj index bbd0934d2..b2aaf95a6 100644 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -16,6 +16,7 @@ + diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index 9220937f6..ed8208c08 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -10,6 +10,8 @@ using Airline.Domain.Items; using Airline.Infrastructure.EfCore; using Airline.Infrastructure.EfCore.Repositories; +using Airline.Infrastructure.Kafka; +using Airline.Infrastructure.Kafka.Deserializers; using Airline.ServiceDefaults; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; @@ -75,6 +77,20 @@ o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); }); +// Kafka Consumer +builder.Services.AddHostedService(); +builder.AddKafkaConsumer>("airline-kafka", + configureBuilder: kafkaBuilder => + { + kafkaBuilder.SetKeyDeserializer(new AirlineKeyDeserializer()); + kafkaBuilder.SetValueDeserializer(new AirlineValueDeserializer()); + }, + configureSettings: settings => + { + settings.Config.GroupId = "airline-consumer"; + settings.Config.AutoOffsetReset = Confluent.Kafka.AutoOffsetReset.Earliest; + }); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index 7f90e0e9f..c6046854f 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -20,6 +20,7 @@ + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index 89e9a0a7f..93a7609a3 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -2,11 +2,25 @@ var builder = DistributedApplication.CreateBuilder(args); -var db = builder.AddMongoDB("mongo").AddDatabase("db"); +var mongo = builder.AddMongoDB("mongo-airline") + .AddDatabase("AirlineDb"); -builder.AddProject("airline-api-host") - .WithReference(db, "airlineClient") - .WaitFor(db); -builder.AddProject("airline-generator-kafka-host"); -builder.Build().Run(); +var kafka = builder.AddKafka("airline-kafka") + .WithKafkaUI(); +var apiHost = builder.AddProject("airline-api-host") + .WithReference(mongo, "airlineDb") + .WithReference(kafka) + .WithEnvironment("KAFKA_BOOTSTRAP_SERVERS", kafka.GetEndpoint("tcp")) + .WithEnvironment("Kafka:TopicName", "airline-contracts") + .WaitFor(mongo) + .WaitFor(kafka); + +builder.AddProject("airline-generator-kafka-host") + .WithReference(kafka) + .WithEnvironment("KAFKA_BOOTSTRAP_SERVERS", kafka.GetEndpoint("tcp")) + .WithEnvironment("Kafka:TopicName", "airline-contracts") + .WaitFor(kafka) + .WaitFor(apiHost); + +builder.Build().Run(); \ No newline at end of file diff --git a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs index b573c1c1b..58ccce021 100644 --- a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs +++ b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs @@ -8,13 +8,13 @@ namespace Airline.Infrastructure.Kafka; /// -/// Kafka background consumer that subscribes to configured topic and creates flights from received contracts +/// Служба для чтения данных из топика Kafka /// -/// Kafka consumer instance -/// Service scope factory used to resolve scoped services -/// Application configuration used to read Kafka settings -/// Logger instance -public sealed class KafkaConsumer( +/// Kafka-консьюмер +/// Фабрика контекста +/// Конфигурация +/// Логгер +public class KafkaConsumer( IConsumer> consumer, IServiceScopeFactory scopeFactory, IConfiguration configuration, @@ -23,99 +23,49 @@ public sealed class KafkaConsumer( private readonly string _topicName = configuration.GetSection("Kafka")["TopicName"] ?? throw new KeyNotFoundException("TopicName section of Kafka is missing"); - /// - /// Starts consumer execution loop - /// - /// Cancellation token + /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + stoppingToken.ThrowIfCancellationRequested(); await Task.Yield(); + await Consume(stoppingToken); + } - try - { - consumer.Subscribe(_topicName); - logger.LogInformation("Consumer successfully subscribed to topic {topic}", _topicName); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to subscribe consumer {consumer} to topic {topic}", consumer.Name, _topicName); - return; - } + /// + /// Хендлер для обработки получаемого сообщения + /// + /// Токен отмены + private async Task Consume(CancellationToken stoppingToken) + { + consumer.Subscribe(_topicName); + logger.LogInformation("Consumer successfully subscribed to topic {topic}", _topicName); - while (!stoppingToken.IsCancellationRequested) + try { - try + while (!stoppingToken.IsCancellationRequested) { + logger.LogInformation("Consuming from topic {topic} via consumer {consumer}", _topicName, consumer.Name); var consumeResult = consumer.Consume(stoppingToken); if (consumeResult?.Message?.Value is null || consumeResult.Message.Value.Count == 0) continue; - logger.LogInformation( - "Consumed message {key} from topic {topic} via consumer {consumer}", - consumeResult.Message.Key, _topicName, consumer.Name); - using var scope = scopeFactory.CreateScope(); var flightService = scope.ServiceProvider.GetRequiredService(); foreach (var contract in consumeResult.Message.Value) { - try - { - await flightService.CreateAsync(contract); - } - catch (KeyNotFoundException ex) - { - logger.LogWarning(ex, "Skipping invalid flight contract ModelId={modelId}", contract.ModelId); - } + await flightService.CreateAsync(contract); } consumer.Commit(consumeResult); - - logger.LogInformation( - "Successfully processed and committed message {key} from topic {topic} via consumer {consumer}", + logger.LogInformation("Successfully consumed message {key} from topic {topic} via consumer {consumer}", consumeResult.Message.Key, _topicName, consumer.Name); } - catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) - { - logger.LogWarning("Topic {topic} is not available yet, waiting...", _topicName); - await Task.Delay(2000, stoppingToken); - } - catch (OperationCanceledException) - { - // Токен отмены — выходим из цикла - logger.LogInformation("Kafka consumer stopping due to cancellation token"); - break; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to consume or process message from topic {topic}", _topicName); - await Task.Delay(1000, stoppingToken); - } - } - - // Закрытие потребителя автоматически происходит при Dispose - logger.LogInformation("Kafka consumer stopped"); - } - - /// - /// Automatically disposes the Kafka consumer when the service is disposed - /// - /// - public override void Dispose() - { - try - { - consumer?.Close(); } catch (Exception ex) { - logger.LogWarning(ex, "Error during consumer close"); - } - finally - { - consumer?.Dispose(); + logger.LogError(ex, "Exception occurred during receiving contracts from {topic}", _topicName); } - base.Dispose(); } } \ No newline at end of file From 27d858f7e02127dd5eb6f9117f0269ce70c6f809 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 01:37:57 +0400 Subject: [PATCH 30/36] something is working --- Airline.Api.Host/Airline.Api.Host.csproj | 2 +- Airline.Api.Host/Program.cs | 5 +++-- Airline.AppHost/Airline.AppHost.csproj | 4 ++-- Airline.AppHost/AppHost.cs | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Airline.Api.Host/Airline.Api.Host.csproj b/Airline.Api.Host/Airline.Api.Host.csproj index b2aaf95a6..7491da9b3 100644 --- a/Airline.Api.Host/Airline.Api.Host.csproj +++ b/Airline.Api.Host/Airline.Api.Host.csproj @@ -8,7 +8,7 @@ - + diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index ed8208c08..eff88abe3 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -68,13 +68,14 @@ } }); + // MongoDB builder.AddMongoDBClient("airlineClient"); builder.Services.AddDbContext((services, o) => { - var db = services.GetRequiredService(); - o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); + var client = services.GetRequiredService(); + o.UseMongoDB(client, "db"); }); // Kafka Consumer diff --git a/Airline.AppHost/Airline.AppHost.csproj b/Airline.AppHost/Airline.AppHost.csproj index c6046854f..40fa4d116 100644 --- a/Airline.AppHost/Airline.AppHost.csproj +++ b/Airline.AppHost/Airline.AppHost.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index 93a7609a3..18b685b9b 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -2,18 +2,18 @@ var builder = DistributedApplication.CreateBuilder(args); -var mongo = builder.AddMongoDB("mongo-airline") - .AddDatabase("AirlineDb"); +var db = builder.AddMongoDB("mongo") + .AddDatabase("db"); var kafka = builder.AddKafka("airline-kafka") .WithKafkaUI(); var apiHost = builder.AddProject("airline-api-host") - .WithReference(mongo, "airlineDb") + .WithReference(db, "airlineClient") .WithReference(kafka) .WithEnvironment("KAFKA_BOOTSTRAP_SERVERS", kafka.GetEndpoint("tcp")) .WithEnvironment("Kafka:TopicName", "airline-contracts") - .WaitFor(mongo) + .WaitFor(db) .WaitFor(kafka); builder.AddProject("airline-generator-kafka-host") From 295b5e5fec77bd4294c50628877b4690eade7979 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 02:35:46 +0400 Subject: [PATCH 31/36] i suppose, it's the end --- .../Controllers/GeneratorController.cs | 14 ++++++++++---- .../Generator/FlightGenerator.cs | 17 ++++++++++++----- Airline.Generator.Kafka.Host/appsettings.json | 6 ++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs index 9922f3c67..423bb7f2a 100644 --- a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs +++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs @@ -40,17 +40,23 @@ public async Task>> Get( var list = new List(payloadLimit); var counter = 0; - var modelIds = configuration.GetSection("Generator:SeedModelIds") - .Get>() ?? []; + var modelId = configuration.GetSection("FlightGenerator:ModelFamilyId") + .Get() ?? Array.Empty(); - if (modelIds.Count == 0) + var departureCity = configuration.GetSection("FlightGenerator:DepartureCity") + .Get() ?? Array.Empty(); + + var arrivalCity = configuration.GetSection("FlightGenerator:ArrivalCity") + .Get() ?? Array.Empty(); + + if (modelId.Length == 0 || departureCity.Length == 0 || arrivalCity.Length == 0) return StatusCode(StatusCodes.Status500InternalServerError, "SeedModelIds is empty"); while (counter < payloadLimit) { var currentBatchSize = Math.Min(batchSize, payloadLimit - counter); - var batch = FlightGenerator.GenerateContracts(currentBatchSize, modelIds); + var batch = FlightGenerator.GenerateContracts(currentBatchSize, modelId, departureCity, arrivalCity); await producerService.SendAsync(batch); diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs index 685c2c2d7..40dcc5434 100644 --- a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs +++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs @@ -1,5 +1,6 @@ -using Bogus; -using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Flight; +using Airline.Domain.Items; +using Bogus; namespace Airline.Generator.Kafka.Host.Generator; @@ -13,13 +14,19 @@ public static class FlightGenerator /// /// Number of contracts to generate /// Pool of existing aircraft model identifiers + /// Pool of existing departure cities identifiers + /// Pool of existing arrival cities identifiers /// Generated list of flight contracts - public static List GenerateContracts(int count, IList modelIds) => + public static List GenerateContracts( + int count, + IList modelIds, + IList departureCity, + IList arrivalCity) => new Faker() .CustomInstantiator(f => new CreateFlightDto( FlightCode: f.Random.String2(2, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + f.Random.Number(100, 999), - DepartureCity: f.Address.City(), - ArrivalCity: f.Address.City(), + DepartureCity: f.PickRandom(departureCity), + ArrivalCity: f.PickRandom(arrivalCity), DepartureDateTime: f.Date.Future(), ArrivalDateTime: f.Date.Future().AddHours(f.Random.Number(1, 24)), ModelId: f.PickRandom(modelIds) diff --git a/Airline.Generator.Kafka.Host/appsettings.json b/Airline.Generator.Kafka.Host/appsettings.json index 10f68b8c8..9cea2c9e0 100644 --- a/Airline.Generator.Kafka.Host/appsettings.json +++ b/Airline.Generator.Kafka.Host/appsettings.json @@ -1,4 +1,10 @@ { + "FlightGenerator": { + "ModelFamilyId": [ 1, 2, 3, 4, 5 ], + "DepartureCity": [ "Moscow", "Samara", "Rome", "New York", "Berlin", "Paris" ], + "ArrivalCity": [ "Wonderland", "London", "Milan", "Moscow", "Tokyo", "Paris" ] + }, + "Logging": { "LogLevel": { "Default": "Information", From 79306f5980c6e1e9b71a1e243c055129c4136fb7 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 03:38:39 +0400 Subject: [PATCH 32/36] some fix --- Airline.Api.Host/Program.cs | 2 +- .../AirlineKafkaConsumer.cs | 156 +++++++++++++----- 2 files changed, 115 insertions(+), 43 deletions(-) diff --git a/Airline.Api.Host/Program.cs b/Airline.Api.Host/Program.cs index eff88abe3..837b54852 100644 --- a/Airline.Api.Host/Program.cs +++ b/Airline.Api.Host/Program.cs @@ -79,7 +79,7 @@ }); // Kafka Consumer -builder.Services.AddHostedService(); +builder.Services.AddHostedService(); builder.AddKafkaConsumer>("airline-kafka", configureBuilder: kafkaBuilder => { diff --git a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs index 58ccce021..9cbf71e8b 100644 --- a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs +++ b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs @@ -1,5 +1,8 @@ -using Confluent.Kafka; -using Airline.Application.Contracts.Flight; +using Airline.Application.Contracts.Flight; +using Airline.Domain; +using Airline.Domain.Items; +using Airline.Infrastructure.Kafka.Deserializers; +using Confluent.Kafka; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -8,64 +11,133 @@ namespace Airline.Infrastructure.Kafka; /// -/// Служба для чтения данных из топика Kafka +/// Background Kafka consumer that subscribes to configured topic and processes flight contract batches. /// -/// Kafka-консьюмер -/// Фабрика контекста -/// Конфигурация -/// Логгер -public class KafkaConsumer( - IConsumer> consumer, +public sealed class FlightKafkaConsumer( IServiceScopeFactory scopeFactory, IConfiguration configuration, - ILogger logger) : BackgroundService + ILogger logger +) : BackgroundService { private readonly string _topicName = - configuration.GetSection("Kafka")["TopicName"] ?? throw new KeyNotFoundException("TopicName section of Kafka is missing"); - - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - stoppingToken.ThrowIfCancellationRequested(); - await Task.Yield(); - await Consume(stoppingToken); - } + configuration["Kafka:TopicName"] ?? throw new KeyNotFoundException("Kafka:TopicName is missing"); /// - /// Хендлер для обработки получаемого сообщения + /// Executes the message consumption loop with automatic reconnection and handling of topic unavailability. /// - /// Токен отмены - private async Task Consume(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - consumer.Subscribe(_topicName); - logger.LogInformation("Consumer successfully subscribed to topic {topic}", _topicName); + var bootstrapServers = (configuration["KAFKA_BOOTSTRAP_SERVERS"] ?? "localhost:9092") + .Replace("tcp://", ""); + + logger.LogInformation("Kafka bootstrap servers: {bootstrapServers}", bootstrapServers); + logger.LogInformation("Kafka topic name: {topicName}", _topicName); - try + while (!stoppingToken.IsCancellationRequested) { - while (!stoppingToken.IsCancellationRequested) + try { - logger.LogInformation("Consuming from topic {topic} via consumer {consumer}", _topicName, consumer.Name); - var consumeResult = consumer.Consume(stoppingToken); + var groupId = "airline-consumer-group-permanent"; - if (consumeResult?.Message?.Value is null || consumeResult.Message.Value.Count == 0) - continue; + var consumerConfig = new ConsumerConfig + { + BootstrapServers = bootstrapServers, + GroupId = groupId, + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = true, + SocketTimeoutMs = 20000, + SessionTimeoutMs = 10000, + HeartbeatIntervalMs = 3000 + }; - using var scope = scopeFactory.CreateScope(); - var flightService = scope.ServiceProvider.GetRequiredService(); + using var consumer = new ConsumerBuilder>(consumerConfig) + .SetKeyDeserializer(new AirlineKeyDeserializer()) + .SetValueDeserializer(new AirlineValueDeserializer()) + .Build(); - foreach (var contract in consumeResult.Message.Value) + consumer.Subscribe(_topicName); + logger.LogInformation("Consumer successfully subscribed to topic {topic} with GroupId {groupId}", _topicName, groupId); + + while (!stoppingToken.IsCancellationRequested) { - await flightService.CreateAsync(contract); - } + try + { + var consumeResult = consumer.Consume(stoppingToken); + + if (consumeResult?.Message?.Value is null || consumeResult.Message.Value.Count == 0) + continue; + + logger.LogInformation( + "Consumed message {key} from topic {topic} (Partition: {partition}, Offset: {offset})", + consumeResult.Message.Key, + _topicName, + consumeResult.TopicPartition.Partition, + consumeResult.Offset); + + using var scope = scopeFactory.CreateScope(); + var flightService = scope.ServiceProvider.GetRequiredService(); + + foreach (var contract in consumeResult.Message.Value) + { + try + { + var modelExists = await scope.ServiceProvider + .GetRequiredService>() + .GetAsync(contract.ModelId) != null; - consumer.Commit(consumeResult); - logger.LogInformation("Successfully consumed message {key} from topic {topic} via consumer {consumer}", - consumeResult.Message.Key, _topicName, consumer.Name); + if (!modelExists) + { + logger.LogWarning("Skipping flight {code}: ModelId {modelId} does not exist", + contract.FlightCode, contract.ModelId); + continue; + } + + await flightService.CreateAsync(contract); + logger.LogInformation("Successfully created flight {code} in database", contract.FlightCode); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Skipping invalid flight contract: Code={code}, ModelId={modelId}", + contract.FlightCode, contract.ModelId); + } + } + + consumer.Commit(consumeResult); + logger.LogInformation("Successfully processed and committed message {key} from topic {topic}", + consumeResult.Message.Key, _topicName); + } + catch (ConsumeException ex) when (ex.Error.Code == ErrorCode.UnknownTopicOrPart) + { + logger.LogWarning("Topic {topic} is not available yet, waiting 5 seconds before retry...", _topicName); + await Task.Delay(5000, stoppingToken); + break; + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + logger.LogInformation("Consumer operation cancelled due to shutdown request"); + return; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to consume or process message from topic {topic}", _topicName); + await Task.Delay(2000, stoppingToken); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Consumer encountered an unrecoverable error, restarting in 5 seconds..."); + await Task.Delay(5000, stoppingToken); } } - catch (Exception ex) - { - logger.LogError(ex, "Exception occurred during receiving contracts from {topic}", _topicName); - } + } + + /// + /// Gracefully stops the consumer when the application shuts down. + /// + public override async Task StopAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Kafka consumer is stopping"); + await base.StopAsync(stoppingToken); } } \ No newline at end of file From 5138bde04829bc07f362cd79c2aee40339ed3ced Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 19:17:01 +0400 Subject: [PATCH 33/36] no comments --- Airline.AppHost/AppHost.cs | 2 -- .../Controllers/GeneratorController.cs | 6 +++--- .../Generator/FlightGenerator.cs | 1 - .../Interface/IProducerService.cs | 16 ---------------- .../AirlineKafkaConsumer.cs | 10 +++++----- 5 files changed, 8 insertions(+), 27 deletions(-) delete mode 100644 Airline.Generator.Kafka.Host/Interface/IProducerService.cs diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index 18b685b9b..69e828eba 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -1,5 +1,3 @@ -using Aspire.Hosting; - var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddMongoDB("mongo") diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs index 423bb7f2a..961fa7f69 100644 --- a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs +++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs @@ -41,13 +41,13 @@ public async Task>> Get( var counter = 0; var modelId = configuration.GetSection("FlightGenerator:ModelFamilyId") - .Get() ?? Array.Empty(); + .Get() ?? []; var departureCity = configuration.GetSection("FlightGenerator:DepartureCity") - .Get() ?? Array.Empty(); + .Get() ?? []; var arrivalCity = configuration.GetSection("FlightGenerator:ArrivalCity") - .Get() ?? Array.Empty(); + .Get() ?? []; if (modelId.Length == 0 || departureCity.Length == 0 || arrivalCity.Length == 0) return StatusCode(StatusCodes.Status500InternalServerError, "SeedModelIds is empty"); diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs index 40dcc5434..14a3d8f76 100644 --- a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs +++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs @@ -1,5 +1,4 @@ using Airline.Application.Contracts.Flight; -using Airline.Domain.Items; using Bogus; namespace Airline.Generator.Kafka.Host.Generator; diff --git a/Airline.Generator.Kafka.Host/Interface/IProducerService.cs b/Airline.Generator.Kafka.Host/Interface/IProducerService.cs deleted file mode 100644 index 57c3795b1..000000000 --- a/Airline.Generator.Kafka.Host/Interface/IProducerService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Airline.Application.Contracts.Flight; - -namespace Airline.Generator.Kafka.Host.Interface; - -/// -/// Abstraction for producing batches of flight create contracts to a Kafka message broker -/// -public interface IProducerService -{ - /// - /// Sends a batch of flight create contracts to Kafka - /// - /// Batch of flight create contracts to send - /// Task representing asynchronous send operation - public Task SendAsync(IList batch); -} \ No newline at end of file diff --git a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs index 9cbf71e8b..aa2b91898 100644 --- a/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs +++ b/Airline.Infrastructure.Kafka/AirlineKafkaConsumer.cs @@ -1,8 +1,8 @@ -using Airline.Application.Contracts.Flight; +using Confluent.Kafka; +using Airline.Application.Contracts.Flight; using Airline.Domain; using Airline.Domain.Items; using Airline.Infrastructure.Kafka.Deserializers; -using Confluent.Kafka; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -11,7 +11,7 @@ namespace Airline.Infrastructure.Kafka; /// -/// Background Kafka consumer that subscribes to configured topic and processes flight contract batches. +/// Kafka consumer service that processes flight contracts from a specified topic and persists them to the database. /// public sealed class FlightKafkaConsumer( IServiceScopeFactory scopeFactory, @@ -23,7 +23,7 @@ ILogger logger configuration["Kafka:TopicName"] ?? throw new KeyNotFoundException("Kafka:TopicName is missing"); /// - /// Executes the message consumption loop with automatic reconnection and handling of topic unavailability. + /// Initializes the Kafka consumer and starts the message processing loop with automatic reconnection. /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -133,7 +133,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } /// - /// Gracefully stops the consumer when the application shuts down. + /// Performs graceful shutdown of the Kafka consumer service. /// public override async Task StopAsync(CancellationToken stoppingToken) { From 5a00b4e9db0ec38631bad3c92df1c535b5fdb332 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 24 Dec 2025 19:28:18 +0400 Subject: [PATCH 34/36] =?UTF-8?q?=D1=81lear=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Airline.AppHost/AppHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Airline.AppHost/AppHost.cs b/Airline.AppHost/AppHost.cs index 69e828eba..88bdd1e8d 100644 --- a/Airline.AppHost/AppHost.cs +++ b/Airline.AppHost/AppHost.cs @@ -3,7 +3,7 @@ var db = builder.AddMongoDB("mongo") .AddDatabase("db"); -var kafka = builder.AddKafka("airline-kafka") +var kafka = builder.AddKafka("airline-kafka", 9092) .WithKafkaUI(); var apiHost = builder.AddProject("airline-api-host") From 0f5e92f9000a7941a541411e958cb2ce7ded9adc Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 25 Dec 2025 14:15:50 +0400 Subject: [PATCH 35/36] first fixes --- .../Airline.Generator.Kafka.Host.http | 6 ------ Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http diff --git a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http b/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http deleted file mode 100644 index ce1e47ffd..000000000 --- a/Airline.Generator.Kafka.Host/Airline.Generator.Kafka.Host.http +++ /dev/null @@ -1,6 +0,0 @@ -@Airline.Generator.Kafka.Host_HostAddress = http://localhost:5206 - -GET {{Airline.Generator.Kafka.Host_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs index 14a3d8f76..e3827d312 100644 --- a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs +++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs @@ -23,7 +23,7 @@ public static List GenerateContracts( IList arrivalCity) => new Faker() .CustomInstantiator(f => new CreateFlightDto( - FlightCode: f.Random.String2(2, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + f.Random.Number(100, 999), + FlightCode: $"{f.Random.Char('A', 'Z')}{f.Random.Char('A', 'Z')}{f.Random.Number(100, 999)}", DepartureCity: f.PickRandom(departureCity), ArrivalCity: f.PickRandom(arrivalCity), DepartureDateTime: f.Date.Future(), From c0d1fbd38c3a782f23ccefbf7e64e44aeeef6f92 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 25 Dec 2025 19:49:20 +0400 Subject: [PATCH 36/36] last fixes --- .../Controllers/GeneratorController.cs | 28 +++------ .../Generator/FlightGenerator.cs | 60 +++++++++++++------ Airline.Generator.Kafka.Host/Program.cs | 12 ++-- 3 files changed, 56 insertions(+), 44 deletions(-) diff --git a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs index 961fa7f69..4b4ed28ee 100644 --- a/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs +++ b/Airline.Generator.Kafka.Host/Controllers/GeneratorController.cs @@ -10,13 +10,11 @@ namespace Airline.Generator.Kafka.Host.Controllers; /// /// Logger instance /// Producer service used to send contracts -/// Configuration instance used to read generator settings [Route("api/[controller]")] [ApiController] public sealed class GeneratorController( ILogger logger, - IProducerService producerService, - IConfiguration configuration) : ControllerBase + IProducerService producerService) : ControllerBase { /// /// Generates flight contracts and sends them via Kafka using batches and delay between sends @@ -33,44 +31,32 @@ public async Task>> Get( [FromQuery] int payloadLimit, [FromQuery] int waitTime) { - logger.LogInformation("Generating {limit} contracts via {batchSize} batches and {waitTime}s delay", payloadLimit, batchSize, waitTime); + logger.LogInformation("Generating {limit} contracts via {batchSize} batches and {waitTime}s delay", + payloadLimit, batchSize, waitTime); try { - var list = new List(payloadLimit); + var results = new List(); var counter = 0; - var modelId = configuration.GetSection("FlightGenerator:ModelFamilyId") - .Get() ?? []; - - var departureCity = configuration.GetSection("FlightGenerator:DepartureCity") - .Get() ?? []; - - var arrivalCity = configuration.GetSection("FlightGenerator:ArrivalCity") - .Get() ?? []; - - if (modelId.Length == 0 || departureCity.Length == 0 || arrivalCity.Length == 0) - return StatusCode(StatusCodes.Status500InternalServerError, "SeedModelIds is empty"); - while (counter < payloadLimit) { var currentBatchSize = Math.Min(batchSize, payloadLimit - counter); - - var batch = FlightGenerator.GenerateContracts(currentBatchSize, modelId, departureCity, arrivalCity); + var batch = FlightGenerator.GenerateContracts(currentBatchSize); await producerService.SendAsync(batch); logger.LogInformation("Batch of {batchSize} items has been sent", currentBatchSize); + results.AddRange(batch); counter += currentBatchSize; - list.AddRange(batch); if (counter < payloadLimit && waitTime > 0) await Task.Delay(waitTime * 1000); } logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); - return Ok(list); + return Ok(results); } catch (Exception ex) { diff --git a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs index e3827d312..a297db77b 100644 --- a/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs +++ b/Airline.Generator.Kafka.Host/Generator/FlightGenerator.cs @@ -8,27 +8,51 @@ namespace Airline.Generator.Kafka.Host.Generator; /// public static class FlightGenerator { + private static int[]? _modelFamilyIds; + private static string[]? _departureCities; + private static string[]? _arrivalCities; + + /// + /// Initializes the generator with configuration data from the specified configuration source. + /// + /// Configuration instance containing generator settings. + public static void Initialize(IConfiguration configuration) + { + _modelFamilyIds = configuration.GetSection("FlightGenerator:ModelFamilyId").Get() ?? []; + _departureCities = configuration.GetSection("FlightGenerator:DepartureCity").Get() ?? []; + _arrivalCities = configuration.GetSection("FlightGenerator:ArrivalCity").Get() ?? []; + } + /// /// Generates a list of flight create contracts /// /// Number of contracts to generate - /// Pool of existing aircraft model identifiers - /// Pool of existing departure cities identifiers - /// Pool of existing arrival cities identifiers /// Generated list of flight contracts - public static List GenerateContracts( - int count, - IList modelIds, - IList departureCity, - IList arrivalCity) => - new Faker() - .CustomInstantiator(f => new CreateFlightDto( - FlightCode: $"{f.Random.Char('A', 'Z')}{f.Random.Char('A', 'Z')}{f.Random.Number(100, 999)}", - DepartureCity: f.PickRandom(departureCity), - ArrivalCity: f.PickRandom(arrivalCity), - DepartureDateTime: f.Date.Future(), - ArrivalDateTime: f.Date.Future().AddHours(f.Random.Number(1, 24)), - ModelId: f.PickRandom(modelIds) - )) - .Generate(count); + public static List GenerateContracts(int count) + { + if (_modelFamilyIds?.Length == 0 || + _departureCities?.Length == 0 || + _arrivalCities?.Length == 0) + { + throw new InvalidOperationException("FlightGenerator configuration is empty"); + } + + try + { + return new Faker() + .CustomInstantiator(f => new CreateFlightDto( + FlightCode: $"{f.Random.Char('A', 'Z')}{f.Random.Char('A', 'Z')}{f.Random.Number(100, 999)}", + DepartureCity: f.PickRandom(_departureCities), + ArrivalCity: f.PickRandom(_arrivalCities), + DepartureDateTime: f.Date.Future(), + ArrivalDateTime: f.Date.Future().AddHours(f.Random.Number(1, 10)), + ModelId: f.PickRandom(_modelFamilyIds) + )) + .Generate(count); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to generate contracts", ex); + } + } } \ No newline at end of file diff --git a/Airline.Generator.Kafka.Host/Program.cs b/Airline.Generator.Kafka.Host/Program.cs index 589fdbc12..738a0f7f4 100644 --- a/Airline.Generator.Kafka.Host/Program.cs +++ b/Airline.Generator.Kafka.Host/Program.cs @@ -1,21 +1,23 @@ using Airline.Application.Contracts.Flight; using Airline.Generator.Kafka.Host; +using Airline.Generator.Kafka.Host.Generator; using Airline.Generator.Kafka.Host.Interfaces; using Airline.Generator.Kafka.Host.Serializers; using Airline.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); -builder.AddKafkaProducer>( - "airline-kafka", - kafkaBuilder => +builder.AddServiceDefaults(); + +FlightGenerator.Initialize(builder.Configuration); + +builder.AddKafkaProducer>("airline-kafka", + configureBuilder: kafkaBuilder => { kafkaBuilder.SetKeySerializer(new AirlineKeySerializer()); kafkaBuilder.SetValueSerializer(new AirlineValueSerializer()); }); -builder.AddServiceDefaults(); - builder.Services.AddScoped(); builder.Services.AddControllers();