From 6f5aa27c04b0736bd53a15f4719e70e6e46bb301 Mon Sep 17 00:00:00 2001 From: Cure232 Date: Tue, 24 Feb 2026 08:19:18 +0400 Subject: [PATCH 1/5] lab1: created class structure and unit-tests --- .../Agency.Domain/Agency.Domain.csproj | 9 + .../Agency.Domain/Data/DataSeeder.cs | 403 ++++++++++++++++++ .../Agency.Domain/Model/ContractRequest.cs | 48 +++ .../Model/ContractRequestType.cs | 15 + .../Agency.Domain/Model/Counterparty.cs | 39 ++ .../Agency.Domain/Model/RealEstate.cs | 83 ++++ .../Agency.Domain/Model/RealEstatePurpose.cs | 21 + .../Agency.Domain/Model/RealEstateType.cs | 24 ++ .../Agency.Tests/Agency.Tests.csproj | 28 ++ RealEstateAgency/Agency.Tests/DomainTests.cs | 259 +++++++++++ RealEstateAgency/RealEstateAgency.sln | 31 ++ 11 files changed, 960 insertions(+) create mode 100644 RealEstateAgency/Agency.Domain/Agency.Domain.csproj create mode 100644 RealEstateAgency/Agency.Domain/Data/DataSeeder.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/ContractRequest.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/ContractRequestType.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/Counterparty.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/RealEstate.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/RealEstatePurpose.cs create mode 100644 RealEstateAgency/Agency.Domain/Model/RealEstateType.cs create mode 100644 RealEstateAgency/Agency.Tests/Agency.Tests.csproj create mode 100644 RealEstateAgency/Agency.Tests/DomainTests.cs create mode 100644 RealEstateAgency/RealEstateAgency.sln diff --git a/RealEstateAgency/Agency.Domain/Agency.Domain.csproj b/RealEstateAgency/Agency.Domain/Agency.Domain.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Agency.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs b/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs new file mode 100644 index 000000000..c20d26560 --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs @@ -0,0 +1,403 @@ +using Agency.Domain.Model; + +namespace Agency.Domain.Data; + +/// +/// Данные для тестирования риэлторского агентства +/// +public class DataSeeder +{ + /// + /// Коллекция контрагентов (клиентов) + /// + public List Counterparties { get; } = []; + + /// + /// Коллекция объектов недвижимости + /// + public List RealEstates { get; } = []; + + /// + /// Коллекция заявок + /// + public List ContractRequests { get; } = []; + + /// + /// Конструктор, выполняющий инициализацию всех тестовых данных + /// + public DataSeeder() + { + // Инициализация контрагентов (клиентов) + Counterparties.AddRange( + [ + new Counterparty + { + Id = 1, + FullName = "Иванов Иван Иванович", + PassportNumber = "4501 123456", + PhoneNumber = "+7 (901) 123-45-67" + }, + new Counterparty + { + Id = 2, + FullName = "Петрова Анна Сергеевна", + PassportNumber = "4502 234567", + PhoneNumber = "+7 (902) 234-56-78" + }, + new Counterparty + { + Id = 3, + FullName = "Сидоров Петр Алексеевич", + PassportNumber = "4503 345678", + PhoneNumber = "+7 (903) 345-67-89" + }, + new Counterparty + { + Id = 4, + FullName = "Козлова Елена Дмитриевна", + PassportNumber = "4504 456789", + PhoneNumber = "+7 (904) 456-78-90" + }, + new Counterparty + { + Id = 5, + FullName = "Смирнов Алексей Владимирович", + PassportNumber = "4505 567890", + PhoneNumber = "+7 (905) 567-89-01" + }, + new Counterparty + { + Id = 6, + FullName = "Михайлова Ольга Игоревна", + PassportNumber = "4506 678901", + PhoneNumber = "+7 (906) 678-90-12" + }, + new Counterparty + { + Id = 7, + FullName = "Федоров Денис Андреевич", + PassportNumber = "4507 789012", + PhoneNumber = "+7 (907) 789-01-23" + }, + new Counterparty + { + Id = 8, + FullName = "Васильева Татьяна Павловна", + PassportNumber = "4508 890123", + PhoneNumber = "+7 (908) 890-12-34" + }, + new Counterparty + { + Id = 9, + FullName = "Николаев Артем Борисович", + PassportNumber = "4509 901234", + PhoneNumber = "+7 (909) 901-23-45" + }, + new Counterparty + { + Id = 10, + FullName = "Александрова Наталья Викторовна", + PassportNumber = "4510 012345", + PhoneNumber = "+7 (910) 012-34-56" + } + ] + ); + + // Инициализация объектов недвижимости + RealEstates.AddRange( + [ + new RealEstate + { + Id = 101, + Type = RealEstateType.Apartment, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "77:01:0001012:1234", + Address = "г. Москва, ул. Ленина, д. 10, кв. 25", + TotalFloors = 9, + TotalArea = 65.5, + NumberOfRooms = 3, + CeilingHeight = 2.7, + Floor = 5, + HasEncumbrances = false + }, + new RealEstate + { + Id = 102, + Type = RealEstateType.Apartment, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "77:01:0001012:5678", + Address = "г. Москва, ул. Ленина, д. 10, кв. 42", + TotalFloors = 9, + TotalArea = 45.0, + NumberOfRooms = 2, + CeilingHeight = 2.7, + Floor = 3, + HasEncumbrances = true, + EncumbrancesDescription = "Ипотека Сбербанк" + }, + new RealEstate + { + Id = 103, + Type = RealEstateType.House, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "50:12:0030215:789", + Address = "Московская обл., Одинцовский р-н, д. Петрово, ул. Центральная, д. 15", + TotalFloors = 2, + TotalArea = 150.0, + NumberOfRooms = 5, + CeilingHeight = 3.2, + Floor = null, + HasEncumbrances = false + }, + new RealEstate + { + Id = 104, + Type = RealEstateType.LandPlot, + Purpose = RealEstatePurpose.Agricultural, + CadastralNumber = "50:12:0030215:123", + Address = "Московская обл., Одинцовский р-н, уч. 45", + TotalFloors = null, + TotalArea = 1200.0, + NumberOfRooms = null, + CeilingHeight = null, + Floor = null, + HasEncumbrances = false + }, + new RealEstate + { + Id = 105, + Type = RealEstateType.Commercial, + Purpose = RealEstatePurpose.Commercial, + CadastralNumber = "77:01:0003025:456", + Address = "г. Москва, ул. Тверская, д. 5, пом. 1", + TotalFloors = 3, + TotalArea = 250.0, + NumberOfRooms = 6, + CeilingHeight = 4.5, + Floor = 1, + HasEncumbrances = true, + EncumbrancesDescription = "Аренда до 2026 г." + }, + new RealEstate + { + Id = 106, + Type = RealEstateType.Garage, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "77:01:0004018:789", + Address = "г. Москва, ГСК 'Автомобилист', бокс 12", + TotalFloors = 1, + TotalArea = 20.0, + NumberOfRooms = null, + CeilingHeight = 2.5, + Floor = 1, + HasEncumbrances = false + }, + new RealEstate + { + Id = 107, + Type = RealEstateType.Apartment, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "78:01:0001034:567", + Address = "г. Санкт-Петербург, Невский пр., д. 25, кв. 12", + TotalFloors = 5, + TotalArea = 82.0, + NumberOfRooms = 4, + CeilingHeight = 3.0, + Floor = 2, + HasEncumbrances = false + }, + new RealEstate + { + Id = 108, + Type = RealEstateType.Apartment, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "78:01:0001034:890", + Address = "г. Санкт-Петербург, Невский пр., д. 25, кв. 15", + TotalFloors = 5, + TotalArea = 55.0, + NumberOfRooms = 2, + CeilingHeight = 3.0, + Floor = 4, + HasEncumbrances = false + }, + new RealEstate + { + Id = 109, + Type = RealEstateType.Commercial, + Purpose = RealEstatePurpose.Commercial, + CadastralNumber = "78:01:0002056:234", + Address = "г. Санкт-Петербург, ул. Рубинштейна, д. 10", + TotalFloors = 4, + TotalArea = 180.0, + NumberOfRooms = 4, + CeilingHeight = 3.5, + Floor = 1, + HasEncumbrances = false + }, + new RealEstate + { + Id = 110, + Type = RealEstateType.House, + Purpose = RealEstatePurpose.Residential, + CadastralNumber = "47:14:0030215:456", + Address = "Ленинградская обл., Всеволожский р-н, д. Романовка, ул. Лесная, д. 7", + TotalFloors = 2, + TotalArea = 120.0, + NumberOfRooms = 4, + CeilingHeight = 2.8, + Floor = null, + HasEncumbrances = true, + EncumbrancesDescription = "Залог" + } + ] + ); + + // Инициализация заявок + ContractRequests.AddRange( + [ + new ContractRequest + { + Id = 201, + Counterparty = Counterparties[0], // Иванов + RealEstate = RealEstates[0], // Квартира на Ленина, 25 + ContractRequestType = ContractRequestType.Sale, + Amount = 12500000, + CreatedDate = new DateTime(2025, 1, 15), + Status = "Closed" + }, + new ContractRequest + { + Id = 202, + Counterparty = Counterparties[1], // Петрова + RealEstate = RealEstates[1], // Квартира на Ленина, 42 + ContractRequestType = ContractRequestType.Sale, + Amount = 9500000, + CreatedDate = new DateTime(2025, 1, 20), + Status = "Active" + }, + new ContractRequest + { + Id = 203, + Counterparty = Counterparties[2], // Сидоров + RealEstate = RealEstates[2], // Дом в Петрово + ContractRequestType = ContractRequestType.Purchase, + Amount = 18500000, + CreatedDate = new DateTime(2025, 2, 5), + Status = "Active" + }, + new ContractRequest + { + Id = 204, + Counterparty = Counterparties[0], // Иванов (снова) + RealEstate = RealEstates[3], // Участок + ContractRequestType = ContractRequestType.Purchase, + Amount = 3500000, + CreatedDate = new DateTime(2025, 2, 10), + Status = "Cancelled" + }, + new ContractRequest + { + Id = 205, + Counterparty = Counterparties[3], // Козлова + RealEstate = RealEstates[4], // Коммерческое на Тверской + ContractRequestType = ContractRequestType.Purchase, + Amount = 45000000, + CreatedDate = new DateTime(2025, 3, 1), + Status = "Active" + }, + new ContractRequest + { + Id = 206, + Counterparty = Counterparties[4], // Смирнов + RealEstate = RealEstates[5], // Гараж + ContractRequestType = ContractRequestType.Purchase, + Amount = 1200000, + CreatedDate = new DateTime(2025, 3, 15), + Status = "Active" + }, + new ContractRequest + { + Id = 207, + Counterparty = Counterparties[5], // Михайлова + RealEstate = RealEstates[6], // Квартира на Невском, 12 + ContractRequestType = ContractRequestType.Sale, + Amount = 16500000, + CreatedDate = new DateTime(2025, 4, 2), + Status = "Active" + }, + new ContractRequest + { + Id = 208, + Counterparty = Counterparties[2], // Сидоров (снова) + RealEstate = RealEstates[7], // Квартира на Невском, 15 + ContractRequestType = ContractRequestType.Purchase, + Amount = 11000000, + CreatedDate = new DateTime(2025, 4, 18), + Status = "Active" + }, + new ContractRequest + { + Id = 209, + Counterparty = Counterparties[6], // Федоров + RealEstate = RealEstates[8], // Коммерческое на Рубинштейна + ContractRequestType = ContractRequestType.Sale, + Amount = 28000000, + CreatedDate = new DateTime(2025, 5, 5), + Status = "Active" + }, + new ContractRequest + { + Id = 210, + Counterparty = Counterparties[7], // Васильева + RealEstate = RealEstates[9], // Дом в Романовке + ContractRequestType = ContractRequestType.Purchase, + Amount = 9500000, + CreatedDate = new DateTime(2025, 5, 20), + Status = "Active" + }, + new ContractRequest + { + Id = 211, + Counterparty = Counterparties[8], // Николаев + RealEstate = RealEstates[0], // Квартира на Ленина, 25 + ContractRequestType = ContractRequestType.Purchase, + Amount = 12500000, + CreatedDate = new DateTime(2025, 6, 1), + Status = "Active" + }, + new ContractRequest + { + Id = 212, + Counterparty = Counterparties[1], // Петрова (снова) + RealEstate = RealEstates[2], // Дом в Петрово + ContractRequestType = ContractRequestType.Purchase, + Amount = 18000000, + CreatedDate = new DateTime(2025, 6, 15), + Status = "Active" + }, + new ContractRequest + { + Id = 213, + Counterparty = Counterparties[9], // Александрова + RealEstate = RealEstates[1], // Квартира на Ленина, 42 + ContractRequestType = ContractRequestType.Purchase, + Amount = 9200000, + CreatedDate = new DateTime(2025, 7, 1), + Status = "Active" + } + ] + ); + + // Устанавливаем навигационные свойства для каждого контрагента + foreach (var counterparty in Counterparties) + { + counterparty.Requests = ContractRequests.Where(r => r.Counterparty.Id == counterparty.Id).ToList(); + } + + // Устанавливаем навигационные свойства для каждого объекта недвижимости + foreach (var realEstate in RealEstates) + { + realEstate.Requests = ContractRequests.Where(r => r.RealEstate.Id == realEstate.Id).ToList(); + } + } +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs b/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs new file mode 100644 index 000000000..85b55e5b9 --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Заявка (контракт) +/// +public class ContractRequest +{ + /// + /// Уникальный идентификатор заявки + /// + [Key] + public required int Id { get; set; } + + /// + /// Контрагент, связанный с заявкой + /// + public required Counterparty Counterparty { get; set; } + + /// + /// Объект недвижимости, связанный с заявкой + /// + public required RealEstate RealEstate { get; set; } + + /// + /// Тип заявки (покупка/продажа) + /// + public required ContractRequestType ContractRequestType { get; set; } + + /// + /// Денежная сумма (в рублях) + /// + [Range(0, 1_000_000_000, ErrorMessage = "Сумма должна быть в диапазоне от 0 до 1 млрд рублей")] + public required decimal Amount { get; set; } + + /// + /// Дата создания заявки + /// + [DataType(DataType.Date)] + public required DateTime CreatedDate { get; set; } + + /// + /// Статус заявки + /// + [StringLength(50, ErrorMessage = "Статус не должен превышать 50 символов")] + public required string Status { get; set; } +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/ContractRequestType.cs b/RealEstateAgency/Agency.Domain/Model/ContractRequestType.cs new file mode 100644 index 000000000..6065e8b43 --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/ContractRequestType.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Тип заявки +/// +public enum ContractRequestType +{ + [Display(Name = "Покупка")] + Purchase, + + [Display(Name = "Продажа")] + Sale +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/Counterparty.cs b/RealEstateAgency/Agency.Domain/Model/Counterparty.cs new file mode 100644 index 000000000..17ef17cca --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/Counterparty.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Контрагент (клиент агентства) +/// +public class Counterparty +{ + /// + /// Уникальный идентификатор контрагента + /// + [Key] + public required int Id { get; set; } + + /// + /// ФИО контрагента + /// + [StringLength(150, ErrorMessage = "ФИО не должно превышать 150 символов")] + public required string FullName { get; set; } + + /// + /// Номер паспорта + /// + [StringLength(20, ErrorMessage = "Номер паспорта не должен превышать 20 символов")] + public required string PassportNumber { get; set; } + + /// + /// Контактный телефон + /// + [StringLength(20, ErrorMessage = "Номер телефона не должен превышать 20 символов")] + [Phone(ErrorMessage = "Неверный формат номера телефона")] + public required string PhoneNumber { get; set; } + + /// + /// Список заявок/контрактов, связанных с данным контрагентом + /// + public List? Requests { get; set; } = []; +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/RealEstate.cs b/RealEstateAgency/Agency.Domain/Model/RealEstate.cs new file mode 100644 index 000000000..5bcf90b7a --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/RealEstate.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Объект недвижимости +/// +public class RealEstate +{ + /// + /// Уникальный идентификатор объекта недвижимости + /// + [Key] + public required int Id { get; set; } + + /// + /// Тип объекта недвижимости + /// + public required RealEstateType Type { get; set; } + + /// + /// Назначение объекта недвижимости + /// + public required RealEstatePurpose Purpose { get; set; } + + /// + /// Кадастровый номер + /// + [StringLength(50, ErrorMessage = "Кадастровый номер не должен превышать 50 символов")] + public required string CadastralNumber { get; set; } + + /// + /// Адрес объекта + /// + [StringLength(200, ErrorMessage = "Адрес не должен превышать 200 символов")] + public required string Address { get; set; } + + /// + /// Этажность здания + /// + [Range(1, 200, ErrorMessage = "Этажность должна быть в диапазоне от 1 до 200")] + public int? TotalFloors { get; set; } + + /// + /// Общая площадь (в кв. метрах) + /// + [Range(1, 10000, ErrorMessage = "Общая площадь должна быть в диапазоне от 1 до 10 000 кв. м")] + public required double TotalArea { get; set; } + + /// + /// Количество комнат + /// + [Range(1, 50, ErrorMessage = "Количество комнат должно быть в диапазоне от 1 до 50")] + public int? NumberOfRooms { get; set; } + + /// + /// Высота потолков (в метрах) + /// + [Range(1.5, 10, ErrorMessage = "Высота потолков должна быть в диапазоне от 1.5 до 10 метров")] + public double? CeilingHeight { get; set; } + + /// + /// Этаж расположения + /// + [Range(1, 200, ErrorMessage = "Этаж расположения должен быть в диапазоне от 1 до 200")] + public int? Floor { get; set; } + + /// + /// Наличие обременений + /// + public required bool HasEncumbrances { get; set; } + + /// + /// Описание обременений + /// + [StringLength(500, ErrorMessage = "Описание обременений не должно превышать 500 символов")] + public string? EncumbrancesDescription { get; set; } + + /// + /// Список заявок/контрактов, связанных с данным объектом недвижимости + /// + public List? Requests { get; set; } = []; +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/RealEstatePurpose.cs b/RealEstateAgency/Agency.Domain/Model/RealEstatePurpose.cs new file mode 100644 index 000000000..1f2884bcc --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/RealEstatePurpose.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Назначение объекта недвижимости +/// +public enum RealEstatePurpose +{ + [Display(Name = "Жилое")] + Residential, + + [Display(Name = "Коммерческое")] + Commercial, + + [Display(Name = "Промышленное")] + Industrial, + + [Display(Name = "Сельскохозяйственное")] + Agricultural +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Domain/Model/RealEstateType.cs b/RealEstateAgency/Agency.Domain/Model/RealEstateType.cs new file mode 100644 index 000000000..4e6912aea --- /dev/null +++ b/RealEstateAgency/Agency.Domain/Model/RealEstateType.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Domain.Model; + +/// +/// Тип объекта недвижимости +/// +public enum RealEstateType +{ + [Display(Name = "Квартира")] + Apartment, + + [Display(Name = "Дом")] + House, + + [Display(Name = "Земельный участок")] + LandPlot, + + [Display(Name = "Коммерческая недвижимость")] + Commercial, + + [Display(Name = "Гараж")] + Garage +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Tests/Agency.Tests.csproj b/RealEstateAgency/Agency.Tests/Agency.Tests.csproj new file mode 100644 index 000000000..acca8178f --- /dev/null +++ b/RealEstateAgency/Agency.Tests/Agency.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + true + + + + + + + + + + + + + + + + + + diff --git a/RealEstateAgency/Agency.Tests/DomainTests.cs b/RealEstateAgency/Agency.Tests/DomainTests.cs new file mode 100644 index 000000000..0c6d0174c --- /dev/null +++ b/RealEstateAgency/Agency.Tests/DomainTests.cs @@ -0,0 +1,259 @@ +using Agency.Domain.Data; +using Agency.Domain.Model; + +namespace Agency.Tests; + +/// +/// Тесты для доменной области риэлторского агентства +/// +public class RealEstateTests(DataSeeder data) : IClassFixture +{ + /// + /// Проверяет, что корректно выводятся все продавцы, оставившие заявки на продажу за заданный период + /// + [Fact] + public void SellersWithRequestsInPeriod() + { + // Arrange + var from = new DateTime(2025, 1, 1); + var to = new DateTime(2025, 3, 31); + + // Актуальные продавцы с заявками на продажу в 1 квартале 2025: + // - Иванов (Id: 1) - заявка 201 (продажа, 15.01.2025) + // - Петрова (Id: 2) - заявка 202 (продажа, 20.01.2025) + // - Козлова (Id: 4) - заявка 205 (покупка, не продажа) - НЕ ДОЛЖНА ПОПАСТЬ + // - Смирнов (Id: 5) - заявка 206 (покупка, не продажа) - НЕ ДОЛЖЕН ПОПАСТЬ + // - Михайлова (Id: 6) - заявка 207 (продажа, 02.04.2025) - после периода + // - Федоров (Id: 7) - заявка 209 (продажа, 05.05.2025) - после периода + + var expectedSellerIds = new List { 1, 2 }; // Иванов и Петрова + + // Act + var sellers = data.ContractRequests + .Where(r => r.ContractRequestType == ContractRequestType.Sale + && r.CreatedDate >= from + && r.CreatedDate <= to) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + + // Assert + Assert.Equal(expectedSellerIds.Count, sellers.Count); + Assert.All(expectedSellerIds, id => Assert.Contains(sellers, s => s.Id == id)); + } + + /// + /// Проверяет, что корректно возвращаются топ 5 клиентов по количеству заявок на покупку + /// + [Fact] + public void Top5BuyersByRequestCount() + { + // Подсчет заявок на покупку по клиентам: + // Иванов (Id: 1) - 1 покупка (заявка 204) + // Петрова (Id: 2) - 2 покупки (заявки 212, и еще одна?) + // Сидоров (Id: 3) - 2 покупки (заявки 203, 208) + // Козлова (Id: 4) - 1 покупка (заявка 205) + // Смирнов (Id: 5) - 1 покупка (заявка 206) + // Васильева (Id: 8) - 1 покупка (заявка 210) + // Николаев (Id: 9) - 1 покупка (заявка 211) + // Александрова (Id: 10) - 1 покупка (заявка 213) + + var buyersByCount = data.ContractRequests + .Where(r => r.ContractRequestType == ContractRequestType.Purchase) + .GroupBy(r => r.Counterparty) + .Select(g => new { Counterparty = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .ToList(); + + // Топ-5 Id клиентов по количеству покупок + var expectedBuyerIds = buyersByCount + .Take(5) + .Select(x => x.Counterparty.Id) + .ToList(); + + // Act + var topBuyers = buyersByCount + .Take(5) + .Select(x => x.Counterparty) + .ToList(); + + // Assert + Assert.Equal(5, topBuyers.Count); + Assert.Equal(expectedBuyerIds, topBuyers.Select(b => b.Id).ToList()); + } + + /// + /// Проверяет, что корректно возвращаются топ 5 клиентов по количеству заявок на продажу + /// + [Fact] + public void Top5SellersByRequestCount() + { + // Подсчет заявок на продажу по клиентам: + // Иванов (Id: 1) - 1 продажа (заявка 201) + // Петрова (Id: 2) - 1 продажа (заявка 202) + // Михайлова (Id: 6) - 1 продажа (заявка 207) + // Федоров (Id: 7) - 1 продажа (заявка 209) + + var sellersByCount = data.ContractRequests + .Where(r => r.ContractRequestType == ContractRequestType.Sale) + .GroupBy(r => r.Counterparty) + .Select(g => new { Counterparty = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .ToList(); + + // Топ Id клиентов по количеству продаж + var expectedSellerIds = sellersByCount + .Select(x => x.Counterparty.Id) + .ToList(); + + // Act + var topSellers = sellersByCount + .Take(5) + .Select(x => x.Counterparty) + .ToList(); + + // Assert + Assert.True(topSellers.Count <= 5); + Assert.Equal(expectedSellerIds, topSellers.Select(s => s.Id).ToList()); + } + + /// + /// Проверяет, что корректно выводится информация о количестве заявок по каждому типу недвижимости + /// + [Fact] + public void RequestCountByPropertyType() + { + // Подсчет заявок по типам недвижимости: + var expectedCounts = new Dictionary + { + { RealEstateType.Apartment, 6 }, // ID 101,102,107,108 - заявки: 201,202,207,208,211,213 (6 заявок) + { RealEstateType.House, 3 }, // ID 103,110 - заявки: 203,210,212 (3 заявки) + { RealEstateType.LandPlot, 1 }, // ID 104 - заявка: 204 (1 заявка) + { RealEstateType.Commercial, 2 }, // ID 105,109 - заявки: 205,209 (2 заявки) + { RealEstateType.Garage, 1 } // ID 106 - заявка: 206 (1 заявка) + }; + + // Act + var requestsByType = data.ContractRequests + .GroupBy(r => r.RealEstate.Type) + .Select(g => new { PropertyType = g.Key, Count = g.Count() }) + .ToDictionary(x => x.PropertyType, x => x.Count); + + // Assert + Assert.Equal(expectedCounts.Count, requestsByType.Count); + + foreach (var type in expectedCounts.Keys) + { + Assert.True(requestsByType.ContainsKey(type), $"Отсутствует тип {type}"); + Assert.Equal(expectedCounts[type], requestsByType[type]); + } + } + + /// + /// Проверяет, что корректно выводится информация о клиентах, открывших заявки с минимальной стоимостью + /// + [Fact] + public void ClientsWithMinimalRequestAmount() + { + // Минимальная сумма заявки - 1 200 000 (гараж, заявка 206, клиент Смирнов Id: 5) + var minAmount = data.ContractRequests.Min(r => r.Amount); + var expectedAmount = 1200000m; + var expectedClientIds = new List { 5 }; // Смирнов + + // Act + var clients = data.ContractRequests + .Where(r => r.Amount == minAmount) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + + // Assert + Assert.Equal(expectedAmount, minAmount); + Assert.Equal(expectedClientIds.Count, clients.Count); + Assert.All(expectedClientIds, id => Assert.Contains(clients, c => c.Id == id)); + } + + /// + /// Проверяет, что корректно выводятся сведения о всех клиентах, ищущих недвижимость заданного типа, упорядоченные по ФИО + /// + [Fact] + public void ClientsSearchingForSpecificPropertyType() + { + // Arrange + var propertyType = RealEstateType.Apartment; + + // Клиенты, ищущие квартиры (покупка): + // - Иванов (Id: 1) - заявка 204 (участок, не квартира) - НЕ ДОЛЖЕН ПОПАСТЬ + // - Сидоров (Id: 3) - заявка 208 (квартира) + // - Петрова (Id: 2) - заявка 212 (дом, не квартира) - НЕ ДОЛЖНА ПОПАСТЬ + // - Николаев (Id: 9) - заявка 211 (квартира) + // - Александрова (Id: 10) - заявка 213 (квартира) + // - Васильева (Id: 8) - заявка 210 (дом, не квартира) - НЕ ДОЛЖНА ПОПАСТЬ + + var expectedClientIds = new List { 3, 9, 10 }; // Сидоров, Николаев, Александрова + + // Act + var clients = data.ContractRequests + .Where(r => r.ContractRequestType == ContractRequestType.Purchase + && r.RealEstate.Type == propertyType) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + + // Assert + Assert.Equal(expectedClientIds.Count, clients.Count); + + // Проверяем сортировку по ФИО + var sortedNames = clients.Select(c => c.FullName).OrderBy(n => n).ToList(); + var actualNames = clients.Select(c => c.FullName).ToList(); + Assert.Equal(sortedNames, actualNames); + + // Проверяем, что все ожидаемые клиенты присутствуют + Assert.All(expectedClientIds, id => Assert.Contains(clients, c => c.Id == id)); + } + + /// + /// Проверяет, что корректно возвращаются клиенты, ищущие дома, упорядоченные по ФИО + /// + [Fact] + public void ClientsSearchingForHousesOrderedByFullName() + { + // Arrange + var propertyType = RealEstateType.House; + + // Клиенты, ищущие дома (покупка): + // - Петрова (Id: 2) - заявка 212 (дом) + // - Сидоров (Id: 3) - заявка 203 (дом) + // - Васильева (Id: 8) - заявка 210 (дом) + + var expectedClientIds = new List { 2, 3, 8 }; // Петрова, Сидоров, Васильева + var expectedSortedNames = new List + { + "Васильева Татьяна Павловна", + "Петрова Анна Сергеевна", + "Сидоров Петр Алексеевич" + }; + + // Act + var clients = data.ContractRequests + .Where(r => r.ContractRequestType == ContractRequestType.Purchase + && r.RealEstate.Type == propertyType) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + + // Assert + Assert.Equal(expectedClientIds.Count, clients.Count); + + // Проверяем сортировку по ФИО + var actualNames = clients.Select(c => c.FullName).ToList(); + Assert.Equal(expectedSortedNames, actualNames); + + // Проверяем, что все ожидаемые клиенты присутствуют + Assert.All(expectedClientIds, id => Assert.Contains(clients, c => c.Id == id)); + } +} \ No newline at end of file diff --git a/RealEstateAgency/RealEstateAgency.sln b/RealEstateAgency/RealEstateAgency.sln new file mode 100644 index 000000000..e9aa9f693 --- /dev/null +++ b/RealEstateAgency/RealEstateAgency.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36616.10 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Domain", "Agency.Domain\Agency.Domain.csproj", "{4D9D2410-8028-4C70-B261-29992356D91C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Tests", "Agency.Tests\Agency.Tests.csproj", "{F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|Any CPU.Build.0 = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B0B699E6-16DB-42D7-83FB-638821B87C80} + EndGlobalSection +EndGlobal From b2c3c90f24db08f196930bf6cb43ddbb5df22bd9 Mon Sep 17 00:00:00 2001 From: Cure232 Date: Tue, 24 Feb 2026 13:02:11 +0400 Subject: [PATCH 2/5] lab2+3 --- .../Agency.Api.Host/Agency.Api.Host.csproj | 17 +++ .../Controllers/AnalyticsController.cs | 130 ++++++++++++++++++ .../Controllers/ContractRequestController.cs | 11 ++ .../Controllers/CounterpartyController.cs | 11 ++ .../Controllers/CrudControllerBase.cs | 111 +++++++++++++++ .../Controllers/RealEstateController.cs | 11 ++ RealEstateAgency/Agency.Api.Host/Program.cs | 83 +++++++++++ .../Properties/launchSettings.json | 32 +++++ .../Agency.AppHost/Agency.AppHost.csproj | 17 +++ RealEstateAgency/Agency.AppHost/AppHost.cs | 9 ++ .../Properties/launchSettings.json | 31 +++++ .../appsettings.Development.json | 9 ++ .../Agency.AppHost/appsettings.json | 10 ++ .../Agency.Application.Contracts.csproj | 11 ++ .../ContractRequestCreateUpdateDto.cs | 16 +++ .../ContractRequests/ContractRequestDto.cs | 17 +++ .../IContractRequestService.cs | 8 ++ .../CounterpartyCreateUpdateDto.cs | 12 ++ .../Counterparties/CounterpartyDto.cs | 13 ++ .../Counterparties/ICounterpartyService.cs | 8 ++ .../IAnalyticsService.cs | 45 ++++++ .../IApplicationService.cs | 16 +++ .../RealEstates/IRealEstateService.cs | 8 ++ .../RealEstates/RealEstateCreateUpdateDto.cs | 21 +++ .../RealEstates/RealEstateDto.cs | 22 +++ .../Agency.Application.csproj | 15 ++ .../Agency.Application/AgencyProfile.cs | 29 ++++ .../Services/AnalyticsService.cs | 96 +++++++++++++ .../Services/ContractRequestService.cs | 57 ++++++++ .../Services/CounterpartyService.cs | 45 ++++++ .../Services/RealEstateService.cs | 45 ++++++ .../Agency.Domain/Data/DataSeeder.cs | 28 +++- RealEstateAgency/Agency.Domain/IRepository.cs | 17 +++ .../Agency.Domain/Model/ContractRequest.cs | 12 +- .../Agency.Infrastructure.EfCore.csproj | 14 ++ .../AgencyDbContext.cs | 61 ++++++++ .../Repositories/ContractRequestRepository.cs | 65 +++++++++ .../Repositories/CounterpartyRepository.cs | 40 ++++++ .../Repositories/RealEstateRepository.cs | 40 ++++++ .../Agency.ServiceDefaults.csproj | 18 +++ .../Agency.ServiceDefaults/Extensions.cs | 90 ++++++++++++ RealEstateAgency/RealEstateAgency.sln | 38 ++++- 42 files changed, 1386 insertions(+), 3 deletions(-) create mode 100644 RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj create mode 100644 RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Program.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Properties/launchSettings.json create mode 100644 RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj create mode 100644 RealEstateAgency/Agency.AppHost/AppHost.cs create mode 100644 RealEstateAgency/Agency.AppHost/Properties/launchSettings.json create mode 100644 RealEstateAgency/Agency.AppHost/appsettings.Development.json create mode 100644 RealEstateAgency/Agency.AppHost/appsettings.json create mode 100644 RealEstateAgency/Agency.Application.Contracts/Agency.Application.Contracts.csproj create mode 100644 RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/ContractRequests/IContractRequestService.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/Counterparties/ICounterpartyService.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/IAnalyticsService.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/IApplicationService.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/RealEstates/IRealEstateService.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs create mode 100644 RealEstateAgency/Agency.Application/Agency.Application.csproj create mode 100644 RealEstateAgency/Agency.Application/AgencyProfile.cs create mode 100644 RealEstateAgency/Agency.Application/Services/AnalyticsService.cs create mode 100644 RealEstateAgency/Agency.Application/Services/ContractRequestService.cs create mode 100644 RealEstateAgency/Agency.Application/Services/CounterpartyService.cs create mode 100644 RealEstateAgency/Agency.Application/Services/RealEstateService.cs create mode 100644 RealEstateAgency/Agency.Domain/IRepository.cs create mode 100644 RealEstateAgency/Agency.Infrastructure.EfCore/Agency.Infrastructure.EfCore.csproj create mode 100644 RealEstateAgency/Agency.Infrastructure.EfCore/AgencyDbContext.cs create mode 100644 RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/ContractRequestRepository.cs create mode 100644 RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/CounterpartyRepository.cs create mode 100644 RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/RealEstateRepository.cs create mode 100644 RealEstateAgency/Agency.ServiceDefaults/Agency.ServiceDefaults.csproj create mode 100644 RealEstateAgency/Agency.ServiceDefaults/Extensions.cs diff --git a/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj b/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj new file mode 100644 index 000000000..1d34962c7 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj @@ -0,0 +1,17 @@ + + + net8.0 + true + enable + enable + + + + + + + + + + + diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..6ad409960 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs @@ -0,0 +1,130 @@ +using Agency.Application.Contracts; +using Agency.Application.Contracts.Counterparties; +using Agency.Domain.Model; +using Microsoft.AspNetCore.Mvc; + +namespace Agency.Api.Host.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AnalyticsController(IAnalyticsService service, ILogger logger) : ControllerBase +{ + [HttpGet("sellers-with-requests-in-period")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetSellersWithRequestsInPeriod([FromQuery] DateTime from, [FromQuery] DateTime to) + { + try + { + var result = await service.GetSellersWithRequestsInPeriod(from, to); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetSellersWithRequestsInPeriod"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("top5-buyers-by-request-count")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetTop5BuyersByRequestCount() + { + try + { + var result = await service.GetTop5BuyersByRequestCount(); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetTop5BuyersByRequestCount"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("top5-sellers-by-request-count")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetTop5SellersByRequestCount() + { + try + { + var result = await service.GetTop5SellersByRequestCount(); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetTop5SellersByRequestCount"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("request-count-by-property-type")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetRequestCountByPropertyType() + { + try + { + var result = await service.GetRequestCountByPropertyType(); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetRequestCountByPropertyType"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("clients-with-minimal-request-amount")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetClientsWithMinimalRequestAmount() + { + try + { + var result = await service.GetClientsWithMinimalRequestAmount(); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetClientsWithMinimalRequestAmount"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("clients-searching-for-property-type")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetClientsSearchingForPropertyType([FromQuery] RealEstateType propertyType) + { + try + { + var result = await service.GetClientsSearchingForPropertyType(propertyType); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetClientsSearchingForPropertyType"); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("clients-searching-for-houses-ordered-by-fullname")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetClientsSearchingForHousesOrderedByFullName() + { + try + { + var result = await service.GetClientsSearchingForHousesOrderedByFullName(); + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in GetClientsSearchingForHousesOrderedByFullName"); + return StatusCode(500, ex.Message); + } + } +} diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs new file mode 100644 index 000000000..145d083dc --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs @@ -0,0 +1,11 @@ +using Agency.Application.Contracts.ContractRequests; +using Microsoft.AspNetCore.Mvc; + +namespace Agency.Api.Host.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class ContractRequestController(IContractRequestService service, ILogger logger) + : CrudControllerBase(service, logger) +{ +} diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs new file mode 100644 index 000000000..4789343fa --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs @@ -0,0 +1,11 @@ +using Agency.Application.Contracts.Counterparties; +using Microsoft.AspNetCore.Mvc; + +namespace Agency.Api.Host.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class CounterpartyController(ICounterpartyService service, ILogger logger) + : CrudControllerBase(service, logger) +{ +} diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs new file mode 100644 index 000000000..bad8b6378 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs @@ -0,0 +1,111 @@ +using Agency.Application.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace Agency.Api.Host.Controllers; + +[Route("api/[controller]")] +[ApiController] +public abstract class CrudControllerBase( + IApplicationService appService, + ILogger> logger) : ControllerBase + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(500)] + public async Task> Create(TCreateUpdateDto newDto) + { + logger.LogInformation("{Method} of {Controller} called with {@Dto}", nameof(Create), GetType().Name, newDto); + try + { + var res = await appService.Create(newDto); + return CreatedAtAction(nameof(Create), res); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in {Method} of {Controller}", nameof(Create), GetType().Name); + return StatusCode(500, $"{ex.Message}\n{ex.InnerException?.Message}"); + } + } + + [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> Edit(TKey id, TCreateUpdateDto newDto) + { + try + { + var res = await appService.Update(newDto, id); + return Ok(res); + } + catch (KeyNotFoundException) + { + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in {Method} of {Controller}", nameof(Edit), GetType().Name); + return StatusCode(500, ex.Message); + } + } + + [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public async Task Delete(TKey id) + { + try + { + var res = await appService.Delete(id); + return res ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in {Method} of {Controller}", nameof(Delete), GetType().Name); + return StatusCode(500, ex.Message); + } + } + + [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetAll() + { + try + { + var res = await appService.GetAll(); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in {Method} of {Controller}", nameof(GetAll), GetType().Name); + return StatusCode(500, ex.Message); + } + } + + [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> Get(TKey id) + { + try + { + var res = await appService.Get(id); + return res == null ? NotFound() : Ok(res); + } + catch (KeyNotFoundException) + { + return NotFound(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in {Method} of {Controller}", nameof(Get), GetType().Name); + return StatusCode(500, ex.Message); + } + } +} diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs new file mode 100644 index 000000000..2c946102e --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs @@ -0,0 +1,11 @@ +using Agency.Application.Contracts.RealEstates; +using Microsoft.AspNetCore.Mvc; + +namespace Agency.Api.Host.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class RealEstateController(IRealEstateService service, ILogger logger) + : CrudControllerBase(service, logger) +{ +} diff --git a/RealEstateAgency/Agency.Api.Host/Program.cs b/RealEstateAgency/Agency.Api.Host/Program.cs new file mode 100644 index 000000000..a30282be3 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Program.cs @@ -0,0 +1,83 @@ +using Agency.Application; +using Agency.Application.Contracts; +using Agency.Application.Contracts.ContractRequests; +using Agency.Application.Contracts.Counterparties; +using Agency.Application.Contracts.RealEstates; +using Agency.Application.Services; +using Agency.Domain; +using Agency.Domain.Data; +using Agency.Domain.Model; +using Agency.Infrastructure.EfCore; +using Agency.Infrastructure.EfCore.Repositories; +using Agency.ServiceDefaults; +using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddSingleton(); +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new AgencyProfile()); +}); + +builder.Services.AddTransient, CounterpartyRepository>(); +builder.Services.AddTransient, RealEstateRepository>(); +builder.Services.AddTransient, ContractRequestRepository>(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name?.StartsWith("Agency") == true)) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } +}); + +builder.AddMongoDBClient("agencyClient"); +builder.Services.AddDbContext((services, o) => +{ + var db = services.GetRequiredService(); + o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); +}); + +var app = builder.Build(); +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + var dataSeed = scope.ServiceProvider.GetRequiredService(); + + if (!await dbContext.Counterparties.AnyAsync()) + { + foreach (var c in dataSeed.Counterparties) + await dbContext.Counterparties.AddAsync(c); + foreach (var r in dataSeed.RealEstates) + await dbContext.RealEstates.AddAsync(r); + foreach (var req in dataSeed.ContractRequests) + await dbContext.ContractRequests.AddAsync(req); + await dbContext.SaveChangesAsync(); + } +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); diff --git a/RealEstateAgency/Agency.Api.Host/Properties/launchSettings.json b/RealEstateAgency/Agency.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..3c49ecf15 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17002;http://localhost:15077", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21295", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22035" + }, + "launchUrl": "swagger" + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15077", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19199", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20119" + }, + "launchUrl": "swagger" + + } + } +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj b/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj new file mode 100644 index 000000000..f3f0145c3 --- /dev/null +++ b/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + realestate-agency-apphost + + + + + + + + + diff --git a/RealEstateAgency/Agency.AppHost/AppHost.cs b/RealEstateAgency/Agency.AppHost/AppHost.cs new file mode 100644 index 000000000..647448806 --- /dev/null +++ b/RealEstateAgency/Agency.AppHost/AppHost.cs @@ -0,0 +1,9 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var db = builder.AddMongoDB("mongo").AddDatabase("db"); + +builder.AddProject("agency-api-host") + .WithReference(db, "agencyClient") + .WaitFor(db); + +builder.Build().Run(); diff --git a/RealEstateAgency/Agency.AppHost/Properties/launchSettings.json b/RealEstateAgency/Agency.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..ba0dc317d --- /dev/null +++ b/RealEstateAgency/Agency.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:17012;http://localhost:15087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21305", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22045" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15087", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19209", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20129" + } + } + } +} + diff --git a/RealEstateAgency/Agency.AppHost/appsettings.Development.json b/RealEstateAgency/Agency.AppHost/appsettings.Development.json new file mode 100644 index 000000000..21b22dbb4 --- /dev/null +++ b/RealEstateAgency/Agency.AppHost/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} + diff --git a/RealEstateAgency/Agency.AppHost/appsettings.json b/RealEstateAgency/Agency.AppHost/appsettings.json new file mode 100644 index 000000000..abdf02560 --- /dev/null +++ b/RealEstateAgency/Agency.AppHost/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} + diff --git a/RealEstateAgency/Agency.Application.Contracts/Agency.Application.Contracts.csproj b/RealEstateAgency/Agency.Application.Contracts/Agency.Application.Contracts.csproj new file mode 100644 index 000000000..fdf4a1940 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/Agency.Application.Contracts.csproj @@ -0,0 +1,11 @@ + + + net8.0 + true + enable + enable + + + + + diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs new file mode 100644 index 000000000..e078ec6ad --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs @@ -0,0 +1,16 @@ +using Agency.Domain.Model; +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.ContractRequests; + +/// +/// DTO для создания/обновления заявки +/// +public record ContractRequestCreateUpdateDto( + int CounterpartyId, + int RealEstateId, + ContractRequestType ContractRequestType, + [Range(0, 1_000_000_000)] decimal Amount, + DateTime CreatedDate, + [Required][StringLength(50)] string Status +); diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs new file mode 100644 index 000000000..95921d93d --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs @@ -0,0 +1,17 @@ +using Agency.Domain.Model; +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.ContractRequests; + +/// +/// DTO заявки +/// +public record ContractRequestDto( + int Id, + int CounterpartyId, + int RealEstateId, + ContractRequestType ContractRequestType, + decimal Amount, + DateTime CreatedDate, + [Required][StringLength(50)] string Status +); diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/IContractRequestService.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/IContractRequestService.cs new file mode 100644 index 000000000..9728daf68 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/IContractRequestService.cs @@ -0,0 +1,8 @@ +namespace Agency.Application.Contracts.ContractRequests; + +/// +/// Сервис для работы с заявками +/// +public interface IContractRequestService : IApplicationService +{ +} diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs new file mode 100644 index 000000000..f3d6ec936 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.Counterparties; + +/// +/// DTO для создания/обновления контрагента +/// +public record CounterpartyCreateUpdateDto( + [Required][StringLength(150)] string FullName, + [Required][StringLength(20)] string PassportNumber, + [Required][StringLength(20)][Phone] string PhoneNumber +); diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs new file mode 100644 index 000000000..a783ed103 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.Counterparties; + +/// +/// DTO контрагента +/// +public record CounterpartyDto( + int Id, + [Required][StringLength(150)] string FullName, + [Required][StringLength(20)] string PassportNumber, + [Required][StringLength(20)] string PhoneNumber +); diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/ICounterpartyService.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/ICounterpartyService.cs new file mode 100644 index 000000000..7b3356f6b --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/ICounterpartyService.cs @@ -0,0 +1,8 @@ +namespace Agency.Application.Contracts.Counterparties; + +/// +/// Сервис для работы с контрагентами +/// +public interface ICounterpartyService : IApplicationService +{ +} diff --git a/RealEstateAgency/Agency.Application.Contracts/IAnalyticsService.cs b/RealEstateAgency/Agency.Application.Contracts/IAnalyticsService.cs new file mode 100644 index 000000000..19dbbc7a6 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/IAnalyticsService.cs @@ -0,0 +1,45 @@ +using Agency.Application.Contracts.Counterparties; +using Agency.Domain.Model; + +namespace Agency.Application.Contracts; + +/// +/// Сервис аналитики риэлторского агентства +/// +public interface IAnalyticsService +{ + /// + /// Продавцы, оставившие заявки на продажу за заданный период + /// + Task> GetSellersWithRequestsInPeriod(DateTime from, DateTime to); + + /// + /// Топ 5 покупателей по количеству заявок на покупку + /// + Task> GetTop5BuyersByRequestCount(); + + /// + /// Топ 5 продавцов по количеству заявок на продажу + /// + Task> GetTop5SellersByRequestCount(); + + /// + /// Количество заявок по каждому типу недвижимости + /// + Task> GetRequestCountByPropertyType(); + + /// + /// Клиенты с минимальной суммой заявки + /// + Task> GetClientsWithMinimalRequestAmount(); + + /// + /// Клиенты, ищущие недвижимость заданного типа (покупка), упорядоченные по ФИО + /// + Task> GetClientsSearchingForPropertyType(RealEstateType propertyType); + + /// + /// Клиенты, ищущие дома (покупка), упорядоченные по ФИО + /// + Task> GetClientsSearchingForHousesOrderedByFullName(); +} diff --git a/RealEstateAgency/Agency.Application.Contracts/IApplicationService.cs b/RealEstateAgency/Agency.Application.Contracts/IApplicationService.cs new file mode 100644 index 000000000..021111af2 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/IApplicationService.cs @@ -0,0 +1,16 @@ +namespace Agency.Application.Contracts; + +/// +/// Интерфейс службы приложения для CRUD операций +/// +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + Task Create(TCreateUpdateDto dto); + Task Get(TKey dtoId); + Task> GetAll(); + Task Update(TCreateUpdateDto dto, TKey dtoId); + Task Delete(TKey dtoId); +} diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/IRealEstateService.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/IRealEstateService.cs new file mode 100644 index 000000000..233a32f44 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/IRealEstateService.cs @@ -0,0 +1,8 @@ +namespace Agency.Application.Contracts.RealEstates; + +/// +/// Сервис для работы с объектами недвижимости +/// +public interface IRealEstateService : IApplicationService +{ +} diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs new file mode 100644 index 000000000..3cc0caef6 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs @@ -0,0 +1,21 @@ +using Agency.Domain.Model; +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.RealEstates; + +/// +/// DTO для создания/обновления объекта недвижимости +/// +public record RealEstateCreateUpdateDto( + RealEstateType Type, + RealEstatePurpose Purpose, + [Required][StringLength(50)] string CadastralNumber, + [Required][StringLength(200)] string Address, + int? TotalFloors, + [Range(1, 10000)] double TotalArea, + int? NumberOfRooms, + double? CeilingHeight, + int? Floor, + bool HasEncumbrances, + string? EncumbrancesDescription +); diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs new file mode 100644 index 000000000..ecc74e0e7 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs @@ -0,0 +1,22 @@ +using Agency.Domain.Model; +using System.ComponentModel.DataAnnotations; + +namespace Agency.Application.Contracts.RealEstates; + +/// +/// DTO объекта недвижимости +/// +public record RealEstateDto( + int Id, + RealEstateType Type, + RealEstatePurpose Purpose, + [Required][StringLength(50)] string CadastralNumber, + [Required][StringLength(200)] string Address, + int? TotalFloors, + double TotalArea, + int? NumberOfRooms, + double? CeilingHeight, + int? Floor, + bool HasEncumbrances, + string? EncumbrancesDescription +); diff --git a/RealEstateAgency/Agency.Application/Agency.Application.csproj b/RealEstateAgency/Agency.Application/Agency.Application.csproj new file mode 100644 index 000000000..8007986ae --- /dev/null +++ b/RealEstateAgency/Agency.Application/Agency.Application.csproj @@ -0,0 +1,15 @@ + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/RealEstateAgency/Agency.Application/AgencyProfile.cs b/RealEstateAgency/Agency.Application/AgencyProfile.cs new file mode 100644 index 000000000..a4f47374b --- /dev/null +++ b/RealEstateAgency/Agency.Application/AgencyProfile.cs @@ -0,0 +1,29 @@ +using Agency.Application.Contracts.ContractRequests; +using Agency.Application.Contracts.Counterparties; +using Agency.Application.Contracts.RealEstates; +using Agency.Domain.Model; +using AutoMapper; + +namespace Agency.Application; + +/// +/// Профиль маппинга AutoMapper для риэлторского агентства +/// +public class AgencyProfile : Profile +{ + public AgencyProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(d => d.CounterpartyId, o => o.MapFrom(s => s.CounterpartyId)) + .ForMember(d => d.RealEstateId, o => o.MapFrom(s => s.RealEstateId)); + CreateMap() + .ForMember(d => d.Counterparty, o => o.Ignore()) + .ForMember(d => d.RealEstate, o => o.Ignore()); + } +} diff --git a/RealEstateAgency/Agency.Application/Services/AnalyticsService.cs b/RealEstateAgency/Agency.Application/Services/AnalyticsService.cs new file mode 100644 index 000000000..69c73e02d --- /dev/null +++ b/RealEstateAgency/Agency.Application/Services/AnalyticsService.cs @@ -0,0 +1,96 @@ +using Agency.Application.Contracts; +using Agency.Application.Contracts.Counterparties; +using Agency.Domain; +using Agency.Domain.Model; +using AutoMapper; + +namespace Agency.Application.Services; + +/// +/// Сервис аналитики риэлторского агентства +/// +public class AnalyticsService( + IRepository requestRepository, + IMapper mapper) : IAnalyticsService +{ + public async Task> GetSellersWithRequestsInPeriod(DateTime from, DateTime to) + { + var requests = await requestRepository.ReadAll(); + var sellers = requests + .Where(r => r.ContractRequestType == ContractRequestType.Sale + && r.CreatedDate >= from + && r.CreatedDate <= to) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + return mapper.Map>(sellers); + } + + public async Task> GetTop5BuyersByRequestCount() + { + var requests = await requestRepository.ReadAll(); + var buyersByCount = requests + .Where(r => r.ContractRequestType == ContractRequestType.Purchase) + .GroupBy(r => r.Counterparty) + .Select(g => new { Counterparty = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .Select(x => x.Counterparty) + .ToList(); + return mapper.Map>(buyersByCount); + } + + public async Task> GetTop5SellersByRequestCount() + { + var requests = await requestRepository.ReadAll(); + var sellersByCount = requests + .Where(r => r.ContractRequestType == ContractRequestType.Sale) + .GroupBy(r => r.Counterparty) + .Select(g => new { Counterparty = g.Key, Count = g.Count() }) + .OrderByDescending(x => x.Count) + .Take(5) + .Select(x => x.Counterparty) + .ToList(); + return mapper.Map>(sellersByCount); + } + + public async Task> GetRequestCountByPropertyType() + { + var requests = await requestRepository.ReadAll(); + return requests + .GroupBy(r => r.RealEstate.Type) + .Select(g => new { PropertyType = g.Key, Count = g.Count() }) + .ToDictionary(x => x.PropertyType, x => x.Count); + } + + public async Task> GetClientsWithMinimalRequestAmount() + { + var requests = await requestRepository.ReadAll(); + var minAmount = requests.Min(r => r.Amount); + var clients = requests + .Where(r => r.Amount == minAmount) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + return mapper.Map>(clients); + } + + public async Task> GetClientsSearchingForPropertyType(RealEstateType propertyType) + { + var requests = await requestRepository.ReadAll(); + var clients = requests + .Where(r => r.ContractRequestType == ContractRequestType.Purchase && r.RealEstate.Type == propertyType) + .Select(r => r.Counterparty) + .Distinct() + .OrderBy(c => c.FullName) + .ToList(); + return mapper.Map>(clients); + } + + public async Task> GetClientsSearchingForHousesOrderedByFullName() + { + return await GetClientsSearchingForPropertyType(RealEstateType.House); + } +} diff --git a/RealEstateAgency/Agency.Application/Services/ContractRequestService.cs b/RealEstateAgency/Agency.Application/Services/ContractRequestService.cs new file mode 100644 index 000000000..1863f3654 --- /dev/null +++ b/RealEstateAgency/Agency.Application/Services/ContractRequestService.cs @@ -0,0 +1,57 @@ +using Agency.Application.Contracts.ContractRequests; +using Agency.Domain; +using Agency.Domain.Model; +using AutoMapper; + +namespace Agency.Application.Services; + +/// +/// Сервис для управления заявками +/// +public class ContractRequestService( + IRepository requestRepository, + IRepository counterpartyRepository, + IRepository realEstateRepository, + IMapper mapper) : IContractRequestService +{ + public async Task Create(ContractRequestCreateUpdateDto dto) + { + var counterparty = await counterpartyRepository.Read(dto.CounterpartyId) ?? throw new KeyNotFoundException($"Контрагент с Id {dto.CounterpartyId} не найден"); + var realEstate = await realEstateRepository.Read(dto.RealEstateId) ?? throw new KeyNotFoundException($"Объект недвижимости с Id {dto.RealEstateId} не найден"); + + var entity = mapper.Map(dto); + entity.Counterparty = counterparty; + entity.RealEstate = realEstate; + var all = await requestRepository.ReadAll(); + entity.Id = all.Count > 0 ? all.Max(x => x.Id) + 1 : 1; + var result = await requestRepository.Create(entity); + return mapper.Map(result); + } + + public async Task Delete(int dtoId) => await requestRepository.Delete(dtoId); + + public async Task Get(int dtoId) + { + var entity = await requestRepository.Read(dtoId); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAll() + { + var all = await requestRepository.ReadAll(); + return mapper.Map>(all); + } + + public async Task Update(ContractRequestCreateUpdateDto dto, int dtoId) + { + var entity = await requestRepository.Read(dtoId) ?? throw new KeyNotFoundException($"Заявка с Id {dtoId} не найдена"); + var counterparty = await counterpartyRepository.Read(dto.CounterpartyId) ?? throw new KeyNotFoundException($"Контрагент с Id {dto.CounterpartyId} не найден"); + var realEstate = await realEstateRepository.Read(dto.RealEstateId) ?? throw new KeyNotFoundException($"Объект недвижимости с Id {dto.RealEstateId} не найден"); + + mapper.Map(dto, entity); + entity.Counterparty = counterparty; + entity.RealEstate = realEstate; + var result = await requestRepository.Update(entity); + return mapper.Map(result); + } +} diff --git a/RealEstateAgency/Agency.Application/Services/CounterpartyService.cs b/RealEstateAgency/Agency.Application/Services/CounterpartyService.cs new file mode 100644 index 000000000..d0fe96883 --- /dev/null +++ b/RealEstateAgency/Agency.Application/Services/CounterpartyService.cs @@ -0,0 +1,45 @@ +using Agency.Application.Contracts.Counterparties; +using Agency.Domain; +using Agency.Domain.Model; +using AutoMapper; + +namespace Agency.Application.Services; + +/// +/// Сервис для управления контрагентами +/// +public class CounterpartyService( + IRepository repository, + IMapper mapper) : ICounterpartyService +{ + public async Task Create(CounterpartyCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var all = await repository.ReadAll(); + entity.Id = all.Count > 0 ? all.Max(x => x.Id) + 1 : 1; + var result = await repository.Create(entity); + return mapper.Map(result); + } + + public async Task Delete(int dtoId) => await repository.Delete(dtoId); + + public async Task Get(int dtoId) + { + var entity = await repository.Read(dtoId); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAll() + { + var all = await repository.ReadAll(); + return mapper.Map>(all); + } + + public async Task Update(CounterpartyCreateUpdateDto dto, int dtoId) + { + var entity = await repository.Read(dtoId) ?? throw new KeyNotFoundException($"Контрагент с Id {dtoId} не найден"); + mapper.Map(dto, entity); + var result = await repository.Update(entity); + return mapper.Map(result); + } +} diff --git a/RealEstateAgency/Agency.Application/Services/RealEstateService.cs b/RealEstateAgency/Agency.Application/Services/RealEstateService.cs new file mode 100644 index 000000000..5877a85a6 --- /dev/null +++ b/RealEstateAgency/Agency.Application/Services/RealEstateService.cs @@ -0,0 +1,45 @@ +using Agency.Application.Contracts.RealEstates; +using Agency.Domain; +using Agency.Domain.Model; +using AutoMapper; + +namespace Agency.Application.Services; + +/// +/// Сервис для управления объектами недвижимости +/// +public class RealEstateService( + IRepository repository, + IMapper mapper) : IRealEstateService +{ + public async Task Create(RealEstateCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + var all = await repository.ReadAll(); + entity.Id = all.Count > 0 ? all.Max(x => x.Id) + 1 : 1; + var result = await repository.Create(entity); + return mapper.Map(result); + } + + public async Task Delete(int dtoId) => await repository.Delete(dtoId); + + public async Task Get(int dtoId) + { + var entity = await repository.Read(dtoId); + return entity == null ? null : mapper.Map(entity); + } + + public async Task> GetAll() + { + var all = await repository.ReadAll(); + return mapper.Map>(all); + } + + public async Task Update(RealEstateCreateUpdateDto dto, int dtoId) + { + var entity = await repository.Read(dtoId) ?? throw new KeyNotFoundException($"Объект недвижимости с Id {dtoId} не найден"); + mapper.Map(dto, entity); + var result = await repository.Update(entity); + return mapper.Map(result); + } +} diff --git a/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs b/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs index c20d26560..2de365d5d 100644 --- a/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs +++ b/RealEstateAgency/Agency.Domain/Data/DataSeeder.cs @@ -1,4 +1,4 @@ -using Agency.Domain.Model; +using Agency.Domain.Model; namespace Agency.Domain.Data; @@ -258,7 +258,9 @@ public DataSeeder() new ContractRequest { Id = 201, + CounterpartyId = 1, Counterparty = Counterparties[0], // Иванов + RealEstateId = 101, RealEstate = RealEstates[0], // Квартира на Ленина, 25 ContractRequestType = ContractRequestType.Sale, Amount = 12500000, @@ -268,7 +270,9 @@ public DataSeeder() new ContractRequest { Id = 202, + CounterpartyId = 2, Counterparty = Counterparties[1], // Петрова + RealEstateId = 102, RealEstate = RealEstates[1], // Квартира на Ленина, 42 ContractRequestType = ContractRequestType.Sale, Amount = 9500000, @@ -278,7 +282,9 @@ public DataSeeder() new ContractRequest { Id = 203, + CounterpartyId = 3, Counterparty = Counterparties[2], // Сидоров + RealEstateId = 103, RealEstate = RealEstates[2], // Дом в Петрово ContractRequestType = ContractRequestType.Purchase, Amount = 18500000, @@ -288,7 +294,9 @@ public DataSeeder() new ContractRequest { Id = 204, + CounterpartyId = 1, Counterparty = Counterparties[0], // Иванов (снова) + RealEstateId = 104, RealEstate = RealEstates[3], // Участок ContractRequestType = ContractRequestType.Purchase, Amount = 3500000, @@ -298,7 +306,9 @@ public DataSeeder() new ContractRequest { Id = 205, + CounterpartyId = 4, Counterparty = Counterparties[3], // Козлова + RealEstateId = 105, RealEstate = RealEstates[4], // Коммерческое на Тверской ContractRequestType = ContractRequestType.Purchase, Amount = 45000000, @@ -308,7 +318,9 @@ public DataSeeder() new ContractRequest { Id = 206, + CounterpartyId = 5, Counterparty = Counterparties[4], // Смирнов + RealEstateId = 106, RealEstate = RealEstates[5], // Гараж ContractRequestType = ContractRequestType.Purchase, Amount = 1200000, @@ -318,7 +330,9 @@ public DataSeeder() new ContractRequest { Id = 207, + CounterpartyId = 6, Counterparty = Counterparties[5], // Михайлова + RealEstateId = 107, RealEstate = RealEstates[6], // Квартира на Невском, 12 ContractRequestType = ContractRequestType.Sale, Amount = 16500000, @@ -328,7 +342,9 @@ public DataSeeder() new ContractRequest { Id = 208, + CounterpartyId = 3, Counterparty = Counterparties[2], // Сидоров (снова) + RealEstateId = 108, RealEstate = RealEstates[7], // Квартира на Невском, 15 ContractRequestType = ContractRequestType.Purchase, Amount = 11000000, @@ -338,7 +354,9 @@ public DataSeeder() new ContractRequest { Id = 209, + CounterpartyId = 7, Counterparty = Counterparties[6], // Федоров + RealEstateId = 109, RealEstate = RealEstates[8], // Коммерческое на Рубинштейна ContractRequestType = ContractRequestType.Sale, Amount = 28000000, @@ -348,7 +366,9 @@ public DataSeeder() new ContractRequest { Id = 210, + CounterpartyId = 8, Counterparty = Counterparties[7], // Васильева + RealEstateId = 110, RealEstate = RealEstates[9], // Дом в Романовке ContractRequestType = ContractRequestType.Purchase, Amount = 9500000, @@ -358,7 +378,9 @@ public DataSeeder() new ContractRequest { Id = 211, + CounterpartyId = 9, Counterparty = Counterparties[8], // Николаев + RealEstateId = 101, RealEstate = RealEstates[0], // Квартира на Ленина, 25 ContractRequestType = ContractRequestType.Purchase, Amount = 12500000, @@ -368,7 +390,9 @@ public DataSeeder() new ContractRequest { Id = 212, + CounterpartyId = 2, Counterparty = Counterparties[1], // Петрова (снова) + RealEstateId = 103, RealEstate = RealEstates[2], // Дом в Петрово ContractRequestType = ContractRequestType.Purchase, Amount = 18000000, @@ -378,7 +402,9 @@ public DataSeeder() new ContractRequest { Id = 213, + CounterpartyId = 10, Counterparty = Counterparties[9], // Александрова + RealEstateId = 102, RealEstate = RealEstates[1], // Квартира на Ленина, 42 ContractRequestType = ContractRequestType.Purchase, Amount = 9200000, diff --git a/RealEstateAgency/Agency.Domain/IRepository.cs b/RealEstateAgency/Agency.Domain/IRepository.cs new file mode 100644 index 000000000..0eefab0fb --- /dev/null +++ b/RealEstateAgency/Agency.Domain/IRepository.cs @@ -0,0 +1,17 @@ +namespace Agency.Domain; + +/// +/// Интерфейс репозитория для CRUD операций +/// +/// Тип сущности +/// Тип идентификатора сущности +public interface IRepository + where TEntity : class + where TKey : struct +{ + Task Create(TEntity entity); + Task Read(TKey entityId); + Task> ReadAll(); + Task Update(TEntity entity); + Task Delete(TKey entityId); +} diff --git a/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs b/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs index 85b55e5b9..7ecdc3476 100644 --- a/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs +++ b/RealEstateAgency/Agency.Domain/Model/ContractRequest.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Agency.Domain.Model; @@ -13,11 +13,21 @@ public class ContractRequest [Key] public required int Id { get; set; } + /// + /// Идентификатор контрагента + /// + public int CounterpartyId { get; set; } + /// /// Контрагент, связанный с заявкой /// public required Counterparty Counterparty { get; set; } + /// + /// Идентификатор объекта недвижимости + /// + public int RealEstateId { get; set; } + /// /// Объект недвижимости, связанный с заявкой /// diff --git a/RealEstateAgency/Agency.Infrastructure.EfCore/Agency.Infrastructure.EfCore.csproj b/RealEstateAgency/Agency.Infrastructure.EfCore/Agency.Infrastructure.EfCore.csproj new file mode 100644 index 000000000..4687c9c3b --- /dev/null +++ b/RealEstateAgency/Agency.Infrastructure.EfCore/Agency.Infrastructure.EfCore.csproj @@ -0,0 +1,14 @@ + + + net8.0 + enable + enable + + + + + + + + + diff --git a/RealEstateAgency/Agency.Infrastructure.EfCore/AgencyDbContext.cs b/RealEstateAgency/Agency.Infrastructure.EfCore/AgencyDbContext.cs new file mode 100644 index 000000000..7194180dd --- /dev/null +++ b/RealEstateAgency/Agency.Infrastructure.EfCore/AgencyDbContext.cs @@ -0,0 +1,61 @@ +using Agency.Domain.Model; +using Microsoft.EntityFrameworkCore; +using MongoDB.EntityFrameworkCore.Extensions; + +namespace Agency.Infrastructure.EfCore; + +/// +/// Контекст базы данных риэлторского агентства (MongoDB) +/// +public class AgencyDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Counterparties => Set(); + public DbSet RealEstates => Set(); + public DbSet ContractRequests => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + + modelBuilder.Entity(builder => + { + builder.ToCollection("counterparties"); + builder.HasKey(c => c.Id); + builder.Property(c => c.Id).HasElementName("_id"); + builder.Property(c => c.FullName).HasElementName("full_name"); + builder.Property(c => c.PassportNumber).HasElementName("passport_number"); + builder.Property(c => c.PhoneNumber).HasElementName("phone_number"); + }); + + modelBuilder.Entity(builder => + { + builder.ToCollection("real_estates"); + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).HasElementName("_id"); + builder.Property(r => r.Type).HasElementName("type"); + builder.Property(r => r.Purpose).HasElementName("purpose"); + builder.Property(r => r.CadastralNumber).HasElementName("cadastral_number"); + builder.Property(r => r.Address).HasElementName("address"); + builder.Property(r => r.TotalFloors).HasElementName("total_floors"); + builder.Property(r => r.TotalArea).HasElementName("total_area"); + builder.Property(r => r.NumberOfRooms).HasElementName("number_of_rooms"); + builder.Property(r => r.CeilingHeight).HasElementName("ceiling_height"); + builder.Property(r => r.Floor).HasElementName("floor"); + builder.Property(r => r.HasEncumbrances).HasElementName("has_encumbrances"); + builder.Property(r => r.EncumbrancesDescription).HasElementName("encumbrances_description"); + }); + + modelBuilder.Entity(builder => + { + builder.ToCollection("contract_requests"); + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).HasElementName("_id"); + builder.Property(r => r.CounterpartyId).HasElementName("counterparty_id"); + builder.Property(r => r.RealEstateId).HasElementName("real_estate_id"); + builder.Property(r => r.ContractRequestType).HasElementName("contract_request_type"); + builder.Property(r => r.Amount).HasElementName("amount"); + builder.Property(r => r.CreatedDate).HasElementName("created_date"); + builder.Property(r => r.Status).HasElementName("status"); + }); + } +} diff --git a/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/ContractRequestRepository.cs b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/ContractRequestRepository.cs new file mode 100644 index 000000000..7fc32a8ed --- /dev/null +++ b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/ContractRequestRepository.cs @@ -0,0 +1,65 @@ +using Agency.Domain; +using Agency.Domain.Model; +using Microsoft.EntityFrameworkCore; + +namespace Agency.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий заявок (MongoDB — навигационные свойства заполняются вручную) +/// +public class ContractRequestRepository(AgencyDbContext context) : IRepository +{ + public async Task Create(ContractRequest entity) + { + var entry = await context.ContractRequests.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(int entityId) + { + var entity = await context.ContractRequests.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) return false; + context.ContractRequests.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task Read(int entityId) + { + var entity = await context.ContractRequests.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) return null; + await FillNavigationProperties(new[] { entity }); + return entity; + } + + public async Task> ReadAll() + { + var list = await context.ContractRequests.ToListAsync(); + await FillNavigationProperties(list); + return list; + } + + public async Task Update(ContractRequest entity) + { + context.ContractRequests.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + private async Task FillNavigationProperties(IList requests) + { + if (requests.Count == 0) return; + var counterpartyIds = requests.Select(r => r.CounterpartyId).Distinct().ToList(); + var realEstateIds = requests.Select(r => r.RealEstateId).Distinct().ToList(); + var counterparties = await context.Counterparties.Where(c => counterpartyIds.Contains(c.Id)).ToListAsync(); + var realEstates = await context.RealEstates.Where(r => realEstateIds.Contains(r.Id)).ToListAsync(); + var cpMap = counterparties.ToDictionary(c => c.Id); + var reMap = realEstates.ToDictionary(r => r.Id); + foreach (var r in requests) + { + if (cpMap.TryGetValue(r.CounterpartyId, out var cp)) r.Counterparty = cp; + if (reMap.TryGetValue(r.RealEstateId, out var re)) r.RealEstate = re; + } + } +} diff --git a/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/CounterpartyRepository.cs b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/CounterpartyRepository.cs new file mode 100644 index 000000000..4be239e97 --- /dev/null +++ b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/CounterpartyRepository.cs @@ -0,0 +1,40 @@ +using Agency.Domain; +using Agency.Domain.Model; +using Microsoft.EntityFrameworkCore; + +namespace Agency.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий контрагентов +/// +public class CounterpartyRepository(AgencyDbContext context) : IRepository +{ + public async Task Create(Counterparty entity) + { + var entry = await context.Counterparties.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(int entityId) + { + var entity = await context.Counterparties.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) return false; + context.Counterparties.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task Read(int entityId) => + await context.Counterparties.FirstOrDefaultAsync(e => e.Id == entityId); + + public async Task> ReadAll() => + await context.Counterparties.ToListAsync(); + + public async Task Update(Counterparty entity) + { + context.Counterparties.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} diff --git a/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/RealEstateRepository.cs b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/RealEstateRepository.cs new file mode 100644 index 000000000..bbf75f11f --- /dev/null +++ b/RealEstateAgency/Agency.Infrastructure.EfCore/Repositories/RealEstateRepository.cs @@ -0,0 +1,40 @@ +using Agency.Domain; +using Agency.Domain.Model; +using Microsoft.EntityFrameworkCore; + +namespace Agency.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий объектов недвижимости +/// +public class RealEstateRepository(AgencyDbContext context) : IRepository +{ + public async Task Create(RealEstate entity) + { + var entry = await context.RealEstates.AddAsync(entity); + await context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(int entityId) + { + var entity = await context.RealEstates.FirstOrDefaultAsync(e => e.Id == entityId); + if (entity == null) return false; + context.RealEstates.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task Read(int entityId) => + await context.RealEstates.FirstOrDefaultAsync(e => e.Id == entityId); + + public async Task> ReadAll() => + await context.RealEstates.ToListAsync(); + + public async Task Update(RealEstate entity) + { + context.RealEstates.Update(entity); + await context.SaveChangesAsync(); + return entity; + } +} diff --git a/RealEstateAgency/Agency.ServiceDefaults/Agency.ServiceDefaults.csproj b/RealEstateAgency/Agency.ServiceDefaults/Agency.ServiceDefaults.csproj new file mode 100644 index 000000000..400c8c46c --- /dev/null +++ b/RealEstateAgency/Agency.ServiceDefaults/Agency.ServiceDefaults.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/RealEstateAgency/Agency.ServiceDefaults/Extensions.cs b/RealEstateAgency/Agency.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..3b51efaf6 --- /dev/null +++ b/RealEstateAgency/Agency.ServiceDefaults/Extensions.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Exporter.OpenTelemetryProtocol; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Agency.ServiceDefaults; + +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 => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + 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 => + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)) + .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(); + } + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks(HealthEndpointPath); + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + return app; + } +} diff --git a/RealEstateAgency/RealEstateAgency.sln b/RealEstateAgency/RealEstateAgency.sln index e9aa9f693..7a0f79e94 100644 --- a/RealEstateAgency/RealEstateAgency.sln +++ b/RealEstateAgency/RealEstateAgency.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36616.10 @@ -7,6 +7,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Domain", "Agency.Dom EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Tests", "Agency.Tests\Agency.Tests.csproj", "{F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Application.Contracts", "Agency.Application.Contracts\Agency.Application.Contracts.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567801}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Application", "Agency.Application\Agency.Application.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567802}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Infrastructure.EfCore", "Agency.Infrastructure.EfCore\Agency.Infrastructure.EfCore.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567803}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Api.Host", "Agency.Api.Host\Agency.Api.Host.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567804}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.ServiceDefaults", "Agency.ServiceDefaults\Agency.ServiceDefaults.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567805}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.AppHost", "Agency.AppHost\Agency.AppHost.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567806}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +33,30 @@ Global {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 70bcd070e03e9403b809a6ef1c6d48194001cb0a Mon Sep 17 00:00:00 2001 From: Cure232 Date: Tue, 24 Feb 2026 13:47:11 +0400 Subject: [PATCH 3/5] lab4 added grpc --- .../Agency.Api.Host/Agency.Api.Host.csproj | 7 + .../Agency.Api.Host/Grpc/AgencyGrpcClient.cs | 152 ++++++++++++++++++ .../Agency.Api.Host/Grpc/AgencyGrpcProfile.cs | 22 +++ RealEstateAgency/Agency.Api.Host/Program.cs | 19 +++ .../Agency.AppHost/Agency.AppHost.csproj | 1 + RealEstateAgency/Agency.AppHost/AppHost.cs | 12 +- .../Agency.AppHost/appsettings.json | 4 + .../Protos/ContractRequest.proto | 30 ++++ .../Agency.Generator.Grpc.Host.csproj | 28 ++++ .../Generator/ContractRequestGenerator.cs | 44 +++++ .../Grpc/AgencyGeneratorGrpcProfile.cs | 22 +++ .../Grpc/AgencyGrpcGeneratorService.cs | 104 ++++++++++++ .../Agency.Generator.Grpc.Host/Program.cs | 24 +++ .../Properties/launchSettings.json | 14 ++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 ++ RealEstateAgency/RealEstateAgency.sln | 84 +++++++++- 17 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs create mode 100644 RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcProfile.cs create mode 100644 RealEstateAgency/Agency.Application.Contracts/Protos/ContractRequest.proto create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Agency.Generator.Grpc.Host.csproj create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Generator/ContractRequestGenerator.cs create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGeneratorGrpcProfile.cs create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGrpcGeneratorService.cs create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Program.cs create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/Properties/launchSettings.json create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.Development.json create mode 100644 RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.json diff --git a/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj b/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj index 1d34962c7..21d0ba78d 100644 --- a/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj +++ b/RealEstateAgency/Agency.Api.Host/Agency.Api.Host.csproj @@ -7,6 +7,8 @@ + + @@ -14,4 +16,9 @@ + + + Protos\ContractRequest.proto + + diff --git a/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs new file mode 100644 index 000000000..4ece71595 --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs @@ -0,0 +1,152 @@ +using Agency.Application.Contracts.ContractRequests; +using Agency.Application.Contracts.Counterparties; +using Agency.Application.Contracts.Protos; +using Agency.Application.Contracts.RealEstates; +using AutoMapper; +using Grpc.Core; +using Microsoft.Extensions.Caching.Memory; + +namespace Agency.Api.Host.Grpc; + +/// +/// Фоновый gRPC клиент для получения батчей ContractRequestCreateUpdateDto из bidirectional стрима и создания заявок в системе +/// +public class AgencyGrpcClient( + ContractRequestGeneratorGrpcService.ContractRequestGeneratorGrpcServiceClient client, + IServiceScopeFactory scopeFactory, + IMapper mapper, + ILogger logger, + IConfiguration cfg, + IMemoryCache cache +) : BackgroundService +{ + private static readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(10); + + /// + /// Основной цикл фонового сервиса который подключается к gRPC серверу отправляет запрос генерации и обрабатывает входящие батчи + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + var countPerRequest = cfg.GetValue("ContractRequestGenerator:CountPerRequest", 100); + var batchSize = cfg.GetValue("ContractRequestGenerator:BatchSize", 10); + + logger.LogInformation("Connecting to ContractRequestGenerator gRPC bidirectional stream..."); + + using var call = client.ContractRequestStream(cancellationToken: stoppingToken); + + var requestId = Guid.NewGuid().ToString("N"); + + var writerTask = Task.Run(async () => + { + await call.RequestStream.WriteAsync(new ContractRequestGenerationRequest + { + RequestId = requestId, + Count = countPerRequest, + BatchSize = batchSize + }); + + await call.RequestStream.CompleteAsync(); + }, stoppingToken); + + await foreach (var msg in call.ResponseStream.ReadAllAsync(stoppingToken)) + { + if (!string.Equals(msg.RequestId, requestId, StringComparison.Ordinal)) + continue; + + var dtos = msg.ContractRequests.Select(mapper.Map).ToList(); + + using var scope = scopeFactory.CreateScope(); + + var contractRequestService = scope.ServiceProvider.GetRequiredService(); + var counterpartyService = scope.ServiceProvider.GetRequiredService(); + var realEstateService = scope.ServiceProvider.GetRequiredService(); + + var valid = new List(dtos.Count); + + foreach (var dto in dtos) + { + if (!await ExistsAsync(dto.CounterpartyId, "Counterparty", dto, counterpartyService.Get, stoppingToken)) + continue; + + if (!await ExistsAsync(dto.RealEstateId, "RealEstate", dto, realEstateService.Get, stoppingToken)) + continue; + + valid.Add(dto); + } + + var created = 0; + foreach (var dto in valid) + { + await contractRequestService.Create(dto); + created++; + } + + logger.LogInformation("Received batch: total={total}, valid={valid}, created={created}, isFinal={isFinal}", dtos.Count, valid.Count, created, msg.IsFinal); + + if (msg.IsFinal) + break; + } + + await writerTask; + + logger.LogInformation("Finished receiving contract requests for request_id={requestId}", requestId); + break; + } + catch (RpcException ex) when (!stoppingToken.IsCancellationRequested) + { + logger.LogError(ex, "gRPC stream error: {code} - {status}", ex.StatusCode, ex.Status); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + catch (Exception ex) when (!stoppingToken.IsCancellationRequested) + { + logger.LogError(ex, "Unexpected exception during receiving contract requests from gRPC stream"); + break; + } + } + } + + /// + /// Проверка наличия сущности по идентификатору с использованием IMemoryCache чтобы не выполнять повторные запросы + /// + private async Task ExistsAsync( + int id, + string entityName, + ContractRequestCreateUpdateDto dto, + Func> readFunc, + CancellationToken ct) + where TEntity : class + { + var cacheKey = $"{entityName}:exists:{id}"; + + if (cache.TryGetValue(cacheKey, out bool cached)) + return cached; + + ct.ThrowIfCancellationRequested(); + + bool exists; + try + { + var entity = await readFunc(id); + exists = entity is not null; + } + catch (KeyNotFoundException) + { + exists = false; + + logger.LogWarning( + "Skipping contract request dto because {entity} with id {id} was not found counterpartyId={counterpartyId} realEstateId={realEstateId}", + entityName, id, dto.CounterpartyId, dto.RealEstateId); + } + + cache.Set(cacheKey, exists, new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheTtl + }); + + return exists; + } +} diff --git a/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcProfile.cs b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcProfile.cs new file mode 100644 index 000000000..3a666eabd --- /dev/null +++ b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcProfile.cs @@ -0,0 +1,22 @@ +using Agency.Application.Contracts.Protos; +using Agency.Application.Contracts.ContractRequests; +using AutoMapper; + +namespace Agency.Api.Host.Grpc; + +/// +/// Профиль AutoMapper для преобразования protobuf сообщений gRPC в контрактные DTO приложения +/// +public class AgencyGrpcProfile : Profile +{ + /// + /// Настройка правил маппинга между сообщениями gRPC и DTO используемыми в прикладном слое + /// + public AgencyGrpcProfile() + { + CreateMap() + .ForCtorParam("ContractRequestType", o => o.MapFrom(s => (Domain.Model.ContractRequestType)s.ContractRequestType)) + .ForCtorParam("Amount", o => o.MapFrom(s => (decimal)s.Amount)) + .ForCtorParam("CreatedDate", o => o.MapFrom(s => DateTime.Parse(s.CreatedDate))); + } +} diff --git a/RealEstateAgency/Agency.Api.Host/Program.cs b/RealEstateAgency/Agency.Api.Host/Program.cs index a30282be3..e7b3f4979 100644 --- a/RealEstateAgency/Agency.Api.Host/Program.cs +++ b/RealEstateAgency/Agency.Api.Host/Program.cs @@ -1,7 +1,9 @@ +using Agency.Api.Host.Grpc; using Agency.Application; using Agency.Application.Contracts; using Agency.Application.Contracts.ContractRequests; using Agency.Application.Contracts.Counterparties; +using Agency.Application.Contracts.Protos; using Agency.Application.Contracts.RealEstates; using Agency.Application.Services; using Agency.Domain; @@ -20,6 +22,7 @@ builder.Services.AddAutoMapper(config => { config.AddProfile(new AgencyProfile()); + config.AddProfile(new AgencyGrpcProfile()); }); builder.Services.AddTransient, CounterpartyRepository>(); @@ -51,6 +54,22 @@ o.UseMongoDB(db.Client, db.DatabaseNamespace.DatabaseName); }); +builder.Services.AddGrpc(options => +{ + options.EnableDetailedErrors = builder.Environment.IsDevelopment(); +}); + +builder.Services.AddGrpcClient(o => +{ + var addr = builder.Configuration["ContractRequestGenerator:GrpcAddress"] + ?? throw new InvalidOperationException("ContractRequestGenerator:GrpcAddress is not configured"); + o.Address = new Uri(addr); +}); + +builder.Services.AddMemoryCache(); + +builder.Services.AddHostedService(); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj b/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj index f3f0145c3..207c2fe5d 100644 --- a/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj +++ b/RealEstateAgency/Agency.AppHost/Agency.AppHost.csproj @@ -13,5 +13,6 @@ + diff --git a/RealEstateAgency/Agency.AppHost/AppHost.cs b/RealEstateAgency/Agency.AppHost/AppHost.cs index 647448806..7cac37559 100644 --- a/RealEstateAgency/Agency.AppHost/AppHost.cs +++ b/RealEstateAgency/Agency.AppHost/AppHost.cs @@ -2,8 +2,18 @@ var db = builder.AddMongoDB("mongo").AddDatabase("db"); +var batchSize = builder.AddParameter("GeneratorBatchSize"); +var waitTime = builder.AddParameter("GeneratorWaitTime"); + +var grpcServer = builder.AddProject("agency-generator-grpc-host") + .WithEnvironment("Generator:BatchSize", batchSize) + .WithEnvironment("Generator:WaitTime", waitTime); + builder.AddProject("agency-api-host") .WithReference(db, "agencyClient") - .WaitFor(db); + .WaitFor(db) + .WithReference(grpcServer) + .WithEnvironment("ContractRequestGenerator__GrpcAddress", grpcServer.GetEndpoint("https")) + .WaitFor(grpcServer); builder.Build().Run(); diff --git a/RealEstateAgency/Agency.AppHost/appsettings.json b/RealEstateAgency/Agency.AppHost/appsettings.json index abdf02560..195a5f668 100644 --- a/RealEstateAgency/Agency.AppHost/appsettings.json +++ b/RealEstateAgency/Agency.AppHost/appsettings.json @@ -5,6 +5,10 @@ "Microsoft.AspNetCore": "Warning", "Aspire.Hosting.Dcp": "Warning" } + }, + "Parameters": { + "GeneratorBatchSize": 4, + "GeneratorWaitTime": 2 } } diff --git a/RealEstateAgency/Agency.Application.Contracts/Protos/ContractRequest.proto b/RealEstateAgency/Agency.Application.Contracts/Protos/ContractRequest.proto new file mode 100644 index 000000000..d42511de2 --- /dev/null +++ b/RealEstateAgency/Agency.Application.Contracts/Protos/ContractRequest.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +option csharp_namespace = "Agency.Application.Contracts.Protos"; + +message ContractRequestGenerationRequest { + string request_id = 1; + + int32 count = 2; + + int32 batch_size = 3; +} + +message ContractRequestCreateUpdateDtoMessage { + int32 counterparty_id = 1; + int32 real_estate_id = 2; + int32 contract_request_type = 3; + double amount = 4; + string created_date = 5; + string status = 6; +} + +message ContractRequestBatchStreamMessage { + string request_id = 1; + repeated ContractRequestCreateUpdateDtoMessage contract_requests = 2; + bool is_final = 3; +} + +service ContractRequestGeneratorGrpcService { + rpc ContractRequestStream (stream ContractRequestGenerationRequest) returns (stream ContractRequestBatchStreamMessage); +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Agency.Generator.Grpc.Host.csproj b/RealEstateAgency/Agency.Generator.Grpc.Host/Agency.Generator.Grpc.Host.csproj new file mode 100644 index 000000000..c917f5c3b --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Agency.Generator.Grpc.Host.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + Protos\ContractRequest.proto + + + + diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Generator/ContractRequestGenerator.cs b/RealEstateAgency/Agency.Generator.Grpc.Host/Generator/ContractRequestGenerator.cs new file mode 100644 index 000000000..af98a9c21 --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Generator/ContractRequestGenerator.cs @@ -0,0 +1,44 @@ +using Agency.Application.Contracts.ContractRequests; +using Agency.Domain.Model; +using Bogus; + +namespace Agency.Generator.Grpc.Host.Generator; + +/// +/// Генератор тестовых ContractRequestCreateUpdateDto на основе Bogus +/// +public static class ContractRequestGenerator +{ + /// + /// Генерация списка DTO для создания или обновления заявок + /// + public static IList Generate(int count) + { + var faker = new Faker("ru"); + + var list = new List(count); + + var statuses = new[] { "Новая", "В обработке", "Одобрена", "Отклонена", "Завершена" }; + + for (var i = 0; i < count; i++) + { + var counterpartyId = faker.Random.Int(1, 10); + var realEstateId = faker.Random.Int(101, 110); + var contractRequestType = faker.PickRandom(); + var amount = Math.Round((decimal)faker.Random.Double(100_000, 100_000_000), 2); + var createdDate = faker.Date.Between(DateTime.Now.AddYears(-2), DateTime.Now); + var status = faker.PickRandom(statuses); + + list.Add(new ContractRequestCreateUpdateDto( + CounterpartyId: counterpartyId, + RealEstateId: realEstateId, + ContractRequestType: contractRequestType, + Amount: amount, + CreatedDate: createdDate, + Status: status + )); + } + + return list; + } +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGeneratorGrpcProfile.cs b/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGeneratorGrpcProfile.cs new file mode 100644 index 000000000..16927dede --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGeneratorGrpcProfile.cs @@ -0,0 +1,22 @@ +using Agency.Application.Contracts.Protos; +using Agency.Application.Contracts.ContractRequests; +using AutoMapper; + +namespace Agency.Generator.Grpc.Host.Grpc; + +/// +/// Профиль AutoMapper для преобразования контрактных DTO в protobuf сообщения gRPC +/// +public sealed class AgencyGeneratorGrpcProfile : Profile +{ + /// + /// Настройка правил маппинга между DTO приложения и сообщениями gRPC + /// + public AgencyGeneratorGrpcProfile() + { + CreateMap() + .ForMember(d => d.ContractRequestType, o => o.MapFrom(s => (int)s.ContractRequestType)) + .ForMember(d => d.Amount, o => o.MapFrom(s => (double)s.Amount)) + .ForMember(d => d.CreatedDate, o => o.MapFrom(s => s.CreatedDate.ToString("o"))); + } +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGrpcGeneratorService.cs b/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGrpcGeneratorService.cs new file mode 100644 index 000000000..8345864c8 --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Grpc/AgencyGrpcGeneratorService.cs @@ -0,0 +1,104 @@ +using AutoMapper; +using Agency.Application.Contracts.Protos; +using Grpc.Core; +using Agency.Generator.Grpc.Host.Generator; + +namespace Agency.Generator.Grpc.Host.Grpc; + +/// +/// Имплементация gRPC серверной части службы для генерации и отправки батчей заявок +/// +/// Конфигурация +/// Профиль маппинга +/// Логгер +public sealed class AgencyGrpcGeneratorService( + IConfiguration configuration, + IMapper mapper, + ILogger logger +) : ContractRequestGeneratorGrpcService.ContractRequestGeneratorGrpcServiceBase +{ + private readonly string _defaultBatchSize = + configuration.GetSection("Generator")["BatchSize"] ?? throw new KeyNotFoundException("BatchSize section of Generator is missing"); + + private readonly string _waitTime = + configuration.GetSection("Generator")["WaitTime"] ?? throw new KeyNotFoundException("WaitTime section of Generator is missing"); + + /// + /// Служба bidirectional стриминга которая принимает запросы генерации и отправляет батчи контрактов клиенту + /// + /// Клиентский стрим запросов + /// Серверный стрим ответов + /// Контекст вызова + /// Если параметры конфигурации не парсятся + public override async Task ContractRequestStream( + IAsyncStreamReader requestStream, + IServerStreamWriter responseStream, + ServerCallContext context) + { + if (!int.TryParse(_defaultBatchSize, out var defaultBatchSize)) + throw new FormatException("Unable to parse Generator:BatchSize"); + + if (!int.TryParse(_waitTime, out var waitTimeSeconds)) + throw new FormatException("Unable to parse Generator:WaitTime"); + + logger.LogInformation("ContractRequest generator stream started peer={peer} defaultBatchSize={batch} waitTimeSec={wait}", context.Peer, defaultBatchSize, waitTimeSeconds); + + await foreach (var req in requestStream.ReadAllAsync(context.CancellationToken)) + { + if (context.CancellationToken.IsCancellationRequested) + break; + + var requestId = req.RequestId ?? string.Empty; + + if (req.Count <= 0) + { + logger.LogWarning("Skipping request with non positive count requestId={requestId} count={count}", requestId, req.Count); + continue; + } + + var batchSize = req.BatchSize > 0 ? req.BatchSize : defaultBatchSize; + + logger.LogInformation("Processing request requestId={requestId} count={count} batchSize={batchSize}", requestId, req.Count, batchSize); + + var sent = 0; + + while (sent < req.Count && !context.CancellationToken.IsCancellationRequested) + { + try + { + var take = Math.Min(batchSize, req.Count - sent); + + var dtos = ContractRequestGenerator.Generate(take); + + var payload = new ContractRequestBatchStreamMessage + { + RequestId = requestId, + IsFinal = false + }; + + payload.ContractRequests.AddRange(mapper.Map>(dtos)); + + sent += take; + payload.IsFinal = sent >= req.Count; + + await responseStream.WriteAsync(payload); + + logger.LogInformation("Sent batch requestId={requestId} batch={batch} sent={sent} total={total} isFinal={isFinal}", requestId, take, sent, req.Count, payload.IsFinal); + + if (payload.IsFinal) + break; + + if (waitTimeSeconds > 0) + await Task.Delay(waitTimeSeconds * 1000, context.CancellationToken); + } + catch (TaskCanceledException ex) + { + logger.LogWarning(ex, "Cancellation requested from client peer={peer} requestId={requestId}", context.Peer, requestId); + return; + } + } + } + + logger.LogInformation("ContractRequest generator stream finished peer={peer}", context.Peer); + } +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Program.cs b/RealEstateAgency/Agency.Generator.Grpc.Host/Program.cs new file mode 100644 index 000000000..b43c66abc --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Program.cs @@ -0,0 +1,24 @@ +using Agency.Generator.Grpc.Host.Grpc; +using Agency.ServiceDefaults; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new AgencyGeneratorGrpcProfile()); +}); + +builder.Services.AddGrpc(options => +{ + options.EnableDetailedErrors = builder.Environment.IsDevelopment(); +}); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +app.MapGrpcService(); + +app.Run(); diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/Properties/launchSettings.json b/RealEstateAgency/Agency.Generator.Grpc.Host/Properties/launchSettings.json new file mode 100644 index 000000000..b1798af6a --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7201;http://localhost:5201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.Development.json b/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.json b/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/RealEstateAgency/Agency.Generator.Grpc.Host/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/RealEstateAgency/RealEstateAgency.sln b/RealEstateAgency/RealEstateAgency.sln index 7a0f79e94..3807eb0da 100644 --- a/RealEstateAgency/RealEstateAgency.sln +++ b/RealEstateAgency/RealEstateAgency.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36616.10 @@ -19,44 +19,126 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.ServiceDefaults", "A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.AppHost", "Agency.AppHost\Agency.AppHost.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567806}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agency.Generator.Grpc.Host", "Agency.Generator.Grpc.Host\Agency.Generator.Grpc.Host.csproj", "{A0B6687C-6065-439C-BFDE-35B7FB3B8938}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|x64.Build.0 = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Debug|x86.Build.0 = Debug|Any CPU {4D9D2410-8028-4C70-B261-29992356D91C}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D9D2410-8028-4C70-B261-29992356D91C}.Release|Any CPU.Build.0 = Release|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|x64.ActiveCfg = Release|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|x64.Build.0 = Release|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|x86.ActiveCfg = Release|Any CPU + {4D9D2410-8028-4C70-B261-29992356D91C}.Release|x86.Build.0 = Release|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|x64.ActiveCfg = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|x64.Build.0 = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|x86.ActiveCfg = Debug|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Debug|x86.Build.0 = Debug|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|x64.ActiveCfg = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|x64.Build.0 = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|x86.ActiveCfg = Release|Any CPU + {F461EB8F-D9AE-4CE0-B5C4-60DC139B5CF9}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567801}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567802}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567803}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567804}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567805}.Release|x86.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567806}.Release|x86.Build.0 = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|x64.ActiveCfg = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|x64.Build.0 = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|x86.ActiveCfg = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Debug|x86.Build.0 = Debug|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|Any CPU.Build.0 = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|x64.ActiveCfg = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|x64.Build.0 = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|x86.ActiveCfg = Release|Any CPU + {A0B6687C-6065-439C-BFDE-35B7FB3B8938}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 08f3a1463273fda73dd9537c8c6c543442f2e864 Mon Sep 17 00:00:00 2001 From: Cure232 Date: Tue, 24 Feb 2026 19:13:38 +0400 Subject: [PATCH 4/5] pr fixes: added controllers summaries for swagger, fixed break in grpc client, now it has 2 seconds delay after finishing matching delay in generator --- .../Controllers/AnalyticsController.cs | 48 ++++++++++++++++++- .../Controllers/CrudControllerBase.cs | 44 +++++++++++++++++ .../Agency.Api.Host/Grpc/AgencyGrpcClient.cs | 2 +- .../ContractRequestCreateUpdateDto.cs | 6 +++ .../ContractRequests/ContractRequestDto.cs | 7 +++ .../CounterpartyCreateUpdateDto.cs | 3 ++ .../Counterparties/CounterpartyDto.cs | 4 ++ .../RealEstates/RealEstateCreateUpdateDto.cs | 11 +++++ .../RealEstates/RealEstateDto.cs | 12 +++++ 9 files changed, 135 insertions(+), 2 deletions(-) diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs index 6ad409960..4f97c2c8b 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs @@ -9,6 +9,15 @@ namespace Agency.Api.Host.Controllers; [ApiController] public class AnalyticsController(IAnalyticsService service, ILogger logger) : ControllerBase { + + /// + /// , + /// + /// () + /// () + /// -, + /// + /// [HttpGet("sellers-with-requests-in-period")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -26,6 +35,12 @@ public async Task>> GetSellersWithRequestsIn } } + /// + /// -5 + /// + /// + /// -5 + /// [HttpGet("top5-buyers-by-request-count")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -43,6 +58,12 @@ public async Task>> GetTop5BuyersByRequestCo } } + /// + /// -5 + /// + /// + /// -5 + /// [HttpGet("top5-sellers-by-request-count")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -60,6 +81,12 @@ public async Task>> GetTop5SellersByRequestC } } + /// + /// + /// + /// , - , - + /// + /// [HttpGet("request-count-by-property-type")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -77,6 +104,12 @@ public async Task>> GetRequestCount } } + /// + /// , + /// + /// , + /// + /// [HttpGet("clients-with-minimal-request-amount")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -94,6 +127,13 @@ public async Task>> GetClientsWithMinimalReq } } + /// + /// , () + /// + /// + /// , , + /// + /// [HttpGet("clients-searching-for-property-type")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -111,6 +151,12 @@ public async Task>> GetClientsSearchingForPr } } + /// + /// , ( House), + /// + /// , , + /// , + /// [HttpGet("clients-searching-for-houses-ordered-by-fullname")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -127,4 +173,4 @@ public async Task>> GetClientsSearchingForHo return StatusCode(500, ex.Message); } } -} +} \ No newline at end of file diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs index bad8b6378..1bc55e33a 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs @@ -3,6 +3,12 @@ namespace Agency.Api.Host.Controllers; +/// +/// CRUD , +/// +/// DTO / +/// DTO +/// (int, Guid, long ..) [Route("api/[controller]")] [ApiController] public abstract class CrudControllerBase( @@ -12,6 +18,13 @@ public abstract class CrudControllerBase( where TCreateUpdateDto : class where TKey : struct { + /// + /// + /// + /// + /// + /// + /// [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] @@ -30,6 +43,15 @@ public async Task> Create(TCreateUpdateDto newDto) } } + /// + /// + /// + /// + /// + /// + /// + /// + /// [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] @@ -52,6 +74,14 @@ public async Task> Edit(TKey id, TCreateUpdateDto newDto) } } + /// + /// + /// + /// + /// + /// + /// ( ) + /// [HttpDelete("{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] @@ -70,6 +100,12 @@ public async Task Delete(TKey id) } } + /// + /// + /// + /// + /// + /// [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -87,6 +123,14 @@ public async Task>> GetAll() } } + /// + /// + /// + /// + /// + /// + /// + /// [HttpGet("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] diff --git a/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs index 4ece71595..8224c510a 100644 --- a/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs +++ b/RealEstateAgency/Agency.Api.Host/Grpc/AgencyGrpcClient.cs @@ -94,7 +94,7 @@ await call.RequestStream.WriteAsync(new ContractRequestGenerationRequest await writerTask; logger.LogInformation("Finished receiving contract requests for request_id={requestId}", requestId); - break; + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } catch (RpcException ex) when (!stoppingToken.IsCancellationRequested) { diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs index e078ec6ad..a6cb45b6e 100644 --- a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs @@ -6,6 +6,12 @@ namespace Agency.Application.Contracts.ContractRequests; /// /// DTO для создания/обновления заявки /// +/// +/// +/// +/// +/// +/// public record ContractRequestCreateUpdateDto( int CounterpartyId, int RealEstateId, diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs index 95921d93d..44ebb4bd5 100644 --- a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs @@ -6,6 +6,13 @@ namespace Agency.Application.Contracts.ContractRequests; /// /// DTO заявки /// +/// +/// +/// +/// +/// +/// +/// public record ContractRequestDto( int Id, int CounterpartyId, diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs index f3d6ec936..f8a284844 100644 --- a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs @@ -5,6 +5,9 @@ namespace Agency.Application.Contracts.Counterparties; /// /// DTO для создания/обновления контрагента /// +/// +/// +/// public record CounterpartyCreateUpdateDto( [Required][StringLength(150)] string FullName, [Required][StringLength(20)] string PassportNumber, diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs index a783ed103..bc8c16383 100644 --- a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs @@ -5,6 +5,10 @@ namespace Agency.Application.Contracts.Counterparties; /// /// DTO контрагента /// +/// +/// +/// +/// public record CounterpartyDto( int Id, [Required][StringLength(150)] string FullName, diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs index 3cc0caef6..3f5f03015 100644 --- a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs @@ -6,6 +6,17 @@ namespace Agency.Application.Contracts.RealEstates; /// /// DTO для создания/обновления объекта недвижимости /// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// public record RealEstateCreateUpdateDto( RealEstateType Type, RealEstatePurpose Purpose, diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs index ecc74e0e7..7a1eb6afe 100644 --- a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs @@ -6,6 +6,18 @@ namespace Agency.Application.Contracts.RealEstates; /// /// DTO объекта недвижимости /// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// public record RealEstateDto( int Id, RealEstateType Type, From ba5eab71525c967bf31cd503def880fde9bfa9ed Mon Sep 17 00:00:00 2001 From: Cure232 Date: Tue, 24 Feb 2026 19:53:41 +0400 Subject: [PATCH 5/5] fix: tried to fix encoding + additional documentation --- .gitattributes | 5 ++ .../Controllers/AnalyticsController.cs | 64 +++++++++--------- .../Controllers/ContractRequestController.cs | 2 +- .../Controllers/CounterpartyController.cs | 2 +- .../Controllers/CrudControllerBase.cs | 66 +++++++++---------- .../Controllers/RealEstateController.cs | 2 +- .../ContractRequestCreateUpdateDto.cs | 15 +++-- .../ContractRequests/ContractRequestDto.cs | 17 ++--- .../CounterpartyCreateUpdateDto.cs | 9 +-- .../Counterparties/CounterpartyDto.cs | 11 ++-- .../RealEstates/RealEstateCreateUpdateDto.cs | 25 +++---- .../RealEstates/RealEstateDto.cs | 27 ++++---- 12 files changed, 128 insertions(+), 117 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d892c195b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.cs text encoding=UTF-8 +*.json text encoding=UTF-8 +*.xml text encoding=UTF-8 +*.config text encoding=UTF-8 \ No newline at end of file diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs index 4f97c2c8b..650067a4d 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/AnalyticsController.cs @@ -1,4 +1,4 @@ -using Agency.Application.Contracts; +using Agency.Application.Contracts; using Agency.Application.Contracts.Counterparties; using Agency.Domain.Model; using Microsoft.AspNetCore.Mvc; @@ -11,13 +11,13 @@ public class AnalyticsController(IAnalyticsService service, ILogger - /// , + /// Получает список всех продавцов, которые оставили заявки на продажу за указанный период времени /// - /// () - /// () - /// -, - /// - /// + /// Начальная дата периода (включительно) + /// Конечная дата периода (включительно) + /// Список контрагентов-продавцов, оставивших заявки в указанный период + /// Успешное получение списка продавцов + /// Внутренняя ошибка сервера [HttpGet("sellers-with-requests-in-period")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -36,11 +36,11 @@ public async Task>> GetSellersWithRequestsIn } /// - /// -5 + /// Получает топ-5 клиентов по количеству заявок на покупку недвижимости /// - /// - /// -5 - /// + /// Список пяти клиентов с наибольшим количеством заявок на покупку + /// Успешное получение списка топ-5 покупателей + /// Внутренняя ошибка сервера [HttpGet("top5-buyers-by-request-count")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -59,11 +59,11 @@ public async Task>> GetTop5BuyersByRequestCo } /// - /// -5 + /// Получает топ-5 клиентов по количеству заявок на продажу недвижимости /// - /// - /// -5 - /// + /// Список пяти клиентов с наибольшим количеством заявок на продажу + /// Успешное получение списка топ-5 продавцов + /// Внутренняя ошибка сервера [HttpGet("top5-sellers-by-request-count")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -82,11 +82,11 @@ public async Task>> GetTop5SellersByRequestC } /// - /// + /// Получает статистику по количеству заявок для каждого типа недвижимости /// - /// , - , - - /// - /// + /// Словарь, где ключ - тип недвижимости, значение - количество заявок + /// Успешное получение статистики по типам недвижимости + /// Внутренняя ошибка сервера [HttpGet("request-count-by-property-type")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -105,11 +105,11 @@ public async Task>> GetRequestCount } /// - /// , + /// Получает список клиентов, которые открыли заявки с минимальной стоимостью /// - /// , - /// - /// + /// Список клиентов, чьи заявки имеют минимальную сумму + /// Успешное получение списка клиентов с минимальными заявками + /// Внутренняя ошибка сервера [HttpGet("clients-with-minimal-request-amount")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -128,12 +128,12 @@ public async Task>> GetClientsWithMinimalReq } /// - /// , () + /// Получает список всех клиентов, ищущих недвижимость заданного типа (покупка) /// - /// - /// , , - /// - /// + /// Тип недвижимости для поиска + /// Список клиентов, ищущих недвижимость указанного типа, отсортированный по ФИО + /// Успешное получение списка клиентов по типу недвижимости + /// Внутренняя ошибка сервера [HttpGet("clients-searching-for-property-type")] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -152,11 +152,11 @@ public async Task>> GetClientsSearchingForPr } /// - /// , ( House), + /// Получает список всех клиентов, ищущих дома (тип недвижимости House), отсортированный по ФИО /// - /// , , - /// , - /// + /// Список клиентов, ищущих дома, отсортированный по ФИО в алфавитном порядке + /// Успешное получение списка клиентов, ищущих дома + /// Внутренняя ошибка сервера [HttpGet("clients-searching-for-houses-ordered-by-fullname")] [ProducesResponseType(200)] [ProducesResponseType(500)] diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs index 145d083dc..ecf19d356 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/ContractRequestController.cs @@ -1,4 +1,4 @@ -using Agency.Application.Contracts.ContractRequests; +using Agency.Application.Contracts.ContractRequests; using Microsoft.AspNetCore.Mvc; namespace Agency.Api.Host.Controllers; diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs index 4789343fa..7c0b59b38 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/CounterpartyController.cs @@ -1,4 +1,4 @@ -using Agency.Application.Contracts.Counterparties; +using Agency.Application.Contracts.Counterparties; using Microsoft.AspNetCore.Mvc; namespace Agency.Api.Host.Controllers; diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs index 1bc55e33a..086df05d5 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/CrudControllerBase.cs @@ -1,14 +1,14 @@ -using Agency.Application.Contracts; +using Agency.Application.Contracts; using Microsoft.AspNetCore.Mvc; namespace Agency.Api.Host.Controllers; /// -/// CRUD , +/// Базовый CRUD контроллер, предоставляющий стандартные операции для работы с сущностями /// -/// DTO / -/// DTO -/// (int, Guid, long ..) +/// Тип DTO для чтения/получения данных сущности +/// Тип DTO для создания и обновления сущности +/// Тип идентификатора сущности (int, Guid, long и т.д.) [Route("api/[controller]")] [ApiController] public abstract class CrudControllerBase( @@ -19,12 +19,12 @@ public abstract class CrudControllerBase( where TKey : struct { /// - /// + /// Создает новую сущность /// - /// - /// - /// - /// + /// Данные для создания новой сущности + /// Созданная сущность с присвоенным идентификатором + /// Сущность успешно создана + /// Внутренняя ошибка сервера [HttpPost] [ProducesResponseType(201)] [ProducesResponseType(500)] @@ -44,14 +44,14 @@ public async Task> Create(TCreateUpdateDto newDto) } /// - /// + /// Обновляет существующую сущность по идентификатору /// - /// - /// - /// - /// - /// - /// + /// Идентификатор обновляемой сущности + /// Новые данные для сущности + /// Обновленная сущность + /// Сущность успешно обновлена + /// Сущность с указанным идентификатором не найдена + /// Внутренняя ошибка сервера [HttpPut("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] @@ -75,13 +75,13 @@ public async Task> Edit(TKey id, TCreateUpdateDto newDto) } /// - /// + /// Удаляет сущность по идентификатору /// - /// - /// - /// - /// ( ) - /// + /// Идентификатор удаляемой сущности + /// Статус выполнения операции + /// Сущность успешно удалена + /// Сущность не найдена (ничего не удалено) + /// Внутренняя ошибка сервера [HttpDelete("{id}")] [ProducesResponseType(200)] [ProducesResponseType(204)] @@ -101,11 +101,11 @@ public async Task Delete(TKey id) } /// - /// + /// Получает список всех сущностей /// - /// - /// - /// + /// Список всех сущностей + /// Успешное получение списка + /// Внутренняя ошибка сервера [HttpGet] [ProducesResponseType(200)] [ProducesResponseType(500)] @@ -124,13 +124,13 @@ public async Task>> GetAll() } /// - /// + /// Получает сущность по идентификатору /// - /// - /// - /// - /// - /// + /// Идентификатор запрашиваемой сущности + /// Сущность с указанным идентификатором + /// Сущность найдена + /// Сущность с указанным идентификатором не найдена + /// Внутренняя ошибка сервера [HttpGet("{id}")] [ProducesResponseType(200)] [ProducesResponseType(404)] diff --git a/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs b/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs index 2c946102e..2c2a0b5aa 100644 --- a/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs +++ b/RealEstateAgency/Agency.Api.Host/Controllers/RealEstateController.cs @@ -1,4 +1,4 @@ -using Agency.Application.Contracts.RealEstates; +using Agency.Application.Contracts.RealEstates; using Microsoft.AspNetCore.Mvc; namespace Agency.Api.Host.Controllers; diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs index a6cb45b6e..92259f1a3 100644 --- a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestCreateUpdateDto.cs @@ -4,14 +4,15 @@ namespace Agency.Application.Contracts.ContractRequests; /// -/// DTO для создания/обновления заявки +/// DTO для создания новой заявки или обновления существующей +/// Используется в HTTP POST (создание) и HTTP PUT (обновление) запросах /// -/// -/// -/// -/// -/// -/// +/// Идентификатор контрагента (клиента), связанного с заявкой. Должен существовать в системе +/// Идентификатор объекта недвижимости, связанного с заявкой. Должен существовать в системе +/// Тип заявки: Purchase (0) - покупка, Sale (1) - продажа +/// Сумма сделки в рублях. Диапазон от 0 до 1 миллиарда рублей +/// Дата и время создания заявки в формате ISO 8601. Обычно устанавливается сервером +/// Текущий статус заявки. Максимальная длина 50 символов. public record ContractRequestCreateUpdateDto( int CounterpartyId, int RealEstateId, diff --git a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs index 44ebb4bd5..f4a925298 100644 --- a/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/ContractRequests/ContractRequestDto.cs @@ -4,15 +4,16 @@ namespace Agency.Application.Contracts.ContractRequests; /// -/// DTO заявки +/// DTO для получения информации о заявке +/// Используется в HTTP GET запросах для возврата данных клиенту /// -/// -/// -/// -/// -/// -/// -/// +/// Уникальный идентификатор заявки. Используется для ссылок на заявку, редактирования и удаления. Генерируется сервером при создании +/// Идентификатор контрагента (клиента), связанного с заявкой. Должен существовать в системе +/// Идентификатор объекта недвижимости, связанного с заявкой. Должен существовать в системе +/// Тип заявки: Purchase (0) - покупка, Sale (1) - продажа +/// Сумма сделки в рублях. Диапазон от 0 до 1 миллиарда рублей +/// Дата и время создания заявки в формате ISO 8601. Обычно устанавливается сервером +/// Текущий статус заявки. Максимальная длина 50 символов. public record ContractRequestDto( int Id, int CounterpartyId, diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs index f8a284844..b0061e097 100644 --- a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyCreateUpdateDto.cs @@ -3,11 +3,12 @@ namespace Agency.Application.Contracts.Counterparties; /// -/// DTO для создания/обновления контрагента +/// DTO для создания нового контрагента или обновления существующего +/// Используется в HTTP POST (создание) и HTTP PUT (обновление) запросах /// -/// -/// -/// +/// Полное имя контрагента в формате "Фамилия Имя Отчество". Максимальная длина 150 символов +/// Номер паспорта в формате "XXXX XXXXXX" (серия и номер). Максимальная длина 20 символов +/// Контактный телефон в международном формате, например "+7 (XXX) XXX-XX-XX". Максимальная длина 20 символов public record CounterpartyCreateUpdateDto( [Required][StringLength(150)] string FullName, [Required][StringLength(20)] string PassportNumber, diff --git a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs index bc8c16383..06e530136 100644 --- a/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/Counterparties/CounterpartyDto.cs @@ -3,12 +3,13 @@ namespace Agency.Application.Contracts.Counterparties; /// -/// DTO контрагента +/// DTO для получения информации о заявке +/// Используется в HTTP GET запросах для возврата данных клиенту /// -/// -/// -/// -/// +/// Уникальный идентификатор контрагента. Используется для ссылок на заявку, редактирования и удаления. Генерируется сервером при создании +/// Полное имя контрагента в формате "Фамилия Имя Отчество". Максимальная длина 150 символов +/// Номер паспорта в формате "XXXX XXXXXX" (серия и номер). Максимальная длина 20 символов +/// Контактный телефон в международном формате, например "+7 (XXX) XXX-XX-XX". Максимальная длина 20 символов public record CounterpartyDto( int Id, [Required][StringLength(150)] string FullName, diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs index 3f5f03015..1a2e45508 100644 --- a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateCreateUpdateDto.cs @@ -4,19 +4,20 @@ namespace Agency.Application.Contracts.RealEstates; /// -/// DTO для создания/обновления объекта недвижимости +/// DTO для создания нового объекта недвижимости или обновления существующего +/// Используется в HTTP POST (создание) и HTTP PUT (обновление) запросах /// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// +/// Тип объекта недвижимости: Apartment (0) - квартира, House (1) - дом, LandPlot (2) - участок, Commercial (3) - коммерческая, Garage (4) - гараж +/// Назначение объекта: Residential (0) - жилое, Commercial (1) - коммерческое, Industrial (2) - промышленное, Agricultural (3) - сельскохозяйственное +/// Кадастровый номер объекта в формате "XX:XX:XXXXXXX:XXXX". Максимальная длина 50 символов +/// Полный почтовый адрес объекта. Максимальная длина 200 символов +/// Общее количество этажей в здании. Указывается для домов и зданий, для участков может быть null +/// Общая площадь объекта в квадратных метрах. Допустимый диапазон от 1 до 10000 кв.м +/// Количество комнат. Указывается для квартир и домов, для участков и коммерческой недвижимости может быть null +/// Высота потолков в метрах. Указывается для помещений, для участков может быть null +/// Этаж расположения. Указывается для квартир и помещений в многоэтажных зданиях +/// Флаг наличия обременений: true - есть обременения (ипотека, арест, аренда), false - нет обременений +/// Описание обременений, если HasEncumbrances = true. Необязательное поле, максимальная длина 500 символов public record RealEstateCreateUpdateDto( RealEstateType Type, RealEstatePurpose Purpose, diff --git a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs index 7a1eb6afe..a0a711841 100644 --- a/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs +++ b/RealEstateAgency/Agency.Application.Contracts/RealEstates/RealEstateDto.cs @@ -4,20 +4,21 @@ namespace Agency.Application.Contracts.RealEstates; /// -/// DTO объекта недвижимости +/// DTO для получения информации об объекте недвижимости +/// Используется в HTTP GET запросах для возврата данных клиенту /// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// +/// Уникальный идентификатор объекта недвижимости. Генерируется сервером при создании +/// Тип объекта недвижимости +/// Назначение объекта +/// Кадастровый номер объекта +/// Полный почтовый адрес объекта +/// Общее количество этажей в здании +/// Общая площадь объекта в квадратных метрах +/// Количество комнат +/// Высота потолков в метрах +/// Этаж расположения +/// Флаг наличия обременений +/// Описание обременений public record RealEstateDto( int Id, RealEstateType Type,