From ada8f21153d8cf3c7e38e1fdfe271a33931f241b Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:40:33 +0400 Subject: [PATCH 01/10] add library models --- Library/Data/DataSeeder.cs | 84 +++++++++++++++++++++++++++++++++++ Library/Models/Book.cs | 38 ++++++++++++++++ Library/Models/BookIssue.cs | 33 ++++++++++++++ Library/Models/EditionType.cs | 11 +++++ Library/Models/Publisher.cs | 11 +++++ Library/Models/Reader.cs | 23 ++++++++++ 6 files changed, 200 insertions(+) create mode 100644 Library/Data/DataSeeder.cs create mode 100644 Library/Models/Book.cs create mode 100644 Library/Models/BookIssue.cs create mode 100644 Library/Models/EditionType.cs create mode 100644 Library/Models/Publisher.cs create mode 100644 Library/Models/Reader.cs diff --git a/Library/Data/DataSeeder.cs b/Library/Data/DataSeeder.cs new file mode 100644 index 000000000..1717ce8b6 --- /dev/null +++ b/Library/Data/DataSeeder.cs @@ -0,0 +1,84 @@ +using Library.Domain.Models; + +namespace Library.Domain.Data; + +/// Класс, содержащий заранее подготовленные тестовые данные для доменной модели библиотеки +public class DataSeeder +{ + /// Список видов изданий + public List EditionTypes { get; } = + [ + new EditionType { Id = 1, Name = "Монография" }, + new EditionType { Id = 2, Name = "Методическое пособие" }, + new EditionType { Id = 3, Name = "Энциклопедия" }, + new EditionType { Id = 4, Name = "Биография" }, + new EditionType { Id = 5, Name = "Фэнтези" }, + new EditionType { Id = 6, Name = "Техническая литература" }, + new EditionType { Id = 7, Name = "Публицистика" }, + new EditionType { Id = 8, Name = "Поэзия" }, + new EditionType { Id = 9, Name = "Психология" }, + new EditionType { Id = 10, Name = "Бизнес-литература" }, + ]; + + /// Список издательств + public List Publishers { get; } = + [ + new Publisher { Id = 1, Name = "Бином" }, + new Publisher { Id = 2, Name = "Инфра-М" }, + new Publisher { Id = 3, Name = "Юрайт" }, + new Publisher { Id = 4, Name = "ДМК Пресс" }, + new Publisher { Id = 5, Name = "Лань" }, + new Publisher { Id = 6, Name = "Альпина Паблишер" }, + new Publisher { Id = 7, Name = "МИФ" }, + new Publisher { Id = 8, Name = "Вильямс" }, + new Publisher { Id = 9, Name = "Самокат" }, + new Publisher { Id = 10, Name = "Энергия" }, + ]; + + /// Список книг с заполненными ссылками на издательства и виды изданий + public List Books { get; } = + [ + new Book { Id = 1, InventoryNumber = "BK-101", AlphabetCode = "И-101", Authors = "И. Ньютон", Title = "Математические начала", EditionTypeId = 1, PublisherId = 5, Year = 1687 }, + new Book { Id = 2, InventoryNumber = "BK-102", AlphabetCode = "Т-210", Authors = "А. Тьюринг", Title = "Вычислительные машины", EditionTypeId = 6, PublisherId = 4, Year = 1936 }, + new Book { Id = 3, InventoryNumber = "BK-103", AlphabetCode = "К-310", Authors = "И. Кант", Title = "Критика чистого разума", EditionTypeId = 7, PublisherId = 6, Year = 1781 }, + new Book { Id = 4, InventoryNumber = "BK-104", AlphabetCode = "Р-410", Authors = "Д. Роулинг", Title = "Тайная комната", EditionTypeId = 5, PublisherId = 9, Year = 1998 }, + new Book { Id = 5, InventoryNumber = "BK-105", AlphabetCode = "М-510", Authors = "М. Портер", Title = "Конкурентная стратегия", EditionTypeId = 10, PublisherId = 7, Year = 1980 }, + new Book { Id = 6, InventoryNumber = "BK-106", AlphabetCode = "С-610", Authors = "К. Саган", Title = "Космос", EditionTypeId = 3, PublisherId = 1, Year = 1980 }, + new Book { Id = 7, InventoryNumber = "BK-107", AlphabetCode = "Ф-710", Authors = "З. Фрейд", Title = "Толкование сновидений", EditionTypeId = 9, PublisherId = 6, Year = 1899 }, + new Book { Id = 8, InventoryNumber = "BK-108", AlphabetCode = "Л-810", Authors = "С. Лем", Title = "Солярис", EditionTypeId = 5, PublisherId = 2, Year = 1961 }, + new Book { Id = 9, InventoryNumber = "BK-109", AlphabetCode = "Х-910", Authors = "Ю. Харари", Title = "Sapiens", EditionTypeId = 4, PublisherId = 6, Year = 2011 }, + new Book { Id = 10, InventoryNumber = "BK-110", AlphabetCode = "Г-999", Authors = "А. Гауди", Title = "Архитектура форм", EditionTypeId = 1, PublisherId = 10, Year = 1925 }, + ]; + + /// Список читателей библиотеки, включающий персональные данные и дату регистрации + public List Readers { get; } = + [ + new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateTime.UtcNow.AddYears(-3) }, + new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateTime.UtcNow.AddYears(-2) }, + new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateTime.UtcNow.AddMonths(-18) }, + new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateTime.UtcNow.AddMonths(-12) }, + new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateTime.UtcNow.AddMonths(-10) }, + new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateTime.UtcNow.AddMonths(-8) }, + new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateTime.UtcNow.AddMonths(-6) }, + new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateTime.UtcNow.AddMonths(-5) }, + new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateTime.UtcNow.AddMonths(-4) }, + new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateTime.UtcNow.AddMonths(-2) }, + ]; + + /// Список фактов выдачи книг + public List BookIssues { get; } = + [ + new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-15), Days = 30, ReturnDate = null }, + new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-200), Days = 60, ReturnDate = DateTime.UtcNow.AddDays(-140) }, + new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-40), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-20) }, + new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-7), Days = 10, ReturnDate = null }, + new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = DateTime.UtcNow.AddDays(-300), Days = 21, ReturnDate = DateTime.UtcNow.AddDays(-260) }, + new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = DateTime.UtcNow.AddDays(-50), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-30) }, + new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = DateTime.UtcNow.AddDays(-3), Days = 7, ReturnDate = null }, + new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = DateTime.UtcNow.AddDays(-120), Days = 30, ReturnDate = DateTime.UtcNow.AddDays(-90) }, + new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = DateTime.UtcNow.AddDays(-60), Days = 20, ReturnDate = DateTime.UtcNow.AddDays(-35) }, + new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = DateTime.UtcNow.AddDays(-25), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-5) }, + new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = DateTime.UtcNow.AddDays(-5), Days = 10, ReturnDate = null }, + new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = DateTime.UtcNow.AddDays(-90), Days = 30, ReturnDate = DateTime.UtcNow.AddDays(-60) } + ]; +} diff --git a/Library/Models/Book.cs b/Library/Models/Book.cs new file mode 100644 index 000000000..d9ee9e402 --- /dev/null +++ b/Library/Models/Book.cs @@ -0,0 +1,38 @@ +namespace Library.Domain.Models; + +/// Сущность книги, содержащая сведения из каталога библиотеки +public class Book +{ + /// Уникальный идентификатор + public required int Id { get; set; } + + /// Инвентарный номер + public required string InventoryNumber { get; set; } + + /// Шифр в алфавитном каталоге + public required string AlphabetCode { get; set; } + + /// Инициалы и фамилии авторов + public string? Authors { get; set; } + + /// Название + public required string Title { get; set; } + + /// Идентификатор вида издания + public required int EditionTypeId { get; set; } + + /// Вид издания + public EditionType? EditionType { get; set; } + + /// Идентификатор издательства + public required int PublisherId { get; set; } + + /// Издательство + public Publisher? Publisher { get; set; } + + /// Год издания + public int Year { get; set; } + + /// Записи о выдаче книги + public ICollection Issues { get; set; } = []; +} \ No newline at end of file diff --git a/Library/Models/BookIssue.cs b/Library/Models/BookIssue.cs new file mode 100644 index 000000000..a029578e1 --- /dev/null +++ b/Library/Models/BookIssue.cs @@ -0,0 +1,33 @@ +namespace Library.Domain.Models; + +/// Сущность выдачи книги с данными сроков и возвратов +public class BookIssue +{ + /// Уникальный идентификатор + public required int Id { get; set; } + + /// Идентификатор книги + public required int BookId { get; set; } + + /// Выданная книга + public Book? Book { get; set; } + + /// Идентификатор читателя + public required int ReaderId { get; set; } + + /// Читатель, кому выдана книга + public Reader? Reader { get; set; } + + /// Дата выдачи книги + public required DateTime IssueDate { get; set; } + + /// Количество дней, на которое выдана книга + public required int Days { get; set; } + + /// Дата возврата книги + public DateTime? ReturnDate { get; set; } + + /// Флаг просрочки срока возврата + public bool IsOverdue => + ReturnDate == null && DateTime.UtcNow.Date > IssueDate.Date.AddDays(Days); +} \ No newline at end of file diff --git a/Library/Models/EditionType.cs b/Library/Models/EditionType.cs new file mode 100644 index 000000000..13eed072d --- /dev/null +++ b/Library/Models/EditionType.cs @@ -0,0 +1,11 @@ +namespace Library.Domain.Models; + +/// Справочник видов издания +public class EditionType +{ + /// Уникальный идентификатор + public required int Id { get; set; } + + /// Наименование вида издания + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Models/Publisher.cs b/Library/Models/Publisher.cs new file mode 100644 index 000000000..2ee46c961 --- /dev/null +++ b/Library/Models/Publisher.cs @@ -0,0 +1,11 @@ +namespace Library.Domain.Models; + +/// Справочник издательств, к которым относятся книги +public class Publisher +{ + /// Уникальный идентификатор + public required int Id { get; set; } + + /// Наименование издательства + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Models/Reader.cs b/Library/Models/Reader.cs new file mode 100644 index 000000000..01a9b599c --- /dev/null +++ b/Library/Models/Reader.cs @@ -0,0 +1,23 @@ +namespace Library.Domain.Models; + +/// Сущность читателя библиотеки с персональными данными и историей выдач +public class Reader +{ + /// Уникальный идентификатор + public required int Id { get; set; } + + /// ФИО читателя + public required string FullName { get; set; } + + /// Адрес читателя + public string? Address { get; set; } + + /// Телефон читателя + public required string Phone { get; set; } + + /// Дата регистрации читателя + public DateTime? RegistrationDate { get; set; } + + /// Выданные читателю книги + public ICollection BookIssues { get; set; } = []; +} \ No newline at end of file From d5f523964cbe9d717c18f56a0286611fdc795a0d Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:19:52 +0400 Subject: [PATCH 02/10] add tests for library --- .gitignore | 3 + .../{ => Library.Domain}/Data/DataSeeder.cs | 19 ++- Library/Library.Domain/Library.Domain.csproj | 9 ++ Library/{ => Library.Domain}/Models/Book.cs | 26 +++- .../{ => Library.Domain}/Models/BookIssue.cs | 26 +++- .../Models/EditionType.cs | 8 +- .../{ => Library.Domain}/Models/Publisher.cs | 6 + Library/{ => Library.Domain}/Models/Reader.cs | 24 ++- Library/Library.Tests/Library.Tests.csproj | 27 ++++ Library/Library.Tests/LibraryTests.cs | 145 ++++++++++++++++++ enterprise-development.sln | 36 +++++ 11 files changed, 315 insertions(+), 14 deletions(-) rename Library/{ => Library.Domain}/Data/DataSeeder.cs (93%) create mode 100644 Library/Library.Domain/Library.Domain.csproj rename Library/{ => Library.Domain}/Models/Book.cs (65%) rename Library/{ => Library.Domain}/Models/BookIssue.cs (55%) rename Library/{ => Library.Domain}/Models/EditionType.cs (52%) rename Library/{ => Library.Domain}/Models/Publisher.cs (75%) rename Library/{ => Library.Domain}/Models/Reader.cs (56%) create mode 100644 Library/Library.Tests/Library.Tests.csproj create mode 100644 Library/Library.Tests/LibraryTests.cs create mode 100644 enterprise-development.sln diff --git a/.gitignore b/.gitignore index ce892922f..6ecb5f9bd 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +**/bin +**/obj \ No newline at end of file diff --git a/Library/Data/DataSeeder.cs b/Library/Library.Domain/Data/DataSeeder.cs similarity index 93% rename from Library/Data/DataSeeder.cs rename to Library/Library.Domain/Data/DataSeeder.cs index 1717ce8b6..82ea79bdf 100644 --- a/Library/Data/DataSeeder.cs +++ b/Library/Library.Domain/Data/DataSeeder.cs @@ -1,11 +1,14 @@ using Library.Domain.Models; namespace Library.Domain.Data; - +/// /// Класс, содержащий заранее подготовленные тестовые данные для доменной модели библиотеки +/// public class DataSeeder { + /// /// Список видов изданий + /// public List EditionTypes { get; } = [ new EditionType { Id = 1, Name = "Монография" }, @@ -20,7 +23,9 @@ public class DataSeeder new EditionType { Id = 10, Name = "Бизнес-литература" }, ]; + /// /// Список издательств + /// public List Publishers { get; } = [ new Publisher { Id = 1, Name = "Бином" }, @@ -35,7 +40,9 @@ public class DataSeeder new Publisher { Id = 10, Name = "Энергия" }, ]; + /// /// Список книг с заполненными ссылками на издательства и виды изданий + /// public List Books { get; } = [ new Book { Id = 1, InventoryNumber = "BK-101", AlphabetCode = "И-101", Authors = "И. Ньютон", Title = "Математические начала", EditionTypeId = 1, PublisherId = 5, Year = 1687 }, @@ -50,7 +57,9 @@ public class DataSeeder new Book { Id = 10, InventoryNumber = "BK-110", AlphabetCode = "Г-999", Authors = "А. Гауди", Title = "Архитектура форм", EditionTypeId = 1, PublisherId = 10, Year = 1925 }, ]; - /// Список читателей библиотеки, включающий персональные данные и дату регистрации + /// + /// Список читателей библиотеки с их данными + /// public List Readers { get; } = [ new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateTime.UtcNow.AddYears(-3) }, @@ -65,10 +74,12 @@ public class DataSeeder new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateTime.UtcNow.AddMonths(-2) }, ]; - /// Список фактов выдачи книг + /// + /// Список выдачи книг + /// public List BookIssues { get; } = [ - new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-15), Days = 30, ReturnDate = null }, + new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-15), Days = 30, ReturnDate = null }, new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-200), Days = 60, ReturnDate = DateTime.UtcNow.AddDays(-140) }, new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-40), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-20) }, new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-7), Days = 10, ReturnDate = null }, diff --git a/Library/Library.Domain/Library.Domain.csproj b/Library/Library.Domain/Library.Domain.csproj new file mode 100644 index 000000000..bb23fb7d6 --- /dev/null +++ b/Library/Library.Domain/Library.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Library/Models/Book.cs b/Library/Library.Domain/Models/Book.cs similarity index 65% rename from Library/Models/Book.cs rename to Library/Library.Domain/Models/Book.cs index d9ee9e402..366828c25 100644 --- a/Library/Models/Book.cs +++ b/Library/Library.Domain/Models/Book.cs @@ -1,38 +1,62 @@ namespace Library.Domain.Models; -/// Сущность книги, содержащая сведения из каталога библиотеки +/// +/// Сущность книги из каталога библиотеки +/// public class Book { + /// /// Уникальный идентификатор + /// public required int Id { get; set; } + /// /// Инвентарный номер + /// public required string InventoryNumber { get; set; } + /// /// Шифр в алфавитном каталоге + /// public required string AlphabetCode { get; set; } + /// /// Инициалы и фамилии авторов + /// public string? Authors { get; set; } + /// /// Название + /// public required string Title { get; set; } + /// /// Идентификатор вида издания + /// public required int EditionTypeId { get; set; } + /// /// Вид издания + /// public EditionType? EditionType { get; set; } + /// /// Идентификатор издательства + /// public required int PublisherId { get; set; } + /// /// Издательство + /// public Publisher? Publisher { get; set; } + /// /// Год издания + /// public int Year { get; set; } + /// /// Записи о выдаче книги + /// public ICollection Issues { get; set; } = []; } \ No newline at end of file diff --git a/Library/Models/BookIssue.cs b/Library/Library.Domain/Models/BookIssue.cs similarity index 55% rename from Library/Models/BookIssue.cs rename to Library/Library.Domain/Models/BookIssue.cs index a029578e1..2f6d2a97d 100644 --- a/Library/Models/BookIssue.cs +++ b/Library/Library.Domain/Models/BookIssue.cs @@ -1,33 +1,53 @@ namespace Library.Domain.Models; -/// Сущность выдачи книги с данными сроков и возвратов +/// +/// Сущность выдачи книги читателю с указанием сроков и состояния возврата +/// public class BookIssue { + /// /// Уникальный идентификатор + /// public required int Id { get; set; } + /// /// Идентификатор книги + /// public required int BookId { get; set; } + /// /// Выданная книга + /// public Book? Book { get; set; } + /// /// Идентификатор читателя + /// public required int ReaderId { get; set; } - /// Читатель, кому выдана книга + /// + /// Читатель, которому была выдана книга + /// public Reader? Reader { get; set; } + /// /// Дата выдачи книги + /// public required DateTime IssueDate { get; set; } + /// /// Количество дней, на которое выдана книга + /// public required int Days { get; set; } + /// /// Дата возврата книги + /// public DateTime? ReturnDate { get; set; } - /// Флаг просрочки срока возврата + /// + /// Флаг просрочки срока возврата книги + /// public bool IsOverdue => ReturnDate == null && DateTime.UtcNow.Date > IssueDate.Date.AddDays(Days); } \ No newline at end of file diff --git a/Library/Models/EditionType.cs b/Library/Library.Domain/Models/EditionType.cs similarity index 52% rename from Library/Models/EditionType.cs rename to Library/Library.Domain/Models/EditionType.cs index 13eed072d..4eba925ba 100644 --- a/Library/Models/EditionType.cs +++ b/Library/Library.Domain/Models/EditionType.cs @@ -1,11 +1,17 @@ namespace Library.Domain.Models; -/// Справочник видов издания +/// +/// Справочник видов издания, используемый для классификации книг +/// public class EditionType { + /// /// Уникальный идентификатор + /// public required int Id { get; set; } + /// /// Наименование вида издания + /// public required string Name { get; set; } } \ No newline at end of file diff --git a/Library/Models/Publisher.cs b/Library/Library.Domain/Models/Publisher.cs similarity index 75% rename from Library/Models/Publisher.cs rename to Library/Library.Domain/Models/Publisher.cs index 2ee46c961..5c1fedbac 100644 --- a/Library/Models/Publisher.cs +++ b/Library/Library.Domain/Models/Publisher.cs @@ -1,11 +1,17 @@ namespace Library.Domain.Models; +/// /// Справочник издательств, к которым относятся книги +/// public class Publisher { + /// /// Уникальный идентификатор + /// public required int Id { get; set; } + /// /// Наименование издательства + /// public required string Name { get; set; } } \ No newline at end of file diff --git a/Library/Models/Reader.cs b/Library/Library.Domain/Models/Reader.cs similarity index 56% rename from Library/Models/Reader.cs rename to Library/Library.Domain/Models/Reader.cs index 01a9b599c..177c31a4a 100644 --- a/Library/Models/Reader.cs +++ b/Library/Library.Domain/Models/Reader.cs @@ -1,23 +1,37 @@ namespace Library.Domain.Models; +/// /// Сущность читателя библиотеки с персональными данными и историей выдач +/// public class Reader { + /// /// Уникальный идентификатор + /// public required int Id { get; set; } - /// ФИО читателя + /// + /// ФИО + /// public required string FullName { get; set; } - /// Адрес читателя + /// + /// Адрес + /// public string? Address { get; set; } - /// Телефон читателя + /// + /// Телефон + /// public required string Phone { get; set; } - /// Дата регистрации читателя + /// + /// Дата регистрации + /// public DateTime? RegistrationDate { get; set; } - /// Выданные читателю книги + /// + /// Выданные книги + /// public ICollection BookIssues { get; set; } = []; } \ No newline at end of file diff --git a/Library/Library.Tests/Library.Tests.csproj b/Library/Library.Tests/Library.Tests.csproj new file mode 100644 index 000000000..6f0ecb3b5 --- /dev/null +++ b/Library/Library.Tests/Library.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs new file mode 100644 index 000000000..9daaa793b --- /dev/null +++ b/Library/Library.Tests/LibraryTests.cs @@ -0,0 +1,145 @@ +using Library.Domain.Data; + +namespace Library.Tests; + +/// +/// Набор unit тестов для тестирования доменной области +/// +public class LibraryTests(DataSeeder dataSeeder) : IClassFixture +{ + /// + /// Проверяет что активные выдачи сортируются по названию книги и возвращают ожидаемый порядок идентификаторов книг + /// + [Fact] + public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() + { + var actualBookIds = dataSeeder.BookIssues + .Where(bi => bi.ReturnDate == null) + .Join(dataSeeder.Books, + bi => bi.BookId, + b => b.Id, + (bi, b) => new { bi, b }) + .OrderBy(x => x.b.Title) + .Select(x => x.b.Id) + .ToList(); + + var expectedBookIds = new List { 1, 1, 4, 7 }; + + Assert.Equal(expectedBookIds, actualBookIds); + } + + /// + /// Топ 5 читателей за последний год по количеству выдач и сравнивает по Id и по количествам + /// + [Fact] + public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() + { + var periodStart = DateTime.UtcNow.AddYears(-1); + var periodEnd = DateTime.UtcNow; + + var topReaders = dataSeeder.BookIssues + .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) + .GroupBy(bi => bi.ReaderId) + .Select(g => new { ReaderId = g.Key, Count = g.Count() }) + .Join(dataSeeder.Readers, g => g.ReaderId, r => r.Id, (g, r) => new { r.Id, r.FullName, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.FullName) + .Take(5) + .ToList(); + + var actualIds = topReaders.Select(x => x.Id).ToList(); + var actualCounts = topReaders.Select(x => x.Count).ToList(); + + var expectedIds = new List { 2, 1, 3, 8, 10 }; + var expectedCounts = new List { 2, 2, 1, 1, 1 }; + + Assert.Equal(expectedIds, actualIds); + Assert.Equal(expectedCounts, actualCounts); + } + + /// + /// Читатели, у которых есть выдачи с максимальным количеством дней, и сортирует их по ФИО + /// + [Fact] + public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() + { + var maxDays = dataSeeder.BookIssues.Max(bi => bi.Days); + + var readersWithMaxDays = dataSeeder.BookIssues + .Where(bi => bi.Days == maxDays) + .Select(bi => bi.ReaderId) + .Distinct() + .Join(dataSeeder.Readers, id => id, r => r.Id, (id, r) => new { r.Id, r.FullName }) + .OrderBy(r => r.FullName) + .Select(r => r.Id) + .ToList(); + + var expectedId = 1; + var expectedDays = 60; + + Assert.Single(readersWithMaxDays); + Assert.Equal(expectedDays, maxDays); + Assert.Equal(expectedId, readersWithMaxDays[0]); + } + + /// + /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам + /// + [Fact] + public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() + { + var lastYearStart = DateTime.UtcNow.AddYears(-1); + var lastYearEnd = DateTime.UtcNow; + + var topPublishers = dataSeeder.BookIssues + .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) + .Join(dataSeeder.Books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) + .GroupBy(pid => pid) + .Select(g => new { PublisherId = g.Key, Count = g.Count() }) + .Join(dataSeeder.Publishers, g => g.PublisherId, p => p.Id, (g, p) => new { p.Id, p.Name, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Name) + .Take(5) + .ToList(); + + var actualPublisherIds = topPublishers.Select(x => x.Id).ToList(); + var actualCounts = topPublishers.Select(x => x.Count).ToList(); + + var expectedPublisherIds = new List { 6, 4, 5, 1, 2 }; + var expectedCounts = new List { 3, 2, 2, 1, 1 }; + + Assert.Equal(expectedPublisherIds, actualPublisherIds); + Assert.Equal(expectedCounts, actualCounts); + } + + /// + /// Топ 5 наименее популярных книг за последний год сравнение по Id и количествам + /// + [Fact] + public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() + { + var lastYearStart = DateTime.UtcNow.AddYears(-1); + var lastYearEnd = DateTime.UtcNow; + + var bookCounts = dataSeeder.Books + .GroupJoin( + dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), + b => b.Id, + bi => bi.BookId, + (b, issues) => new { Book = b, Count = issues.Count() } + ) + .OrderBy(x => x.Count) + .ThenBy(x => x.Book.Title) + .Take(5) + .ToList(); + + var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); + var actualCounts = bookCounts.Select(x => x.Count).ToList(); + + var expectedBookIds = new List { 10, 5, 6, 3, 8 }; + var expectedCounts = new List { 1, 1, 1, 1, 1 }; + + Assert.Equal(expectedBookIds, actualBookIds); + Assert.Equal(expectedCounts, actualCounts); + } +} \ No newline at end of file diff --git a/enterprise-development.sln b/enterprise-development.sln new file mode 100644 index 000000000..6550f8d0c --- /dev/null +++ b/enterprise-development.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{50A17DA1-3556-4046-CEDC-33EB466D9C32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Domain", "Library\Library.Domain\Library.Domain.csproj", "{39E55976-5424-CEC7-0978-4D789AC54AC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Tests", "Library\Library.Tests\Library.Tests.csproj", "{60F294C7-3D77-63D8-5514-C4AB9BB913BD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {39E55976-5424-CEC7-0978-4D789AC54AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39E55976-5424-CEC7-0978-4D789AC54AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39E55976-5424-CEC7-0978-4D789AC54AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39E55976-5424-CEC7-0978-4D789AC54AC6}.Release|Any CPU.Build.0 = Release|Any CPU + {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {39E55976-5424-CEC7-0978-4D789AC54AC6} = {50A17DA1-3556-4046-CEDC-33EB466D9C32} + {60F294C7-3D77-63D8-5514-C4AB9BB913BD} = {50A17DA1-3556-4046-CEDC-33EB466D9C32} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B2802E6A-D42A-4C1D-8949-77992D768DF6} + EndGlobalSection +EndGlobal From f76f3c48dc4ac716a8f7dcfb3c601f07feddea24 Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:26:59 +0400 Subject: [PATCH 03/10] add github action tests --- .github/workflows/tests.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..c56adb52b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,29 @@ +name: Run .NET Tests + +on: + push: + branches: lab_1 + pull_request: + branches: lab_1 + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore enterprise-development.sln + + - name: Build + run: dotnet build enterprise-development.sln --configuration Release --no-restore + + - name: Run tests + run: dotnet test enterprise-development.sln --configuration Release --no-build \ No newline at end of file From b370ceb6bcb2b83aa22297ff205ffea3c0d3db1e Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:49:12 +0400 Subject: [PATCH 04/10] fix bottom5 test --- Library/Library.Tests/LibraryTests.cs | 46 ++++++++++++--------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 9daaa793b..3becaf6f9 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -116,30 +116,26 @@ public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() /// Топ 5 наименее популярных книг за последний год сравнение по Id и количествам /// [Fact] - public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() - { - var lastYearStart = DateTime.UtcNow.AddYears(-1); - var lastYearEnd = DateTime.UtcNow; - - var bookCounts = dataSeeder.Books - .GroupJoin( - dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), - b => b.Id, - bi => bi.BookId, - (b, issues) => new { Book = b, Count = issues.Count() } - ) - .OrderBy(x => x.Count) - .ThenBy(x => x.Book.Title) - .Take(5) - .ToList(); - - var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); - var actualCounts = bookCounts.Select(x => x.Count).ToList(); - - var expectedBookIds = new List { 10, 5, 6, 3, 8 }; - var expectedCounts = new List { 1, 1, 1, 1, 1 }; - - Assert.Equal(expectedBookIds, actualBookIds); - Assert.Equal(expectedCounts, actualCounts); + public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() +{ + var lastYearStart = DateTime.UtcNow.AddYears(-1); + var lastYearEnd = DateTime.UtcNow; + + var bookCounts = dataSeeder.Books + .GroupJoin( + dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), + b => b.Id, + bi => bi.BookId, + (b, issues) => new { Book = b, Count = issues.Count() } + ) + .OrderBy(x => x.Count) + .ThenBy(x => x.Book.Title, StringComparer.Ordinal) // Явно указываем Ordinal сортировку + .Take(5) + .ToList(); + + var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); + var expectedBookIds = new List { 9, 10, 5, 6, 3 }; // Исправь ожидаемый порядок! + + Assert.Equal(expectedBookIds, actualBookIds); } } \ No newline at end of file From c9b8bae4e66ec34722b3642d1bf6fa835fbfc5b8 Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:58:44 +0400 Subject: [PATCH 05/10] fix tests --- Library/Library.Tests/LibraryTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 3becaf6f9..028fc70f5 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -129,13 +129,11 @@ public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() (b, issues) => new { Book = b, Count = issues.Count() } ) .OrderBy(x => x.Count) - .ThenBy(x => x.Book.Title, StringComparer.Ordinal) // Явно указываем Ordinal сортировку - .Take(5) + .ThenBy(x => x.Book.Title, StringComparer.Ordinal) .ToList(); var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); - var expectedBookIds = new List { 9, 10, 5, 6, 3 }; // Исправь ожидаемый порядок! - + var expectedBookIds = new List { 9, 10, 5, 6, 3 }; Assert.Equal(expectedBookIds, actualBookIds); } } \ No newline at end of file From f7e7b933094b16ea359e4f6669d71dc18866261a Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:42:04 +0400 Subject: [PATCH 06/10] correcting comments, replacing utc, fixing tests --- .github/workflows/tests.yml | 6 +- enterprise-development.sln => Library.sln | 0 .../Abstractions/ITimeProvider.cs | 9 ++ .../Abstractions/SystemTimeProvider.cs | 12 +++ Library/Library.Domain/Data/DataSeeder.cs | 81 +++++++------- Library/Library.Domain/Models/Book.cs | 2 +- Library/Library.Domain/Models/BookIssue.cs | 16 ++- Library/Library.Domain/Models/Reader.cs | 2 +- Library/Library.Tests/FakeTimeProvider.cs | 22 ++++ Library/Library.Tests/LibraryTests.cs | 100 +++++++++++------- 10 files changed, 160 insertions(+), 90 deletions(-) rename enterprise-development.sln => Library.sln (100%) create mode 100644 Library/Library.Domain/Abstractions/ITimeProvider.cs create mode 100644 Library/Library.Domain/Abstractions/SystemTimeProvider.cs create mode 100644 Library/Library.Tests/FakeTimeProvider.cs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c56adb52b..6bc513941 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,10 +20,10 @@ jobs: dotnet-version: "8.0.x" - name: Restore dependencies - run: dotnet restore enterprise-development.sln + run: dotnet restore Library.sln - name: Build - run: dotnet build enterprise-development.sln --configuration Release --no-restore + run: dotnet build Library.sln --configuration Release --no-restore - name: Run tests - run: dotnet test enterprise-development.sln --configuration Release --no-build \ No newline at end of file + run: dotnet test Library.sln --configuration Release --no-build \ No newline at end of file diff --git a/enterprise-development.sln b/Library.sln similarity index 100% rename from enterprise-development.sln rename to Library.sln diff --git a/Library/Library.Domain/Abstractions/ITimeProvider.cs b/Library/Library.Domain/Abstractions/ITimeProvider.cs new file mode 100644 index 000000000..56d5e29cb --- /dev/null +++ b/Library/Library.Domain/Abstractions/ITimeProvider.cs @@ -0,0 +1,9 @@ +namespace Library.Domain.Abstractions; + +/// +/// Интерфейс поставщика времени для детерминированного тестирования +/// +public interface ITimeProvider +{ + public DateTime Now { get; } +} diff --git a/Library/Library.Domain/Abstractions/SystemTimeProvider.cs b/Library/Library.Domain/Abstractions/SystemTimeProvider.cs new file mode 100644 index 000000000..ace01f126 --- /dev/null +++ b/Library/Library.Domain/Abstractions/SystemTimeProvider.cs @@ -0,0 +1,12 @@ +namespace Library.Domain.Abstractions; + +/// +/// Поставщик времени, возвращающий текущее системное время +/// +public class SystemTimeProvider : ITimeProvider +{ + /// + /// Возвращает текущее системное локальное время. + /// + public DateTime Now => DateTime.Now; +} diff --git a/Library/Library.Domain/Data/DataSeeder.cs b/Library/Library.Domain/Data/DataSeeder.cs index 82ea79bdf..51d5b29c7 100644 --- a/Library/Library.Domain/Data/DataSeeder.cs +++ b/Library/Library.Domain/Data/DataSeeder.cs @@ -1,14 +1,23 @@ using Library.Domain.Models; +using Library.Domain.Abstractions; namespace Library.Domain.Data; + /// -/// Класс, содержащий заранее подготовленные тестовые данные для доменной модели библиотеки +/// Класс, содержащий заранее подготовленные тестовые данные /// public class DataSeeder { + private readonly DateTime _now; + /// - /// Список видов изданий + /// Создаёт экземпляр DataSeeder с заданным провайдером времени. /// + public DataSeeder(ITimeProvider timeProvider) + { + _now = timeProvider.Now; + } + public List EditionTypes { get; } = [ new EditionType { Id = 1, Name = "Монография" }, @@ -23,9 +32,6 @@ public class DataSeeder new EditionType { Id = 10, Name = "Бизнес-литература" }, ]; - /// - /// Список издательств - /// public List Publishers { get; } = [ new Publisher { Id = 1, Name = "Бином" }, @@ -40,9 +46,6 @@ public class DataSeeder new Publisher { Id = 10, Name = "Энергия" }, ]; - /// - /// Список книг с заполненными ссылками на издательства и виды изданий - /// public List Books { get; } = [ new Book { Id = 1, InventoryNumber = "BK-101", AlphabetCode = "И-101", Authors = "И. Ньютон", Title = "Математические начала", EditionTypeId = 1, PublisherId = 5, Year = 1687 }, @@ -57,39 +60,33 @@ public class DataSeeder new Book { Id = 10, InventoryNumber = "BK-110", AlphabetCode = "Г-999", Authors = "А. Гауди", Title = "Архитектура форм", EditionTypeId = 1, PublisherId = 10, Year = 1925 }, ]; - /// - /// Список читателей библиотеки с их данными - /// - public List Readers { get; } = - [ - new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateTime.UtcNow.AddYears(-3) }, - new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateTime.UtcNow.AddYears(-2) }, - new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateTime.UtcNow.AddMonths(-18) }, - new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateTime.UtcNow.AddMonths(-12) }, - new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateTime.UtcNow.AddMonths(-10) }, - new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateTime.UtcNow.AddMonths(-8) }, - new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateTime.UtcNow.AddMonths(-6) }, - new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateTime.UtcNow.AddMonths(-5) }, - new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateTime.UtcNow.AddMonths(-4) }, - new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateTime.UtcNow.AddMonths(-2) }, - ]; + public List Readers => new() + { + new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-3)) }, + new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateOnly.FromDateTime(_now.AddYears(-2)) }, + new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-18)) }, + new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-12)) }, + new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-10)) }, + new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-8)) }, + new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-6)) }, + new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-5)) }, + new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-4)) }, + new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-2)) }, + }; - /// - /// Список выдачи книг - /// - public List BookIssues { get; } = - [ - new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-15), Days = 30, ReturnDate = null }, - new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = DateTime.UtcNow.AddDays(-200), Days = 60, ReturnDate = DateTime.UtcNow.AddDays(-140) }, - new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-40), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-20) }, - new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = DateTime.UtcNow.AddDays(-7), Days = 10, ReturnDate = null }, - new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = DateTime.UtcNow.AddDays(-300), Days = 21, ReturnDate = DateTime.UtcNow.AddDays(-260) }, - new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = DateTime.UtcNow.AddDays(-50), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-30) }, - new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = DateTime.UtcNow.AddDays(-3), Days = 7, ReturnDate = null }, - new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = DateTime.UtcNow.AddDays(-120), Days = 30, ReturnDate = DateTime.UtcNow.AddDays(-90) }, - new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = DateTime.UtcNow.AddDays(-60), Days = 20, ReturnDate = DateTime.UtcNow.AddDays(-35) }, - new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = DateTime.UtcNow.AddDays(-25), Days = 14, ReturnDate = DateTime.UtcNow.AddDays(-5) }, - new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = DateTime.UtcNow.AddDays(-5), Days = 10, ReturnDate = null }, - new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = DateTime.UtcNow.AddDays(-90), Days = 30, ReturnDate = DateTime.UtcNow.AddDays(-60) } - ]; + public List BookIssues => new() + { + new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = _now.AddDays(-15), Days = 30 }, + new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = _now.AddDays(-200), Days = 60 }, + new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = _now.AddDays(-40), Days = 14 }, + new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = _now.AddDays(-7), Days = 10 }, + new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = _now.AddDays(-300), Days = 21 }, + new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = _now.AddDays(-50), Days = 14 }, + new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = _now.AddDays(-3), Days = 7 }, + new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = _now.AddDays(-120), Days = 30 }, + new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = _now.AddDays(-60), Days = 20 }, + new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = _now.AddDays(-25), Days = 14 }, + new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = _now.AddDays(-5), Days = 10 }, + new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = _now.AddDays(-90), Days = 30 } + }; } diff --git a/Library/Library.Domain/Models/Book.cs b/Library/Library.Domain/Models/Book.cs index 366828c25..b599cdfaa 100644 --- a/Library/Library.Domain/Models/Book.cs +++ b/Library/Library.Domain/Models/Book.cs @@ -53,7 +53,7 @@ public class Book /// /// Год издания /// - public int Year { get; set; } + public required int Year { get; set; } /// /// Записи о выдаче книги diff --git a/Library/Library.Domain/Models/BookIssue.cs b/Library/Library.Domain/Models/BookIssue.cs index 2f6d2a97d..4145b0ba4 100644 --- a/Library/Library.Domain/Models/BookIssue.cs +++ b/Library/Library.Domain/Models/BookIssue.cs @@ -5,6 +5,8 @@ namespace Library.Domain.Models; /// public class BookIssue { + private readonly Library.Domain.Abstractions.ITimeProvider _timeProvider; + /// /// Уникальный идентификатор /// @@ -41,13 +43,21 @@ public class BookIssue public required int Days { get; set; } /// - /// Дата возврата книги + /// Дата возврата книги (IssueDate + Days) + /// + public DateTime? ReturnDate => IssueDate.AddDays(Days); + + /// + /// Конструктор с опциональным поставщиком времени /// - public DateTime? ReturnDate { get; set; } + public BookIssue(Library.Domain.Abstractions.ITimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? new Library.Domain.Abstractions.SystemTimeProvider(); + } /// /// Флаг просрочки срока возврата книги /// public bool IsOverdue => - ReturnDate == null && DateTime.UtcNow.Date > IssueDate.Date.AddDays(Days); + ReturnDate == null && _timeProvider.Now.Date > IssueDate.AddDays(Days).Date; } \ No newline at end of file diff --git a/Library/Library.Domain/Models/Reader.cs b/Library/Library.Domain/Models/Reader.cs index 177c31a4a..3e4c87e4d 100644 --- a/Library/Library.Domain/Models/Reader.cs +++ b/Library/Library.Domain/Models/Reader.cs @@ -28,7 +28,7 @@ public class Reader /// /// Дата регистрации /// - public DateTime? RegistrationDate { get; set; } + public DateOnly? RegistrationDate { get; set; } /// /// Выданные книги diff --git a/Library/Library.Tests/FakeTimeProvider.cs b/Library/Library.Tests/FakeTimeProvider.cs new file mode 100644 index 000000000..c69aa7ef4 --- /dev/null +++ b/Library/Library.Tests/FakeTimeProvider.cs @@ -0,0 +1,22 @@ +using Library.Domain.Abstractions; + +namespace Library.Tests; + +/// +/// Провайдер времени, возвращающий фиксированное значение +/// +public class FakeTimeProvider : ITimeProvider +{ + /// + /// Текущее время + /// + public DateTime Now { get; } + + /// + /// Создаёт экземпляр с заданным временем + /// + public FakeTimeProvider(DateTime now) + { + Now = now; + } +} diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 028fc70f5..9517c76ac 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -1,24 +1,42 @@ using Library.Domain.Data; +using Library.Domain.Abstractions; namespace Library.Tests; /// /// Набор unit тестов для тестирования доменной области /// -public class LibraryTests(DataSeeder dataSeeder) : IClassFixture +public class LibraryTests { + // Экземпляр DataSeeder для доступа к тестовым данным + private readonly DataSeeder _dataSeeder; + /// - /// Проверяет что активные выдачи сортируются по названию книги и возвращают ожидаемый порядок идентификаторов книг + // Фиксированная дата + /// + private readonly DateTime _fixedNow = new(2026, 2, 19); + + /// + /// Конструктор класса, создаёт DataSeeder с фиксированной датой + /// + public LibraryTests() + { + var fakeTime = new FakeTimeProvider(_fixedNow); + _dataSeeder = new DataSeeder(fakeTime); + } + + /// + /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам /// [Fact] public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() { - var actualBookIds = dataSeeder.BookIssues - .Where(bi => bi.ReturnDate == null) - .Join(dataSeeder.Books, - bi => bi.BookId, - b => b.Id, - (bi, b) => new { bi, b }) + var actualBookIds = _dataSeeder.BookIssues + .Where(bi => _fixedNow.Date < bi.ReturnDate) + .Join(_dataSeeder.Books, + bi => bi.BookId, + b => b.Id, + (bi, b) => new { bi, b }) .OrderBy(x => x.b.Title) .Select(x => x.b.Id) .ToList(); @@ -34,14 +52,14 @@ public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() [Fact] public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() { - var periodStart = DateTime.UtcNow.AddYears(-1); - var periodEnd = DateTime.UtcNow; + var periodStart = _fixedNow.AddYears(-1); + var periodEnd = _fixedNow; - var topReaders = dataSeeder.BookIssues + var topReaders = _dataSeeder.BookIssues .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) .GroupBy(bi => bi.ReaderId) .Select(g => new { ReaderId = g.Key, Count = g.Count() }) - .Join(dataSeeder.Readers, g => g.ReaderId, r => r.Id, (g, r) => new { r.Id, r.FullName, g.Count }) + .Join(_dataSeeder.Readers, g => g.ReaderId, r => r.Id, (g, r) => new { r.Id, r.FullName, g.Count }) .OrderByDescending(x => x.Count) .ThenBy(x => x.FullName) .Take(5) @@ -63,13 +81,13 @@ public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() [Fact] public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() { - var maxDays = dataSeeder.BookIssues.Max(bi => bi.Days); + var maxDays = _dataSeeder.BookIssues.Max(bi => bi.Days); - var readersWithMaxDays = dataSeeder.BookIssues + var readersWithMaxDays = _dataSeeder.BookIssues .Where(bi => bi.Days == maxDays) .Select(bi => bi.ReaderId) .Distinct() - .Join(dataSeeder.Readers, id => id, r => r.Id, (id, r) => new { r.Id, r.FullName }) + .Join(_dataSeeder.Readers, id => id, r => r.Id, (id, r) => new { r.Id, r.FullName }) .OrderBy(r => r.FullName) .Select(r => r.Id) .ToList(); @@ -88,15 +106,15 @@ public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() [Fact] public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() { - var lastYearStart = DateTime.UtcNow.AddYears(-1); - var lastYearEnd = DateTime.UtcNow; + var lastYearStart = _fixedNow.AddYears(-1); + var lastYearEnd = _fixedNow; - var topPublishers = dataSeeder.BookIssues + var topPublishers = _dataSeeder.BookIssues .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) - .Join(dataSeeder.Books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) + .Join(_dataSeeder.Books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) .GroupBy(pid => pid) .Select(g => new { PublisherId = g.Key, Count = g.Count() }) - .Join(dataSeeder.Publishers, g => g.PublisherId, p => p.Id, (g, p) => new { p.Id, p.Name, g.Count }) + .Join(_dataSeeder.Publishers, g => g.PublisherId, p => p.Id, (g, p) => new { p.Id, p.Name, g.Count }) .OrderByDescending(x => x.Count) .ThenBy(x => x.Name) .Take(5) @@ -116,24 +134,26 @@ public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() /// Топ 5 наименее популярных книг за последний год сравнение по Id и количествам /// [Fact] - public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() -{ - var lastYearStart = DateTime.UtcNow.AddYears(-1); - var lastYearEnd = DateTime.UtcNow; - - var bookCounts = dataSeeder.Books - .GroupJoin( - dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), - b => b.Id, - bi => bi.BookId, - (b, issues) => new { Book = b, Count = issues.Count() } - ) - .OrderBy(x => x.Count) - .ThenBy(x => x.Book.Title, StringComparer.Ordinal) - .ToList(); - - var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); - var expectedBookIds = new List { 9, 10, 5, 6, 3 }; - Assert.Equal(expectedBookIds, actualBookIds); + public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() + { + var lastYearStart = _fixedNow.AddYears(-1); + var lastYearEnd = _fixedNow; + + var bookCounts = _dataSeeder.Books + .GroupJoin( + _dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), + b => b.Id, + bi => bi.BookId, + (b, issues) => new { Book = b, Count = issues.Count() } + ) + .OrderBy(x => x.Count) + .ThenBy(x => x.Book.Title, StringComparer.Ordinal) + .Take(5) + .ToList(); + + var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); + var expectedBookIds = new List { 9, 10, 5, 6, 3 }; + + Assert.Equal(expectedBookIds, actualBookIds); } -} \ No newline at end of file +} From 6e9534eeb2482860aa54a5bf1146e364aa37859e Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:13:36 +0400 Subject: [PATCH 07/10] removed overengineering --- .github/workflows/tests.yml | 2 - .../Abstractions/ITimeProvider.cs | 9 --- .../Abstractions/SystemTimeProvider.cs | 12 ---- Library/Library.Domain/Data/DataSeeder.cs | 56 ++++++++----------- Library/Library.Domain/Models/BookIssue.cs | 14 +---- Library/Library.Tests/FakeTimeProvider.cs | 22 -------- Library/Library.Tests/LibraryTests.cs | 30 ++++------ 7 files changed, 37 insertions(+), 108 deletions(-) delete mode 100644 Library/Library.Domain/Abstractions/ITimeProvider.cs delete mode 100644 Library/Library.Domain/Abstractions/SystemTimeProvider.cs delete mode 100644 Library/Library.Tests/FakeTimeProvider.cs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6bc513941..6f2699892 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,7 @@ name: Run .NET Tests on: push: - branches: lab_1 pull_request: - branches: lab_1 jobs: build-and-test: diff --git a/Library/Library.Domain/Abstractions/ITimeProvider.cs b/Library/Library.Domain/Abstractions/ITimeProvider.cs deleted file mode 100644 index 56d5e29cb..000000000 --- a/Library/Library.Domain/Abstractions/ITimeProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Library.Domain.Abstractions; - -/// -/// Интерфейс поставщика времени для детерминированного тестирования -/// -public interface ITimeProvider -{ - public DateTime Now { get; } -} diff --git a/Library/Library.Domain/Abstractions/SystemTimeProvider.cs b/Library/Library.Domain/Abstractions/SystemTimeProvider.cs deleted file mode 100644 index ace01f126..000000000 --- a/Library/Library.Domain/Abstractions/SystemTimeProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Library.Domain.Abstractions; - -/// -/// Поставщик времени, возвращающий текущее системное время -/// -public class SystemTimeProvider : ITimeProvider -{ - /// - /// Возвращает текущее системное локальное время. - /// - public DateTime Now => DateTime.Now; -} diff --git a/Library/Library.Domain/Data/DataSeeder.cs b/Library/Library.Domain/Data/DataSeeder.cs index 51d5b29c7..226a31ba2 100644 --- a/Library/Library.Domain/Data/DataSeeder.cs +++ b/Library/Library.Domain/Data/DataSeeder.cs @@ -1,5 +1,4 @@ using Library.Domain.Models; -using Library.Domain.Abstractions; namespace Library.Domain.Data; @@ -8,15 +7,8 @@ namespace Library.Domain.Data; /// public class DataSeeder { - private readonly DateTime _now; - - /// - /// Создаёт экземпляр DataSeeder с заданным провайдером времени. - /// - public DataSeeder(ITimeProvider timeProvider) - { - _now = timeProvider.Now; - } + + private static readonly DateTime SeedNow = new(2026, 2, 19); public List EditionTypes { get; } = [ @@ -62,31 +54,31 @@ public DataSeeder(ITimeProvider timeProvider) public List Readers => new() { - new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-3)) }, - new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateOnly.FromDateTime(_now.AddYears(-2)) }, - new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-18)) }, - new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-12)) }, - new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-10)) }, - new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-8)) }, - new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-6)) }, - new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-5)) }, - new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-4)) }, - new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateOnly.FromDateTime(_now.AddMonths(-2)) }, + new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-3)) }, + new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddYears(-2)) }, + new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-18)) }, + new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-12)) }, + new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-10)) }, + new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-8)) }, + new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-6)) }, + new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-5)) }, + new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-4)) }, + new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-2)) }, }; public List BookIssues => new() { - new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = _now.AddDays(-15), Days = 30 }, - new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = _now.AddDays(-200), Days = 60 }, - new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = _now.AddDays(-40), Days = 14 }, - new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = _now.AddDays(-7), Days = 10 }, - new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = _now.AddDays(-300), Days = 21 }, - new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = _now.AddDays(-50), Days = 14 }, - new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = _now.AddDays(-3), Days = 7 }, - new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = _now.AddDays(-120), Days = 30 }, - new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = _now.AddDays(-60), Days = 20 }, - new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = _now.AddDays(-25), Days = 14 }, - new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = _now.AddDays(-5), Days = 10 }, - new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = _now.AddDays(-90), Days = 30 } + new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = SeedNow.AddDays(-15), Days = 30 }, + new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = SeedNow.AddDays(-200), Days = 60 }, + new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = SeedNow.AddDays(-40), Days = 14 }, + new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = SeedNow.AddDays(-7), Days = 10 }, + new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = SeedNow.AddDays(-300), Days = 21 }, + new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = SeedNow.AddDays(-50), Days = 14 }, + new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = SeedNow.AddDays(-3), Days = 7 }, + new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = SeedNow.AddDays(-120), Days = 30 }, + new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = SeedNow.AddDays(-60), Days = 20 }, + new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = SeedNow.AddDays(-25), Days = 14 }, + new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = SeedNow.AddDays(-5), Days = 10 }, + new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = SeedNow.AddDays(-90), Days = 30 } }; } diff --git a/Library/Library.Domain/Models/BookIssue.cs b/Library/Library.Domain/Models/BookIssue.cs index 4145b0ba4..81b8b8104 100644 --- a/Library/Library.Domain/Models/BookIssue.cs +++ b/Library/Library.Domain/Models/BookIssue.cs @@ -5,8 +5,6 @@ namespace Library.Domain.Models; /// public class BookIssue { - private readonly Library.Domain.Abstractions.ITimeProvider _timeProvider; - /// /// Уникальный идентификатор /// @@ -47,17 +45,9 @@ public class BookIssue /// public DateTime? ReturnDate => IssueDate.AddDays(Days); - /// - /// Конструктор с опциональным поставщиком времени - /// - public BookIssue(Library.Domain.Abstractions.ITimeProvider? timeProvider = null) - { - _timeProvider = timeProvider ?? new Library.Domain.Abstractions.SystemTimeProvider(); - } - /// /// Флаг просрочки срока возврата книги /// - public bool IsOverdue => - ReturnDate == null && _timeProvider.Now.Date > IssueDate.AddDays(Days).Date; + public bool IsOverdue(DateTime currentDate) => + ReturnDate == null && currentDate.Date > IssueDate.AddDays(Days).Date; } \ No newline at end of file diff --git a/Library/Library.Tests/FakeTimeProvider.cs b/Library/Library.Tests/FakeTimeProvider.cs deleted file mode 100644 index c69aa7ef4..000000000 --- a/Library/Library.Tests/FakeTimeProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Library.Domain.Abstractions; - -namespace Library.Tests; - -/// -/// Провайдер времени, возвращающий фиксированное значение -/// -public class FakeTimeProvider : ITimeProvider -{ - /// - /// Текущее время - /// - public DateTime Now { get; } - - /// - /// Создаёт экземпляр с заданным временем - /// - public FakeTimeProvider(DateTime now) - { - Now = now; - } -} diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 9517c76ac..c6384e67e 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -1,5 +1,4 @@ using Library.Domain.Data; -using Library.Domain.Abstractions; namespace Library.Tests; @@ -8,22 +7,15 @@ namespace Library.Tests; /// public class LibraryTests { - // Экземпляр DataSeeder для доступа к тестовым данным - private readonly DataSeeder _dataSeeder; - /// - // Фиксированная дата + /// Тестовые данные библиотеки /// - private readonly DateTime _fixedNow = new(2026, 2, 19); + private readonly DataSeeder _dataSeeder = new(); /// - /// Конструктор класса, создаёт DataSeeder с фиксированной датой + /// Контрольная дата, используемая в тестах /// - public LibraryTests() - { - var fakeTime = new FakeTimeProvider(_fixedNow); - _dataSeeder = new DataSeeder(fakeTime); - } + private readonly DateTime _now = new(2026, 2, 19); /// /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам @@ -32,7 +24,7 @@ public LibraryTests() public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() { var actualBookIds = _dataSeeder.BookIssues - .Where(bi => _fixedNow.Date < bi.ReturnDate) + .Where(bi => _now.Date < bi.ReturnDate) .Join(_dataSeeder.Books, bi => bi.BookId, b => b.Id, @@ -52,8 +44,8 @@ public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() [Fact] public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() { - var periodStart = _fixedNow.AddYears(-1); - var periodEnd = _fixedNow; + var periodStart = _now.AddYears(-1); + var periodEnd = _now; var topReaders = _dataSeeder.BookIssues .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) @@ -106,8 +98,8 @@ public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() [Fact] public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() { - var lastYearStart = _fixedNow.AddYears(-1); - var lastYearEnd = _fixedNow; + var lastYearStart = _now.AddYears(-1); + var lastYearEnd = _now; var topPublishers = _dataSeeder.BookIssues .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) @@ -136,8 +128,8 @@ public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() [Fact] public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() { - var lastYearStart = _fixedNow.AddYears(-1); - var lastYearEnd = _fixedNow; + var lastYearStart = _now.AddYears(-1); + var lastYearEnd = _now; var bookCounts = _dataSeeder.Books .GroupJoin( From 90b42f5c3146dfcf09bf334ffca5066002399a99 Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:28:32 +0400 Subject: [PATCH 08/10] fully tested working version of Lab 3, a server application with CRUD operations has been added, migrations have been created, and Aspire has been configured --- .github/workflows/tests.yml | 54 +- .gitignore | 3 - Library.sln | 36 - .../Controllers/AnalyticsController.cs | 141 ++++ .../Controllers/BookController.cs | 108 +++ .../Controllers/BookIssueController.cs | 15 + .../Controllers/CrudControllerBase.cs | 158 +++++ .../Controllers/EditionTypeController.cs | 15 + .../Controllers/PublisherController.cs | 15 + .../Controllers/ReaderController.cs | 46 ++ .../Library.Api.Host/Library.Api.Host.csproj | 25 + Library/Library.Api.Host/Program.cs | 81 +++ .../Properties/launchSettings.json | 41 ++ .../appsettings.Development.json | 8 + Library/Library.Api.Host/appsettings.json | 12 + Library/Library.AppHost/AppHost.cs | 10 + .../Library.AppHost/Library.AppHost.csproj | 22 + .../Properties/launchSettings.json | 29 + .../appsettings.Development.json | 8 + Library/Library.AppHost/appsettings.json | 9 + .../Analytics/BookIssuesStatDto.cs | 19 + .../Analytics/PublisherIssuesStatDto.cs | 19 + .../Analytics/ReaderIssuesStatDto.cs | 19 + .../BookIssues/BookIssueCreateUpdateDto.cs | 32 + .../BookIssues/BookIssueDto.cs | 37 + .../Books/BookCreateUpdateDto.cs | 42 ++ .../Books/BookDto.cs | 37 + .../Books/IBookService.cs | 32 + .../EditionTypeCreateUpdateDto.cs | 12 + .../EditionTypes/EditionTypeDto.cs | 17 + .../IAnalyticsService.cs | 40 ++ .../IApplicationService.cs | 48 ++ .../Library.Application.Contracts.csproj | 10 + .../Publishers/PublisherCreateUpdateDto.cs | 12 + .../Publishers/PublisherDto.cs | 17 + .../Readers/IReaderService.cs | 16 + .../Readers/ReaderCreateUpdateDto.cs | 27 + .../Readers/ReaderDto.cs | 32 + .../Library.Application.csproj | 18 + Library/Library.Application/LibraryProfile.cs | 35 + .../Services/AnalyticsService.cs | 152 +++++ .../Services/BookIssueService.cs | 89 +++ .../Services/BookService.cs | 144 ++++ .../Services/EditionTypeService.cs | 75 ++ .../Services/PublisherService.cs | 75 ++ .../Services/ReaderService.cs | 95 +++ Library/Library.Domain/Data/DataSeeder.cs | 187 ++--- Library/Library.Domain/IRepository.cs | 49 ++ Library/Library.Domain/Library.Domain.csproj | 18 +- Library/Library.Domain/Models/Book.cs | 122 ++-- Library/Library.Domain/Models/BookIssue.cs | 103 ++- Library/Library.Domain/Models/EditionType.cs | 32 +- Library/Library.Domain/Models/Publisher.cs | 32 +- Library/Library.Domain/Models/Reader.cs | 72 +- .../Library.Infrastructure.EfCore.csproj | 17 + .../LibraryDbContext.cs | 216 ++++++ ...260222160435_InitialCreateGuid.Designer.cs | 642 ++++++++++++++++++ .../20260222160435_InitialCreateGuid.cs | 282 ++++++++ .../LibraryDbContextModelSnapshot.cs | 639 +++++++++++++++++ .../Repositories/BookIssueRepository.cs | 75 ++ .../Repositories/BookRepository.cs | 70 ++ .../Repositories/EditionTypeRepository.cs | 65 ++ .../Repositories/PublisherRepository.cs | 65 ++ .../Repositories/ReaderRepository.cs | 77 +++ Library/Library.ServiceDefaults/Extensions.cs | 127 ++++ .../Library.ServiceDefaults.csproj | 22 + Library/Library.Tests/Library.Tests.csproj | 54 +- Library/Library.Tests/LibraryTests.cs | 296 ++++---- Library/Library.sln | 67 ++ 69 files changed, 4798 insertions(+), 518 deletions(-) delete mode 100644 Library.sln create mode 100644 Library/Library.Api.Host/Controllers/AnalyticsController.cs create mode 100644 Library/Library.Api.Host/Controllers/BookController.cs create mode 100644 Library/Library.Api.Host/Controllers/BookIssueController.cs create mode 100644 Library/Library.Api.Host/Controllers/CrudControllerBase.cs create mode 100644 Library/Library.Api.Host/Controllers/EditionTypeController.cs create mode 100644 Library/Library.Api.Host/Controllers/PublisherController.cs create mode 100644 Library/Library.Api.Host/Controllers/ReaderController.cs create mode 100644 Library/Library.Api.Host/Library.Api.Host.csproj create mode 100644 Library/Library.Api.Host/Program.cs create mode 100644 Library/Library.Api.Host/Properties/launchSettings.json create mode 100644 Library/Library.Api.Host/appsettings.Development.json create mode 100644 Library/Library.Api.Host/appsettings.json create mode 100644 Library/Library.AppHost/AppHost.cs create mode 100644 Library/Library.AppHost/Library.AppHost.csproj create mode 100644 Library/Library.AppHost/Properties/launchSettings.json create mode 100644 Library/Library.AppHost/appsettings.Development.json create mode 100644 Library/Library.AppHost/appsettings.json create mode 100644 Library/Library.Application.Contracts/Analytics/BookIssuesStatDto.cs create mode 100644 Library/Library.Application.Contracts/Analytics/PublisherIssuesStatDto.cs create mode 100644 Library/Library.Application.Contracts/Analytics/ReaderIssuesStatDto.cs create mode 100644 Library/Library.Application.Contracts/BookIssues/BookIssueCreateUpdateDto.cs create mode 100644 Library/Library.Application.Contracts/BookIssues/BookIssueDto.cs create mode 100644 Library/Library.Application.Contracts/Books/BookCreateUpdateDto.cs create mode 100644 Library/Library.Application.Contracts/Books/BookDto.cs create mode 100644 Library/Library.Application.Contracts/Books/IBookService.cs create mode 100644 Library/Library.Application.Contracts/EditionTypes/EditionTypeCreateUpdateDto.cs create mode 100644 Library/Library.Application.Contracts/EditionTypes/EditionTypeDto.cs create mode 100644 Library/Library.Application.Contracts/IAnalyticsService.cs create mode 100644 Library/Library.Application.Contracts/IApplicationService.cs create mode 100644 Library/Library.Application.Contracts/Library.Application.Contracts.csproj create mode 100644 Library/Library.Application.Contracts/Publishers/PublisherCreateUpdateDto.cs create mode 100644 Library/Library.Application.Contracts/Publishers/PublisherDto.cs create mode 100644 Library/Library.Application.Contracts/Readers/IReaderService.cs create mode 100644 Library/Library.Application.Contracts/Readers/ReaderCreateUpdateDto.cs create mode 100644 Library/Library.Application.Contracts/Readers/ReaderDto.cs create mode 100644 Library/Library.Application/Library.Application.csproj create mode 100644 Library/Library.Application/LibraryProfile.cs create mode 100644 Library/Library.Application/Services/AnalyticsService.cs create mode 100644 Library/Library.Application/Services/BookIssueService.cs create mode 100644 Library/Library.Application/Services/BookService.cs create mode 100644 Library/Library.Application/Services/EditionTypeService.cs create mode 100644 Library/Library.Application/Services/PublisherService.cs create mode 100644 Library/Library.Application/Services/ReaderService.cs create mode 100644 Library/Library.Domain/IRepository.cs create mode 100644 Library/Library.Infrastructure.EfCore/Library.Infrastructure.EfCore.csproj create mode 100644 Library/Library.Infrastructure.EfCore/LibraryDbContext.cs create mode 100644 Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.Designer.cs create mode 100644 Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.cs create mode 100644 Library/Library.Infrastructure.EfCore/Migrations/LibraryDbContextModelSnapshot.cs create mode 100644 Library/Library.Infrastructure.EfCore/Repositories/BookIssueRepository.cs create mode 100644 Library/Library.Infrastructure.EfCore/Repositories/BookRepository.cs create mode 100644 Library/Library.Infrastructure.EfCore/Repositories/EditionTypeRepository.cs create mode 100644 Library/Library.Infrastructure.EfCore/Repositories/PublisherRepository.cs create mode 100644 Library/Library.Infrastructure.EfCore/Repositories/ReaderRepository.cs create mode 100644 Library/Library.ServiceDefaults/Extensions.cs create mode 100644 Library/Library.ServiceDefaults/Library.ServiceDefaults.csproj create mode 100644 Library/Library.sln diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f2699892..f08487dd1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,27 +1,27 @@ -name: Run .NET Tests - -on: - push: - pull_request: - -jobs: - build-and-test: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "8.0.x" - - - name: Restore dependencies - run: dotnet restore Library.sln - - - name: Build - run: dotnet build Library.sln --configuration Release --no-restore - - - name: Run tests - run: dotnet test Library.sln --configuration Release --no-build \ No newline at end of file +name: Run .NET Tests + +on: + push: + pull_request: + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore Library/Library.sln + + - name: Build + run: dotnet build Library/Library.sln --configuration Release --no-restore + + - name: Run tests + run: dotnet test Library/Library.sln --configuration Release --no-build diff --git a/.gitignore b/.gitignore index 6ecb5f9bd..ce892922f 100644 --- a/.gitignore +++ b/.gitignore @@ -416,6 +416,3 @@ FodyWeavers.xsd *.msix *.msm *.msp - -**/bin -**/obj \ No newline at end of file diff --git a/Library.sln b/Library.sln deleted file mode 100644 index 6550f8d0c..000000000 --- a/Library.sln +++ /dev/null @@ -1,36 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{50A17DA1-3556-4046-CEDC-33EB466D9C32}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Domain", "Library\Library.Domain\Library.Domain.csproj", "{39E55976-5424-CEC7-0978-4D789AC54AC6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Tests", "Library\Library.Tests\Library.Tests.csproj", "{60F294C7-3D77-63D8-5514-C4AB9BB913BD}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {39E55976-5424-CEC7-0978-4D789AC54AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39E55976-5424-CEC7-0978-4D789AC54AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39E55976-5424-CEC7-0978-4D789AC54AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39E55976-5424-CEC7-0978-4D789AC54AC6}.Release|Any CPU.Build.0 = Release|Any CPU - {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60F294C7-3D77-63D8-5514-C4AB9BB913BD}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {39E55976-5424-CEC7-0978-4D789AC54AC6} = {50A17DA1-3556-4046-CEDC-33EB466D9C32} - {60F294C7-3D77-63D8-5514-C4AB9BB913BD} = {50A17DA1-3556-4046-CEDC-33EB466D9C32} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B2802E6A-D42A-4C1D-8949-77992D768DF6} - EndGlobalSection -EndGlobal diff --git a/Library/Library.Api.Host/Controllers/AnalyticsController.cs b/Library/Library.Api.Host/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..5393a9649 --- /dev/null +++ b/Library/Library.Api.Host/Controllers/AnalyticsController.cs @@ -0,0 +1,141 @@ +using Library.Application.Contracts; +using Library.Application.Contracts.Analytics; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.Readers; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для выполнения аналитических запросов по библиотеке +/// +[Route("api/[controller]")] +[ApiController] +public class AnalyticsController( + IAnalyticsService analyticsService, + ILogger logger) : ControllerBase +{ + /// + /// Возвращает информацию о выданных книгах, упорядоченных по названию + /// + /// Список DTO для получения книг + [HttpGet("issued-books")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetIssuedBooksOrderedByTitle() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetIssuedBooksOrderedByTitle), GetType().Name); + try + { + var res = await analyticsService.GetIssuedBooksOrderedByTitle(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetIssuedBooksOrderedByTitle), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetIssuedBooksOrderedByTitle), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает информацию о топ 5 читателей, прочитавших больше всего книг за заданный период + /// + /// Начало периода в UTC + /// Конец периода в UTC + /// Список DTO для получения статистики по читателям + [HttpGet("top-readers")] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task>> GetTop5ReadersByIssuesCount([FromQuery] DateTime periodStart, [FromQuery] DateTime periodEnd) + { + logger.LogInformation( + "{method} method of {controller} is called with {start},{end} parameters", + nameof(GetTop5ReadersByIssuesCount), GetType().Name, periodStart, periodEnd); + + if (periodEnd < periodStart) + return BadRequest("periodEnd cannot be less than PeriodStart"); + + try + { + var res = await analyticsService.GetTop5ReadersByIssuesCount(periodStart, periodEnd); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5ReadersByIssuesCount), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5ReadersByIssuesCount), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает информацию о читателях, бравших книги на наибольший период времени, упорядоченных по ФИО + /// + /// Список DTO для получения читателей + [HttpGet("readers-max-loan-days")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetReadersByMaxLoanDaysOrderedByFullName() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetReadersByMaxLoanDaysOrderedByFullName), GetType().Name); + try + { + var res = await analyticsService.GetReadersByMaxLoanDaysOrderedByFullName(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetReadersByMaxLoanDaysOrderedByFullName), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetReadersByMaxLoanDaysOrderedByFullName), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает топ 5 наиболее популярных издательств за последний год + /// + /// Список DTO для получения статистики по издательствам + [HttpGet("top-publishers-last-year")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetTop5PublishersByIssuesCountLastYear() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetTop5PublishersByIssuesCountLastYear), GetType().Name); + try + { + var res = await analyticsService.GetTop5PublishersByIssuesCountLastYear(DateTime.UtcNow); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetTop5PublishersByIssuesCountLastYear), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetTop5PublishersByIssuesCountLastYear), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает топ 5 наименее популярных книг за последний год + /// + /// Список DTO для получения статистики по книгам + [HttpGet("bottom-books-last-year")] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetBottom5BooksByIssuesCountLastYear() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetBottom5BooksByIssuesCountLastYear), GetType().Name); + try + { + var res = await analyticsService.GetBottom5BooksByIssuesCountLastYear(DateTime.UtcNow); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetBottom5BooksByIssuesCountLastYear), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetBottom5BooksByIssuesCountLastYear), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } +} \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/BookController.cs b/Library/Library.Api.Host/Controllers/BookController.cs new file mode 100644 index 000000000..9e9645100 --- /dev/null +++ b/Library/Library.Api.Host/Controllers/BookController.cs @@ -0,0 +1,108 @@ +using Library.Application.Contracts.Books; +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.EditionTypes; +using Library.Application.Contracts.Publishers; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для работы с книгами +/// +[Route("api/[controller]")] +[ApiController] +public class BookController( + IBookService bookService, + ILogger logger) + : CrudControllerBase(bookService, logger) +{ + /// + /// Возвращает записи о выдачах книги + /// + /// Идентификатор книги + /// Список DTO для получения выдач книг + [HttpGet("{id}/Issues")] + [ProducesResponseType(typeof(IList), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task>> GetIssues(int id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(GetIssues), GetType().Name, id); + try + { + var res = await bookService.GetIssues(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetIssues), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(GetIssues), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetIssues), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает вид издания книги + /// + /// Идентификатор книги + /// DTO для получения вида издания + [HttpGet("{id}/EditionType")] + [ProducesResponseType(typeof(EditionTypeDto), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> GetEditionType(int id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(GetEditionType), GetType().Name, id); + try + { + var res = await bookService.GetEditionType(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetEditionType), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(GetEditionType), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetEditionType), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает издательство книги + /// + /// Идентификатор книги + /// DTO для получения издательства + [HttpGet("{id}/Publisher")] + [ProducesResponseType(typeof(PublisherDto), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> GetPublisher(int id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(GetPublisher), GetType().Name, id); + try + { + var res = await bookService.GetPublisher(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetPublisher), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(GetPublisher), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetPublisher), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } +} \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/BookIssueController.cs b/Library/Library.Api.Host/Controllers/BookIssueController.cs new file mode 100644 index 000000000..d8f07a5a2 --- /dev/null +++ b/Library/Library.Api.Host/Controllers/BookIssueController.cs @@ -0,0 +1,15 @@ +using Library.Application.Contracts; +using Library.Application.Contracts.BookIssues; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для работы с выдачами книг +/// +[Route("api/[controller]")] +[ApiController] +public class BookIssueController( + IApplicationService appService, + ILogger logger) + : CrudControllerBase(appService, logger); \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/CrudControllerBase.cs b/Library/Library.Api.Host/Controllers/CrudControllerBase.cs new file mode 100644 index 000000000..488e17cbe --- /dev/null +++ b/Library/Library.Api.Host/Controllers/CrudControllerBase.cs @@ -0,0 +1,158 @@ +using Library.Application.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Базовый CRUD контроллер для работы с сущностями через сервис приложения и DTO +/// +/// DTO для операций чтения +/// DTO для операций создания и обновления +/// Тип идентификатора +[Route("api/[controller]")] +[ApiController] +public abstract class CrudControllerBase(IApplicationService appService, + ILogger> logger) : ControllerBase + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + /// + /// Создаёт сущность + /// + /// DTO для создания или обновления сущности + /// DTO для получения созданной сущности + [HttpPost] + [ProducesResponseType(201)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> Create(TCreateUpdateDto newDto) + { + logger.LogInformation("{method} method of {controller} is called with {@dto} parameter", nameof(Create), GetType().Name, newDto); + try + { + var res = await appService.Create(newDto); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Create), GetType().Name); + return CreatedAtAction(nameof(this.Create), res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(Create), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(Create), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Обновляет сущность по идентификатору + /// + /// Идентификатор сущности + /// DTO для создания или обновления сущности + /// DTO для получения обновлённой сущности + [HttpPut("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> Edit(TKey id, TCreateUpdateDto newDto) + { + logger.LogInformation("{method} method of {controller} is called with {key},{@dto} parameters", nameof(Edit), GetType().Name, id, newDto); + try + { + var res = await appService.Update(newDto, id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Edit), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(Edit), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(Edit), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Удаляет сущность по идентификатору + /// + /// Идентификатор сущности + /// Результат удаления + [HttpDelete("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(204)] + [ProducesResponseType(500)] + public async Task Delete(TKey id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Delete), GetType().Name, id); + try + { + var res = await appService.Delete(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Delete), GetType().Name); + return res ? Ok() : NoContent(); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(Delete), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает список сущностей + /// + /// Список DTO для получения сущностей + [HttpGet] + [ProducesResponseType(200)] + [ProducesResponseType(500)] + public async Task>> GetAll() + { + logger.LogInformation("{method} method of {controller} is called", nameof(GetAll), GetType().Name); + try + { + var res = await appService.GetAll(); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetAll), GetType().Name); + return Ok(res); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetAll), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } + + /// + /// Возвращает сущность по идентификатору + /// + /// Идентификатор сущности + /// DTO для получения сущности + [HttpGet("{id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task> Get(TKey id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(Get), GetType().Name, id); + try + { + var res = await appService.Get(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(Get), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(Get), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(Get), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } +} \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/EditionTypeController.cs b/Library/Library.Api.Host/Controllers/EditionTypeController.cs new file mode 100644 index 000000000..05e85e832 --- /dev/null +++ b/Library/Library.Api.Host/Controllers/EditionTypeController.cs @@ -0,0 +1,15 @@ +using Library.Application.Contracts; +using Library.Application.Contracts.EditionTypes; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для работы с видами изданий +/// +[Route("api/[controller]")] +[ApiController] +public class EditionTypeController( + IApplicationService appService, + ILogger logger) + : CrudControllerBase(appService, logger); \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/PublisherController.cs b/Library/Library.Api.Host/Controllers/PublisherController.cs new file mode 100644 index 000000000..e44cade30 --- /dev/null +++ b/Library/Library.Api.Host/Controllers/PublisherController.cs @@ -0,0 +1,15 @@ +using Library.Application.Contracts; +using Library.Application.Contracts.Publishers; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для работы с издательствами +/// +[Route("api/[controller]")] +[ApiController] +public class PublisherController( + IApplicationService appService, + ILogger logger) + : CrudControllerBase(appService, logger); \ No newline at end of file diff --git a/Library/Library.Api.Host/Controllers/ReaderController.cs b/Library/Library.Api.Host/Controllers/ReaderController.cs new file mode 100644 index 000000000..5bf95bacd --- /dev/null +++ b/Library/Library.Api.Host/Controllers/ReaderController.cs @@ -0,0 +1,46 @@ +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.Readers; +using Microsoft.AspNetCore.Mvc; + +namespace Library.Api.Host.Controllers; + +/// +/// Контроллер для работы с читателями +/// +[Route("api/[controller]")] +[ApiController] +public class ReaderController( + IReaderService readerService, + ILogger logger) + : CrudControllerBase(readerService, logger) +{ + /// + /// Возвращает записи о выдачах книг читателю + /// + /// Идентификатор читателя + /// Список DTO для получения выдач книг + [HttpGet("{id}/Issues")] + [ProducesResponseType(typeof(IList), 200)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task>> GetIssues(int id) + { + logger.LogInformation("{method} method of {controller} is called with {id} parameter", nameof(GetIssues), GetType().Name, id); + try + { + var res = await readerService.GetIssues(id); + logger.LogInformation("{method} method of {controller} executed successfully", nameof(GetIssues), GetType().Name); + return Ok(res); + } + catch (KeyNotFoundException ex) + { + logger.LogWarning("A not found exception happened during {method} method of {controller}: {@exception}", nameof(GetIssues), GetType().Name, ex); + return NotFound(ex.Message); + } + catch (Exception ex) + { + logger.LogError("An exception happened during {method} method of {controller}: {@exception}", nameof(GetIssues), GetType().Name, ex); + return StatusCode(500, $"{ex.Message}\n\r{ex.InnerException?.Message}"); + } + } +} \ No newline at end of file diff --git a/Library/Library.Api.Host/Library.Api.Host.csproj b/Library/Library.Api.Host/Library.Api.Host.csproj new file mode 100644 index 000000000..1712ada6c --- /dev/null +++ b/Library/Library.Api.Host/Library.Api.Host.csproj @@ -0,0 +1,25 @@ + + + + true + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/Library/Library.Api.Host/Program.cs b/Library/Library.Api.Host/Program.cs new file mode 100644 index 000000000..5291c2bbe --- /dev/null +++ b/Library/Library.Api.Host/Program.cs @@ -0,0 +1,81 @@ +using Library.Application; +using Library.Application.Contracts; +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.EditionTypes; +using Library.Application.Contracts.Publishers; +using Library.Application.Contracts.Readers; +using Library.Application.Services; +using Library.Domain; +using Library.Domain.Data; +using Library.Domain.Models; +using Library.Infrastructure.EfCore; +using Library.Infrastructure.EfCore.Repositories; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); + +builder.Services.AddAutoMapper(config => +{ + config.AddProfile(new LibraryProfile()); +}); + +builder.AddServiceDefaults(); + +builder.Services.AddTransient, BookIssueRepository>(); +builder.Services.AddTransient, BookRepository>(); +builder.Services.AddTransient, EditionTypeRepository>(); +builder.Services.AddTransient, PublisherRepository>(); +builder.Services.AddTransient, ReaderRepository>(); + +builder.Services.AddScoped(); +builder.Services.AddScoped, BookIssueService>(); +builder.Services.AddScoped, EditionTypeService>(); +builder.Services.AddScoped, PublisherService>(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.UseInlineDefinitionsForEnums(); + + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name!.StartsWith("Library")) + .Distinct(); + + foreach (var assembly in assemblies) + { + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + c.IncludeXmlComments(xmlPath); + } +}); + +builder.AddSqlServerDbContext("Connection"); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Library/Library.Api.Host/Properties/launchSettings.json b/Library/Library.Api.Host/Properties/launchSettings.json new file mode 100644 index 000000000..cc310460f --- /dev/null +++ b/Library/Library.Api.Host/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36507", + "sslPort": 44361 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5103", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7086;http://localhost:5103", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Library/Library.Api.Host/appsettings.Development.json b/Library/Library.Api.Host/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Library/Library.Api.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Library/Library.Api.Host/appsettings.json b/Library/Library.Api.Host/appsettings.json new file mode 100644 index 000000000..7f74c93da --- /dev/null +++ b/Library/Library.Api.Host/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "Connection": "Server=localhost;Database=LibraryDb;Trusted_Connection=True;Encrypt=False;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Library/Library.AppHost/AppHost.cs b/Library/Library.AppHost/AppHost.cs new file mode 100644 index 000000000..d52993f8d --- /dev/null +++ b/Library/Library.AppHost/AppHost.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var sqlDb = builder.AddSqlServer("library-sql-server") + .AddDatabase("LibraryDb"); + +builder.AddProject("library-api-host") + .WithReference(sqlDb, "Connection") + .WaitFor(sqlDb); + +builder.Build().Run(); \ No newline at end of file diff --git a/Library/Library.AppHost/Library.AppHost.csproj b/Library/Library.AppHost/Library.AppHost.csproj new file mode 100644 index 000000000..747481a8d --- /dev/null +++ b/Library/Library.AppHost/Library.AppHost.csproj @@ -0,0 +1,22 @@ + + + + + + Exe + net8.0 + enable + enable + 7b71ac1f-e117-4c12-831b-157ecc95ce17 + + + + + + + + + + + + diff --git a/Library/Library.AppHost/Properties/launchSettings.json b/Library/Library.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..12c69b72d --- /dev/null +++ b/Library/Library.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17166;http://localhost:15234", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21293", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22168" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15234", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19037", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20112" + } + } + } +} diff --git a/Library/Library.AppHost/appsettings.Development.json b/Library/Library.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Library/Library.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Library/Library.AppHost/appsettings.json b/Library/Library.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/Library/Library.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Library/Library.Application.Contracts/Analytics/BookIssuesStatDto.cs b/Library/Library.Application.Contracts/Analytics/BookIssuesStatDto.cs new file mode 100644 index 000000000..5e749975f --- /dev/null +++ b/Library/Library.Application.Contracts/Analytics/BookIssuesStatDto.cs @@ -0,0 +1,19 @@ +using Library.Application.Contracts.Books; + +namespace Library.Application.Contracts.Analytics; + +/// +/// DTO для получения статистики по книге +/// +public class BookIssuesStatDto +{ + /// + /// DTO для получения книги + /// + public required BookDto Book { get; set; } + + /// + /// Количество выдач + /// + public required int IssuesCount { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Analytics/PublisherIssuesStatDto.cs b/Library/Library.Application.Contracts/Analytics/PublisherIssuesStatDto.cs new file mode 100644 index 000000000..ef09147ad --- /dev/null +++ b/Library/Library.Application.Contracts/Analytics/PublisherIssuesStatDto.cs @@ -0,0 +1,19 @@ +using Library.Application.Contracts.Publishers; + +namespace Library.Application.Contracts.Analytics; + +/// +/// DTO для получения статистики по издательству +/// +public class PublisherIssuesStatDto +{ + /// + /// DTO для получения издательства + /// + public required PublisherDto Publisher { get; set; } + + /// + /// Количество выдач + /// + public required int IssuesCount { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Analytics/ReaderIssuesStatDto.cs b/Library/Library.Application.Contracts/Analytics/ReaderIssuesStatDto.cs new file mode 100644 index 000000000..b418f965b --- /dev/null +++ b/Library/Library.Application.Contracts/Analytics/ReaderIssuesStatDto.cs @@ -0,0 +1,19 @@ +using Library.Application.Contracts.Readers; + +namespace Library.Application.Contracts.Analytics; + +/// +/// DTO для получения статистики по читателю +/// +public class ReaderIssuesStatDto +{ + /// + /// DTO для получения читателя + /// + public required ReaderDto Reader { get; set; } + + /// + /// Количество выдач + /// + public required int IssuesCount { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/BookIssues/BookIssueCreateUpdateDto.cs b/Library/Library.Application.Contracts/BookIssues/BookIssueCreateUpdateDto.cs new file mode 100644 index 000000000..a49468b02 --- /dev/null +++ b/Library/Library.Application.Contracts/BookIssues/BookIssueCreateUpdateDto.cs @@ -0,0 +1,32 @@ +namespace Library.Application.Contracts.BookIssues; + +/// +/// DTO для создания или обновления факта выдачи книги +/// +public class BookIssueCreateUpdateDto +{ + /// + /// Идентификатор книги + /// + public required int BookId { get; set; } + + /// + /// Идентификатор читателя + /// + public required int ReaderId { get; set; } + + /// + /// Дата выдачи книги + /// + public required DateTime IssueDate { get; set; } + + /// + /// Количество дней, на которое выдана книга + /// + public required int Days { get; set; } + + /// + /// Дата возврата книги если null то книга не возвращена + /// + public DateTime? ReturnDate { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/BookIssues/BookIssueDto.cs b/Library/Library.Application.Contracts/BookIssues/BookIssueDto.cs new file mode 100644 index 000000000..89f628673 --- /dev/null +++ b/Library/Library.Application.Contracts/BookIssues/BookIssueDto.cs @@ -0,0 +1,37 @@ +namespace Library.Application.Contracts.BookIssues; + +/// +/// DTO для получения факта выдачи книги +/// +public class BookIssueDto +{ + /// + /// Уникальный идентификатор факта выдачи + /// + public required int Id { get; set; } + + /// + /// Идентификатор книги + /// + public required int BookId { get; set; } + + /// + /// Идентификатор читателя + /// + public required int ReaderId { get; set; } + + /// + /// Дата выдачи книги + /// + public required DateTime IssueDate { get; set; } + + /// + /// Количество дней, на которое выдана книга + /// + public required int Days { get; set; } + + /// + /// Дата возврата книги если null то книга не возвращена + /// + public DateTime? ReturnDate { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Books/BookCreateUpdateDto.cs b/Library/Library.Application.Contracts/Books/BookCreateUpdateDto.cs new file mode 100644 index 000000000..8443dbfc7 --- /dev/null +++ b/Library/Library.Application.Contracts/Books/BookCreateUpdateDto.cs @@ -0,0 +1,42 @@ +namespace Library.Application.Contracts.Books; + +/// +/// DTO для создания или обновления книги +/// +public class BookCreateUpdateDto +{ + /// + /// Инвентарный номер + /// + public required string InventoryNumber { get; set; } + + /// + /// Шифр в алфавитном каталоге + /// + public required string AlphabetCode { get; set; } + + /// + /// Инициалы и фамилии авторов + /// + public string? Authors { get; set; } + + /// + /// Название книги + /// + public required string Title { get; set; } + + /// + /// Идентификатор вида издания + /// + public required int EditionTypeId { get; set; } + + /// + /// Идентификатор издательства + /// + public required int PublisherId { get; set; } + + /// + /// Год издания + /// + public int Year { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Books/BookDto.cs b/Library/Library.Application.Contracts/Books/BookDto.cs new file mode 100644 index 000000000..c656e9561 --- /dev/null +++ b/Library/Library.Application.Contracts/Books/BookDto.cs @@ -0,0 +1,37 @@ +namespace Library.Application.Contracts.Books; + +/// +/// DTO для получения книги +/// +public class BookDto +{ + /// + /// Уникальный идентификатор книги + /// + public required int Id { get; set; } + + /// + /// Инвентарный номер + /// + public required string InventoryNumber { get; set; } + + /// + /// Шифр в алфавитном каталоге + /// + public required string AlphabetCode { get; set; } + + /// + /// Инициалы и фамилии авторов + /// + public string? Authors { get; set; } + + /// + /// Название книги + /// + public required string Title { get; set; } + + /// + /// Год издания + /// + public int Year { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Books/IBookService.cs b/Library/Library.Application.Contracts/Books/IBookService.cs new file mode 100644 index 000000000..bfd09d0f3 --- /dev/null +++ b/Library/Library.Application.Contracts/Books/IBookService.cs @@ -0,0 +1,32 @@ +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.EditionTypes; +using Library.Application.Contracts.Publishers; + +namespace Library.Application.Contracts.Books; + +/// +/// Сервис приложения для работы с книгами +/// +public interface IBookService : IApplicationService +{ + /// + /// Возвращает записи о выдачах книги + /// + /// Идентификатор книги + /// Список DTO для получения выдач книг + public Task> GetIssues(int bookId); + + /// + /// Возвращает вид издания книги + /// + /// Идентификатор книги + /// DTO для получения вида издания + public Task GetEditionType(int bookId); + + /// + /// Возвращает издательство книги + /// + /// Идентификатор книги + /// DTO для получения издательства + public Task GetPublisher(int bookId); +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/EditionTypes/EditionTypeCreateUpdateDto.cs b/Library/Library.Application.Contracts/EditionTypes/EditionTypeCreateUpdateDto.cs new file mode 100644 index 000000000..d2b53d89e --- /dev/null +++ b/Library/Library.Application.Contracts/EditionTypes/EditionTypeCreateUpdateDto.cs @@ -0,0 +1,12 @@ +namespace Library.Application.Contracts.EditionTypes; + +/// +/// DTO для создания или обновления вида издания +/// +public class EditionTypeCreateUpdateDto +{ + /// + /// Наименование вида издания + /// + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/EditionTypes/EditionTypeDto.cs b/Library/Library.Application.Contracts/EditionTypes/EditionTypeDto.cs new file mode 100644 index 000000000..9e152e623 --- /dev/null +++ b/Library/Library.Application.Contracts/EditionTypes/EditionTypeDto.cs @@ -0,0 +1,17 @@ +namespace Library.Application.Contracts.EditionTypes; + +/// +/// DTO для получения вида издания +/// +public class EditionTypeDto +{ + /// + /// Уникальный идентификатор вида издания + /// + public required int Id { get; set; } + + /// + /// Наименование вида издания + /// + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/IAnalyticsService.cs b/Library/Library.Application.Contracts/IAnalyticsService.cs new file mode 100644 index 000000000..1d8101ff9 --- /dev/null +++ b/Library/Library.Application.Contracts/IAnalyticsService.cs @@ -0,0 +1,40 @@ +using Library.Application.Contracts.Analytics; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.Readers; + +namespace Library.Application.Contracts; + +/// +/// Сервис аналитических запросов по доменной области библиотеки +/// +public interface IAnalyticsService +{ + /// + /// Возвращает информацию о выданных книгах, упорядоченных по названию + /// + public Task> GetIssuedBooksOrderedByTitle(); + + /// + /// Возвращает топ 5 читателей, прочитавших больше всего книг за заданный период + /// + /// Начало периода в UTC + /// Конец периода в UTC + public Task> GetTop5ReadersByIssuesCount(DateTime periodStart, DateTime periodEnd); + + /// + /// Возвращает читателей, бравших книги на наибольший период времени, упорядоченных по ФИО + /// + public Task> GetReadersByMaxLoanDaysOrderedByFullName(); + + /// + /// Возвращает топ 5 наиболее популярных издательств за последний год + /// + /// Текущая точка времени в UTC для расчёта периода + public Task> GetTop5PublishersByIssuesCountLastYear(DateTime nowUtc); + + /// + /// Возвращает топ 5 наименее популярных книг за последний год + /// + /// Текущая точка времени в UTC для расчёта периода + public Task> GetBottom5BooksByIssuesCountLastYear(DateTime nowUtc); +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/IApplicationService.cs b/Library/Library.Application.Contracts/IApplicationService.cs new file mode 100644 index 000000000..4b6604786 --- /dev/null +++ b/Library/Library.Application.Contracts/IApplicationService.cs @@ -0,0 +1,48 @@ +namespace Library.Application.Contracts; + +/// +/// Универсальный интерфейс службы приложения для CRUD операций над сущностями через DTO +/// +/// DTO для операций чтения +/// DTO для операций создания и обновления +/// Тип идентификатора DTO +public interface IApplicationService + where TDto : class + where TCreateUpdateDto : class + where TKey : struct +{ + /// + /// Создаёт сущность на основе DTO для создания и возвращает DTO для чтения + /// + /// DTO, содержащий данные для создания + /// Созданный объект в формате DTO для чтения + public Task Create(TCreateUpdateDto dto); + + /// + /// Возвращает DTO для чтения по идентификатору + /// + /// Идентификатор + /// DTO для чтения или null если объект не найден + public Task Get(TKey dtoId); + + /// + /// Возвращает список DTO для чтения + /// + /// Список DTO для чтения + public Task> GetAll(); + + /// + /// Обновляет сущность по идентификатору на основе DTO для обновления и возвращает DTO для чтения + /// + /// DTO, содержащий новые значения + /// Идентификатор обновляемого объекта + /// Обновлённый объект в формате DTO для чтения + public Task Update(TCreateUpdateDto dto, TKey dtoId); + + /// + /// Удаляет объект по идентификатору + /// + /// Идентификатор удаляемого объекта + /// true если объект был удалён иначе false + public Task Delete(TKey dtoId); +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Library.Application.Contracts.csproj b/Library/Library.Application.Contracts/Library.Application.Contracts.csproj new file mode 100644 index 000000000..fe6f12ba8 --- /dev/null +++ b/Library/Library.Application.Contracts/Library.Application.Contracts.csproj @@ -0,0 +1,10 @@ + + + + true + net8.0 + enable + enable + + + diff --git a/Library/Library.Application.Contracts/Publishers/PublisherCreateUpdateDto.cs b/Library/Library.Application.Contracts/Publishers/PublisherCreateUpdateDto.cs new file mode 100644 index 000000000..756e1eb3c --- /dev/null +++ b/Library/Library.Application.Contracts/Publishers/PublisherCreateUpdateDto.cs @@ -0,0 +1,12 @@ +namespace Library.Application.Contracts.Publishers; + +/// +/// DTO для создания или обновления издательства +/// +public class PublisherCreateUpdateDto +{ + /// + /// Наименование издательства + /// + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Publishers/PublisherDto.cs b/Library/Library.Application.Contracts/Publishers/PublisherDto.cs new file mode 100644 index 000000000..6068df620 --- /dev/null +++ b/Library/Library.Application.Contracts/Publishers/PublisherDto.cs @@ -0,0 +1,17 @@ +namespace Library.Application.Contracts.Publishers; + +/// +/// DTO для получения издательства +/// +public class PublisherDto +{ + /// + /// Уникальный идентификатор издательства + /// + public required int Id { get; set; } + + /// + /// Наименование издательства + /// + public required string Name { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Readers/IReaderService.cs b/Library/Library.Application.Contracts/Readers/IReaderService.cs new file mode 100644 index 000000000..58a06f06d --- /dev/null +++ b/Library/Library.Application.Contracts/Readers/IReaderService.cs @@ -0,0 +1,16 @@ +using Library.Application.Contracts.BookIssues; + +namespace Library.Application.Contracts.Readers; + +/// +/// Сервис приложения для работы с читателями +/// +public interface IReaderService : IApplicationService +{ + /// + /// Возвращает записи о выдачах книг читателю + /// + /// Идентификатор читателя + /// Список DTO для получения выдач книг + public Task> GetIssues(int readerId); +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Readers/ReaderCreateUpdateDto.cs b/Library/Library.Application.Contracts/Readers/ReaderCreateUpdateDto.cs new file mode 100644 index 000000000..d71483e4d --- /dev/null +++ b/Library/Library.Application.Contracts/Readers/ReaderCreateUpdateDto.cs @@ -0,0 +1,27 @@ +namespace Library.Application.Contracts.Readers; + +/// +/// DTO для создания или обновления читателя +/// +public class ReaderCreateUpdateDto +{ + /// + /// ФИО читателя + /// + public required string FullName { get; set; } + + /// + /// Адрес читателя + /// + public string? Address { get; set; } + + /// + /// Телефон читателя + /// + public required string Phone { get; set; } + + /// + /// Дата регистрации читателя + /// + public DateTime? RegistrationDate { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application.Contracts/Readers/ReaderDto.cs b/Library/Library.Application.Contracts/Readers/ReaderDto.cs new file mode 100644 index 000000000..0534d5b2e --- /dev/null +++ b/Library/Library.Application.Contracts/Readers/ReaderDto.cs @@ -0,0 +1,32 @@ +namespace Library.Application.Contracts.Readers; + +/// +/// DTO для получения читателя +/// +public class ReaderDto +{ + /// + /// Уникальный идентификатор читателя + /// + public required int Id { get; set; } + + /// + /// ФИО читателя + /// + public required string FullName { get; set; } + + /// + /// Адрес читателя + /// + public string? Address { get; set; } + + /// + /// Телефон читателя + /// + public required string Phone { get; set; } + + /// + /// Дата регистрации читателя + /// + public DateTime? RegistrationDate { get; set; } +} \ No newline at end of file diff --git a/Library/Library.Application/Library.Application.csproj b/Library/Library.Application/Library.Application.csproj new file mode 100644 index 000000000..5199084e3 --- /dev/null +++ b/Library/Library.Application/Library.Application.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Library/Library.Application/LibraryProfile.cs b/Library/Library.Application/LibraryProfile.cs new file mode 100644 index 000000000..fb8b4affc --- /dev/null +++ b/Library/Library.Application/LibraryProfile.cs @@ -0,0 +1,35 @@ +using AutoMapper; +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.EditionTypes; +using Library.Application.Contracts.Publishers; +using Library.Application.Contracts.Readers; +using Library.Domain.Models; + +namespace Library.Application; +/// +/// Профиль AutoMapper для сопоставления доменных сущностей и DTO библиотечного приложения +/// +public class LibraryProfile : Profile +{ + /// + /// Инициализирует правила сопоставления сущностей и DTO для операций получения и создания или обновления + /// + public LibraryProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Library/Library.Application/Services/AnalyticsService.cs b/Library/Library.Application/Services/AnalyticsService.cs new file mode 100644 index 000000000..5ac00dcee --- /dev/null +++ b/Library/Library.Application/Services/AnalyticsService.cs @@ -0,0 +1,152 @@ +using AutoMapper; +using Library.Application.Contracts; +using Library.Application.Contracts.Analytics; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.Publishers; +using Library.Application.Contracts.Readers; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + + +/// +/// Сервис аналитических запросов по доменной области библиотеки +/// +public class AnalyticsService( + IRepository bookIssueRepository, + IRepository bookRepository, + IRepository readerRepository, + IRepository publisherRepository, + IMapper mapper) : IAnalyticsService +{ + /// + /// Возвращает информацию о выданных книгах, упорядоченных по названию + /// + public async Task> GetIssuedBooksOrderedByTitle() + { + var issues = await bookIssueRepository.ReadAll(); + var books = await bookRepository.ReadAll(); + + var issuedBooks = issues + .Where(bi => bi.ReturnDate == null) + .Join(books, + bi => bi.BookId, + b => b.Id, + (bi, b) => b) + .OrderBy(b => b.Title) + .ToList(); + + return mapper.Map>(issuedBooks); + } + + /// + /// Возвращает топ 5 читателей, прочитавших больше всего книг за заданный период + /// + public async Task> GetTop5ReadersByIssuesCount(DateTime periodStart, DateTime periodEnd) + { + var issues = await bookIssueRepository.ReadAll(); + var readers = await readerRepository.ReadAll(); + + var topReaders = issues + .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) + .GroupBy(bi => bi.ReaderId) + .Select(g => new { ReaderId = g.Key, Count = g.Count() }) + .Join(readers, g => g.ReaderId, r => r.Id, (g, r) => new { Reader = r, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Reader.FullName) + .Take(5) + .Select(x => new ReaderIssuesStatDto + { + Reader = mapper.Map(x.Reader), + IssuesCount = x.Count + }) + .ToList(); + + return topReaders; + } + + /// + /// Возвращает информацию о читателях, бравших книги на наибольший период времени, упорядоченную по ФИО + /// + public async Task> GetReadersByMaxLoanDaysOrderedByFullName() + { + var issues = await bookIssueRepository.ReadAll(); + var readers = await readerRepository.ReadAll(); + + var maxDays = issues.Max(bi => bi.Days); + + var resultReaders = issues + .Where(bi => bi.Days == maxDays) + .Select(bi => bi.ReaderId) + .Distinct() + .Join(readers, id => id, r => r.Id, (id, r) => r) + .OrderBy(r => r.FullName) + .ToList(); + + return mapper.Map>(resultReaders); + } + + /// + /// Возвращает топ 5 наиболее популярных издательств за последний год + /// + public async Task> GetTop5PublishersByIssuesCountLastYear(DateTime nowUtc) + { + var issues = await bookIssueRepository.ReadAll(); + var books = await bookRepository.ReadAll(); + var publishers = await publisherRepository.ReadAll(); + + var lastYearStart = nowUtc.AddYears(-1); + var lastYearEnd = nowUtc; + + var result = issues + .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) + .Join(books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) + .GroupBy(pid => pid) + .Select(g => new { PublisherId = g.Key, Count = g.Count() }) + .Join(publishers, g => g.PublisherId, p => p.Id, (g, p) => new { Publisher = p, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Publisher.Name) + .Take(5) + .Select(x => new PublisherIssuesStatDto + { + Publisher = mapper.Map(x.Publisher), + IssuesCount = x.Count + }) + .ToList(); + + return result; + } + + /// + /// Возвращает топ 5 наименее популярных книг за последний год + /// + public async Task> GetBottom5BooksByIssuesCountLastYear(DateTime nowUtc) + { + var issues = await bookIssueRepository.ReadAll(); + var books = await bookRepository.ReadAll(); + + var lastYearStart = nowUtc.AddYears(-1); + var lastYearEnd = nowUtc; + + var issuesInPeriod = issues + .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd); + + var result = books + .GroupJoin( + issuesInPeriod, + b => b.Id, + bi => bi.BookId, + (b, joinedIssues) => new BookIssuesStatDto + { + Book = mapper.Map(b), + IssuesCount = joinedIssues.Count() + }) + .OrderBy(x => x.IssuesCount) + .ThenBy(x => x.Book.Title) + .Take(5) + .ToList(); + + return result; + } +} \ No newline at end of file diff --git a/Library/Library.Application/Services/BookIssueService.cs b/Library/Library.Application/Services/BookIssueService.cs new file mode 100644 index 000000000..7d7116064 --- /dev/null +++ b/Library/Library.Application/Services/BookIssueService.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using Library.Application.Contracts; +using Library.Application.Contracts.BookIssues; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + +/// +/// Сервис приложения для работы с выдачами книг +/// +public class BookIssueService( + IRepository bookIssueRepository, + IRepository bookRepository, + IRepository readerRepository, + IMapper mapper) : IApplicationService +{ + /// + /// Создаёт выдачу книги + /// + /// DTO для создания или обновления выдачи книги + /// DTO для получения выдачи книги + public async Task Create(BookIssueCreateUpdateDto dto) + { + _ = await bookRepository.Read(dto.BookId) + ?? throw new KeyNotFoundException($"Book with id {dto.BookId} not found"); + + _ = await readerRepository.Read(dto.ReaderId) + ?? throw new KeyNotFoundException($"Reader with id {dto.ReaderId} not found"); + + var entity = mapper.Map(dto); + + var created = await bookIssueRepository.Create(entity); + return mapper.Map(created); + } + + /// + /// Возвращает выдачу книги по идентификатору + /// + /// Идентификатор выдачи книги + /// DTO для получения выдачи книги + public async Task Get(int dtoId) + { + var entity = await bookIssueRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Book Issue with id {dtoId} not found"); + + return mapper.Map(entity); + } + + /// + /// Возвращает список выдач книг + /// + /// Список DTO для получения выдач книг + public async Task> GetAll() + { + var items = await bookIssueRepository.ReadAll(); + return [.. items.Select(mapper.Map)]; + } + + /// + /// Обновляет выдачу книги по идентификатору + /// + /// DTO для создания или обновления выдачи книги + /// Идентификатор выдачи книги + /// DTO для получения выдачи книги + public async Task Update(BookIssueCreateUpdateDto dto, int dtoId) + { + _ = await bookRepository.Read(dto.BookId) + ?? throw new KeyNotFoundException($"Book with id {dto.BookId} not found"); + + _ = await readerRepository.Read(dto.ReaderId) + ?? throw new KeyNotFoundException($"Reader with id {dto.ReaderId} not found"); + + var entity = await bookIssueRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Book Issue with id {dtoId} not found"); + + mapper.Map(dto, entity); + + var updated = await bookIssueRepository.Update(entity); + return mapper.Map(updated); + } + + /// + /// Удаляет выдачу книги по идентификатору + /// + /// Идентификатор выдачи книги + /// true если удаление выполнено иначе false + public Task Delete(int dtoId) => bookIssueRepository.Delete(dtoId); +} \ No newline at end of file diff --git a/Library/Library.Application/Services/BookService.cs b/Library/Library.Application/Services/BookService.cs new file mode 100644 index 000000000..868c6d0d2 --- /dev/null +++ b/Library/Library.Application/Services/BookService.cs @@ -0,0 +1,144 @@ +using AutoMapper; +using Library.Application.Contracts; +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.Books; +using Library.Application.Contracts.EditionTypes; +using Library.Application.Contracts.Publishers; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + +/// +/// Сервис приложения для работы с книгами +/// +public class BookService( + IRepository bookRepository, + IRepository bookIssueRepository, + IRepository publisherRepository, + IRepository editionTypeRepository, + IMapper mapper) : IBookService +{ + /// + /// Создаёт книгу + /// + /// DTO для создания или обновления книги + /// DTO для получения книги + public async Task Create(BookCreateUpdateDto dto) + { + _ = await publisherRepository.Read(dto.PublisherId) + ?? throw new KeyNotFoundException($"Publisher with id {dto.PublisherId} not found"); + + _ = await editionTypeRepository.Read(dto.EditionTypeId) + ?? throw new KeyNotFoundException($"Edition Type with id {dto.EditionTypeId} not found"); + + var entity = mapper.Map(dto); + + var created = await bookRepository.Create(entity); + return mapper.Map(created); + } + + /// + /// Возвращает книгу по идентификатору + /// + /// Идентификатор книги + /// DTO для получения книги + public async Task Get(int dtoId) + { + var entity = await bookRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Book with id {dtoId} not found"); + + return mapper.Map(entity); + } + + /// + /// Возвращает список книг + /// + /// Список DTO для получения книг + public async Task> GetAll() + { + var items = await bookRepository.ReadAll(); + return [.. items.Select(mapper.Map)]; + } + + /// + /// Обновляет книгу по идентификатору + /// + /// DTO для создания или обновления книги + /// Идентификатор книги + /// DTO для получения книги + public async Task Update(BookCreateUpdateDto dto, int dtoId) + { + _ = await publisherRepository.Read(dto.PublisherId) + ?? throw new KeyNotFoundException($"Publisher with id {dto.PublisherId} not found"); + + _ = await editionTypeRepository.Read(dto.EditionTypeId) + ?? throw new KeyNotFoundException($"Edition Type with id {dto.EditionTypeId} not found"); + + var entity = await bookRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Book with id {dtoId} not found"); + + mapper.Map(dto, entity); + + var updated = await bookRepository.Update(entity); + return mapper.Map(updated); + } + + /// + /// Удаляет книгу по идентификатору + /// + /// Идентификатор книги + /// true если удаление выполнено иначе false + public Task Delete(int dtoId) => bookRepository.Delete(dtoId); + + /// + /// Возвращает записи о выдачах книги + /// + /// Идентификатор книги + /// Список DTO для получения выдач книг + public async Task> GetIssues(int bookId) + { + _ = await bookRepository.Read(bookId) + ?? throw new KeyNotFoundException($"Book with id {bookId} not found"); + + var issues = await bookIssueRepository.ReadAll(); + + var bookIssues = issues + .Where(x => x.BookId == bookId) + .ToList(); + + return [.. bookIssues.Select(mapper.Map)]; + } + + /// + /// Возвращает вид издания книги + /// + /// Идентификатор книги + /// DTO для получения вида издания + public async Task GetEditionType(int bookId) + { + var book = await bookRepository.Read(bookId) + ?? throw new KeyNotFoundException($"Book with id {bookId} not found"); + + var editionType = await editionTypeRepository.Read(book.EditionTypeId) + ?? throw new KeyNotFoundException($"Edition Type with id {book.EditionTypeId} not found"); + + return mapper.Map(editionType); + } + + /// + /// Возвращает издательство книги + /// + /// Идентификатор книги + /// DTO для получения издательства + public async Task GetPublisher(int bookId) + { + var book = await bookRepository.Read(bookId) + ?? throw new KeyNotFoundException($"Book with id {bookId} not found"); + + var publisher = await publisherRepository.Read(book.PublisherId) + ?? throw new KeyNotFoundException($"Publisher with id {book.PublisherId} not found"); + + return mapper.Map(publisher); + } +} \ No newline at end of file diff --git a/Library/Library.Application/Services/EditionTypeService.cs b/Library/Library.Application/Services/EditionTypeService.cs new file mode 100644 index 000000000..693f7eff4 --- /dev/null +++ b/Library/Library.Application/Services/EditionTypeService.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using Library.Application.Contracts; +using Library.Application.Contracts.EditionTypes; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + +/// +/// Сервис приложения для работы с видами изданий +/// +public class EditionTypeService( + IRepository editionTypeRepository, + IMapper mapper) : IApplicationService +{ + /// + /// Создаёт вид издания + /// + /// DTO для создания или обновления вида издания + /// DTO для получения вида издания + public async Task Create(EditionTypeCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + + var created = await editionTypeRepository.Create(entity); + return mapper.Map(created); + } + + /// + /// Возвращает вид издания по идентификатору + /// + /// Идентификатор вида издания + /// DTO для получения вида издания + public async Task Get(int dtoId) + { + var entity = await editionTypeRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Edition Type with id {dtoId} not found"); + + return mapper.Map(entity); + } + + /// + /// Возвращает список видов изданий + /// + /// Список DTO для получения видов изданий + public async Task> GetAll() + { + var items = await editionTypeRepository.ReadAll(); + return [.. items.Select(mapper.Map)]; + } + + /// + /// Обновляет вид издания по идентификатору + /// + /// DTO для создания или обновления вида издания + /// Идентификатор вида издания + /// DTO для получения вида издания + public async Task Update(EditionTypeCreateUpdateDto dto, int dtoId) + { + var entity = await editionTypeRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Edition Type with id {dtoId} not found"); + + mapper.Map(dto, entity); + + var updated = await editionTypeRepository.Update(entity); + return mapper.Map(updated); + } + + /// + /// Удаляет вид издания по идентификатору + /// + /// Идентификатор вида издания + /// true если удаление выполнено иначе false + public Task Delete(int dtoId) => editionTypeRepository.Delete(dtoId); +} \ No newline at end of file diff --git a/Library/Library.Application/Services/PublisherService.cs b/Library/Library.Application/Services/PublisherService.cs new file mode 100644 index 000000000..0b951fc8a --- /dev/null +++ b/Library/Library.Application/Services/PublisherService.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using Library.Application.Contracts; +using Library.Application.Contracts.Publishers; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + +/// +/// Сервис приложения для работы с издательствами +/// +public class PublisherService( + IRepository publisherRepository, + IMapper mapper) : IApplicationService +{ + /// + /// Создаёт издательство + /// + /// DTO для создания или обновления издательства + /// DTO для получения издательства + public async Task Create(PublisherCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + + var created = await publisherRepository.Create(entity); + return mapper.Map(created); + } + + /// + /// Возвращает издательство по идентификатору + /// + /// Идентификатор издательства + /// DTO для получения издательства + public async Task Get(int dtoId) + { + var entity = await publisherRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Publisher with id {dtoId} not found"); + + return mapper.Map(entity); + } + + /// + /// Возвращает список издательств + /// + /// Список DTO для получения издательств + public async Task> GetAll() + { + var items = await publisherRepository.ReadAll(); + return [.. items.Select(mapper.Map)]; + } + + /// + /// Обновляет издательство по идентификатору + /// + /// DTO для создания или обновления издательства + /// Идентификатор издательства + /// DTO для получения издательства + public async Task Update(PublisherCreateUpdateDto dto, int dtoId) + { + var entity = await publisherRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Publisher with id {dtoId} not found"); + + mapper.Map(dto, entity); + + var updated = await publisherRepository.Update(entity); + return mapper.Map(updated); + } + + /// + /// Удаляет издательство по идентификатору + /// + /// Идентификатор издательства + /// true если удаление выполнено иначе false + public Task Delete(int dtoId) => publisherRepository.Delete(dtoId); +} \ No newline at end of file diff --git a/Library/Library.Application/Services/ReaderService.cs b/Library/Library.Application/Services/ReaderService.cs new file mode 100644 index 000000000..f43eb417a --- /dev/null +++ b/Library/Library.Application/Services/ReaderService.cs @@ -0,0 +1,95 @@ +using AutoMapper; +using Library.Application.Contracts.BookIssues; +using Library.Application.Contracts.Readers; +using Library.Domain; +using Library.Domain.Models; + +namespace Library.Application.Services; + +/// +/// Сервис приложения для работы с читателями +/// +public class ReaderService( + IRepository readerRepository, + IRepository bookIssueRepository, + IMapper mapper) : IReaderService +{ + /// + /// Создаёт читателя + /// + /// DTO для создания или обновления читателя + /// DTO для получения читателя + public async Task Create(ReaderCreateUpdateDto dto) + { + var entity = mapper.Map(dto); + + var created = await readerRepository.Create(entity); + return mapper.Map(created); + } + + /// + /// Возвращает читателя по идентификатору + /// + /// Идентификатор читателя + /// DTO для получения читателя + public async Task Get(int dtoId) + { + var entity = await readerRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Reader with id {dtoId} not found"); + + return mapper.Map(entity); + } + + /// + /// Возвращает список читателей + /// + /// Список DTO для получения читателей + public async Task> GetAll() + { + var items = await readerRepository.ReadAll(); + return [.. items.Select(mapper.Map)]; + } + + /// + /// Обновляет читателя по идентификатору + /// + /// DTO для создания или обновления читателя + /// Идентификатор читателя + /// DTO для получения читателя + public async Task Update(ReaderCreateUpdateDto dto, int dtoId) + { + var entity = await readerRepository.Read(dtoId) + ?? throw new KeyNotFoundException($"Reader with id {dtoId} not found"); + + mapper.Map(dto, entity); + + var updated = await readerRepository.Update(entity); + return mapper.Map(updated); + } + + /// + /// Удаляет читателя по идентификатору + /// + /// Идентификатор читателя + /// true если удаление выполнено иначе false + public Task Delete(int dtoId) => readerRepository.Delete(dtoId); + + /// + /// Возвращает записи о выдачах книг читателю + /// + /// Идентификатор читателя + /// Список DTO для получения выдач книг + public async Task> GetIssues(int readerId) + { + _ = await readerRepository.Read(readerId) + ?? throw new KeyNotFoundException($"Reader with id {readerId} not found"); + + var issues = await bookIssueRepository.ReadAll(); + + var readerIssues = issues + .Where(x => x.ReaderId == readerId) + .ToList(); + + return [.. readerIssues.Select(mapper.Map)]; + } +} \ No newline at end of file diff --git a/Library/Library.Domain/Data/DataSeeder.cs b/Library/Library.Domain/Data/DataSeeder.cs index 226a31ba2..0f3589f95 100644 --- a/Library/Library.Domain/Data/DataSeeder.cs +++ b/Library/Library.Domain/Data/DataSeeder.cs @@ -1,84 +1,103 @@ -using Library.Domain.Models; - -namespace Library.Domain.Data; - -/// -/// Класс, содержащий заранее подготовленные тестовые данные -/// -public class DataSeeder -{ - - private static readonly DateTime SeedNow = new(2026, 2, 19); - - public List EditionTypes { get; } = - [ - new EditionType { Id = 1, Name = "Монография" }, - new EditionType { Id = 2, Name = "Методическое пособие" }, - new EditionType { Id = 3, Name = "Энциклопедия" }, - new EditionType { Id = 4, Name = "Биография" }, - new EditionType { Id = 5, Name = "Фэнтези" }, - new EditionType { Id = 6, Name = "Техническая литература" }, - new EditionType { Id = 7, Name = "Публицистика" }, - new EditionType { Id = 8, Name = "Поэзия" }, - new EditionType { Id = 9, Name = "Психология" }, - new EditionType { Id = 10, Name = "Бизнес-литература" }, - ]; - - public List Publishers { get; } = - [ - new Publisher { Id = 1, Name = "Бином" }, - new Publisher { Id = 2, Name = "Инфра-М" }, - new Publisher { Id = 3, Name = "Юрайт" }, - new Publisher { Id = 4, Name = "ДМК Пресс" }, - new Publisher { Id = 5, Name = "Лань" }, - new Publisher { Id = 6, Name = "Альпина Паблишер" }, - new Publisher { Id = 7, Name = "МИФ" }, - new Publisher { Id = 8, Name = "Вильямс" }, - new Publisher { Id = 9, Name = "Самокат" }, - new Publisher { Id = 10, Name = "Энергия" }, - ]; - - public List Books { get; } = - [ - new Book { Id = 1, InventoryNumber = "BK-101", AlphabetCode = "И-101", Authors = "И. Ньютон", Title = "Математические начала", EditionTypeId = 1, PublisherId = 5, Year = 1687 }, - new Book { Id = 2, InventoryNumber = "BK-102", AlphabetCode = "Т-210", Authors = "А. Тьюринг", Title = "Вычислительные машины", EditionTypeId = 6, PublisherId = 4, Year = 1936 }, - new Book { Id = 3, InventoryNumber = "BK-103", AlphabetCode = "К-310", Authors = "И. Кант", Title = "Критика чистого разума", EditionTypeId = 7, PublisherId = 6, Year = 1781 }, - new Book { Id = 4, InventoryNumber = "BK-104", AlphabetCode = "Р-410", Authors = "Д. Роулинг", Title = "Тайная комната", EditionTypeId = 5, PublisherId = 9, Year = 1998 }, - new Book { Id = 5, InventoryNumber = "BK-105", AlphabetCode = "М-510", Authors = "М. Портер", Title = "Конкурентная стратегия", EditionTypeId = 10, PublisherId = 7, Year = 1980 }, - new Book { Id = 6, InventoryNumber = "BK-106", AlphabetCode = "С-610", Authors = "К. Саган", Title = "Космос", EditionTypeId = 3, PublisherId = 1, Year = 1980 }, - new Book { Id = 7, InventoryNumber = "BK-107", AlphabetCode = "Ф-710", Authors = "З. Фрейд", Title = "Толкование сновидений", EditionTypeId = 9, PublisherId = 6, Year = 1899 }, - new Book { Id = 8, InventoryNumber = "BK-108", AlphabetCode = "Л-810", Authors = "С. Лем", Title = "Солярис", EditionTypeId = 5, PublisherId = 2, Year = 1961 }, - new Book { Id = 9, InventoryNumber = "BK-109", AlphabetCode = "Х-910", Authors = "Ю. Харари", Title = "Sapiens", EditionTypeId = 4, PublisherId = 6, Year = 2011 }, - new Book { Id = 10, InventoryNumber = "BK-110", AlphabetCode = "Г-999", Authors = "А. Гауди", Title = "Архитектура форм", EditionTypeId = 1, PublisherId = 10, Year = 1925 }, - ]; - - public List Readers => new() - { - new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-3)) }, - new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddYears(-2)) }, - new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-18)) }, - new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-12)) }, - new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-10)) }, - new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-8)) }, - new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-6)) }, - new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-5)) }, - new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-4)) }, - new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = DateOnly.FromDateTime(SeedNow.AddMonths(-2)) }, - }; - - public List BookIssues => new() - { - new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = SeedNow.AddDays(-15), Days = 30 }, - new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = SeedNow.AddDays(-200), Days = 60 }, - new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = SeedNow.AddDays(-40), Days = 14 }, - new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = SeedNow.AddDays(-7), Days = 10 }, - new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = SeedNow.AddDays(-300), Days = 21 }, - new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = SeedNow.AddDays(-50), Days = 14 }, - new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = SeedNow.AddDays(-3), Days = 7 }, - new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = SeedNow.AddDays(-120), Days = 30 }, - new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = SeedNow.AddDays(-60), Days = 20 }, - new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = SeedNow.AddDays(-25), Days = 14 }, - new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = SeedNow.AddDays(-5), Days = 10 }, - new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = SeedNow.AddDays(-90), Days = 30 } - }; -} +using Library.Domain.Models; + +namespace Library.Domain.Data; + +/// +/// Класс, содержащий заранее подготовленные тестовые данные для доменной модели библиотеки +/// +public class DataSeeder +{ + /// + /// Фиксированная точка времени для детерминированных данных + /// + public static readonly DateTime SeedNowUtc = + DateTime.SpecifyKind(new DateTime(2026, 2, 19, 0, 0, 0), DateTimeKind.Utc); + + /// + /// Список видов изданий + /// + public List EditionTypes { get; } = + [ + new EditionType { Id = 1, Name = "Монография" }, + new EditionType { Id = 2, Name = "Методическое пособие" }, + new EditionType { Id = 3, Name = "Энциклопедия" }, + new EditionType { Id = 4, Name = "Биография" }, + new EditionType { Id = 5, Name = "Фэнтези" }, + new EditionType { Id = 6, Name = "Техническая литература" }, + new EditionType { Id = 7, Name = "Публицистика" }, + new EditionType { Id = 8, Name = "Поэзия" }, + new EditionType { Id = 9, Name = "Психология" }, + new EditionType { Id = 10, Name = "Бизнес-литература" }, + ]; + + /// + /// Список издательств + /// + public List Publishers { get; } = + [ + new Publisher { Id = 1, Name = "Бином" }, + new Publisher { Id = 2, Name = "Инфра-М" }, + new Publisher { Id = 3, Name = "Юрайт" }, + new Publisher { Id = 4, Name = "ДМК Пресс" }, + new Publisher { Id = 5, Name = "Лань" }, + new Publisher { Id = 6, Name = "Альпина Паблишер" }, + new Publisher { Id = 7, Name = "МИФ" }, + new Publisher { Id = 8, Name = "Вильямс" }, + new Publisher { Id = 9, Name = "Самокат" }, + new Publisher { Id = 10, Name = "Энергия" }, + ]; + + /// + /// Список книг с заполненными ссылками на издательства и виды изданий + /// + public List Books { get; } = + [ + new Book { Id = 1, InventoryNumber = "BK-101", AlphabetCode = "И-101", Authors = "И. Ньютон", Title = "Математические начала", EditionTypeId = 1, PublisherId = 5, Year = 1687 }, + new Book { Id = 2, InventoryNumber = "BK-102", AlphabetCode = "Т-210", Authors = "А. Тьюринг", Title = "Вычислительные машины", EditionTypeId = 6, PublisherId = 4, Year = 1936 }, + new Book { Id = 3, InventoryNumber = "BK-103", AlphabetCode = "К-310", Authors = "И. Кант", Title = "Критика чистого разума", EditionTypeId = 7, PublisherId = 6, Year = 1781 }, + new Book { Id = 4, InventoryNumber = "BK-104", AlphabetCode = "Р-410", Authors = "Д. Роулинг", Title = "Тайная комната", EditionTypeId = 5, PublisherId = 9, Year = 1998 }, + new Book { Id = 5, InventoryNumber = "BK-105", AlphabetCode = "М-510", Authors = "М. Портер", Title = "Конкурентная стратегия", EditionTypeId = 10, PublisherId = 7, Year = 1980 }, + new Book { Id = 6, InventoryNumber = "BK-106", AlphabetCode = "С-610", Authors = "К. Саган", Title = "Космос", EditionTypeId = 3, PublisherId = 1, Year = 1980 }, + new Book { Id = 7, InventoryNumber = "BK-107", AlphabetCode = "Ф-710", Authors = "З. Фрейд", Title = "Толкование сновидений", EditionTypeId = 9, PublisherId = 6, Year = 1899 }, + new Book { Id = 8, InventoryNumber = "BK-108", AlphabetCode = "Л-810", Authors = "С. Лем", Title = "Солярис", EditionTypeId = 5, PublisherId = 2, Year = 1961 }, + new Book { Id = 9, InventoryNumber = "BK-109", AlphabetCode = "Х-910", Authors = "Ю. Харари", Title = "Sapiens", EditionTypeId = 4, PublisherId = 6, Year = 2011 }, + new Book { Id = 10, InventoryNumber = "BK-110", AlphabetCode = "Г-999", Authors = "А. Гауди", Title = "Архитектура форм", EditionTypeId = 1, PublisherId = 10, Year = 1925 }, + ]; + + /// + /// Список читателей библиотеки, включающий персональные данные и дату регистрации + /// + public List Readers { get; } = + [ + new Reader { Id = 1, FullName = "Орлов Денис Сергеевич", Address = "ул. Березовая, 12", Phone = "89110000001", RegistrationDate = SeedNowUtc.AddMonths(-3) }, + new Reader { Id = 2, FullName = "Мельников Артем Игоревич", Address = "ул. Солнечная, 45", Phone = "89110000002", RegistrationDate = SeedNowUtc.AddYears(-2) }, + new Reader { Id = 3, FullName = "Белов Кирилл Андреевич", Address = "ул. Полевая, 7", Phone = "89110000003", RegistrationDate = SeedNowUtc.AddMonths(-18) }, + new Reader { Id = 4, FullName = "Егорова Марина Олеговна", Address = "ул. Озерная, 21", Phone = "89110000004", RegistrationDate = SeedNowUtc.AddMonths(-12) }, + new Reader { Id = 5, FullName = "Тарасов Максим Дмитриевич", Address = "ул. Лесная, 3", Phone = "89110000005", RegistrationDate = SeedNowUtc.AddMonths(-10) }, + new Reader { Id = 6, FullName = "Крылова Анастасия Павловна", Address = "ул. Школьная, 9", Phone = "89110000006", RegistrationDate = SeedNowUtc.AddMonths(-8) }, + new Reader { Id = 7, FullName = "Никитин Роман Евгеньевич", Address = "ул. Центральная, 15", Phone = "89110000007", RegistrationDate = SeedNowUtc.AddMonths(-6) }, + new Reader { Id = 8, FullName = "Волкова Дарья Ильинична", Address = "ул. Мира, 19", Phone = "89110000008", RegistrationDate = SeedNowUtc.AddMonths(-5) }, + new Reader { Id = 9, FullName = "Зайцев Павел Николаевич", Address = "ул. Новая, 8", Phone = "89110000009", RegistrationDate = SeedNowUtc.AddMonths(-4) }, + new Reader { Id = 10, FullName = "Громова София Артемовна", Address = "ул. Южная, 14", Phone = "89110000010", RegistrationDate = SeedNowUtc.AddMonths(-2) }, + ]; + + + /// + /// Список фактов выдачи книг + /// + public List BookIssues { get; } = + [ + new BookIssue { Id = 1, BookId = 1, ReaderId = 1, IssueDate = SeedNowUtc.AddDays(-15), Days = 30 }, + new BookIssue { Id = 2, BookId = 2, ReaderId = 1, IssueDate = SeedNowUtc.AddDays(-200), Days = 60 }, + new BookIssue { Id = 3, BookId = 3, ReaderId = 2, IssueDate = SeedNowUtc.AddDays(-40), Days = 14 }, + new BookIssue { Id = 4, BookId = 4, ReaderId = 2, IssueDate = SeedNowUtc.AddDays(-7), Days = 10 }, + new BookIssue { Id = 5, BookId = 5, ReaderId = 3, IssueDate = SeedNowUtc.AddDays(-300), Days = 21 }, + new BookIssue { Id = 6, BookId = 6, ReaderId = 4, IssueDate = SeedNowUtc.AddDays(-50), Days = 14 }, + new BookIssue { Id = 7, BookId = 7, ReaderId = 5, IssueDate = SeedNowUtc.AddDays(-3), Days = 7 }, + new BookIssue { Id = 8, BookId = 8, ReaderId = 6, IssueDate = SeedNowUtc.AddDays(-120), Days = 30 }, + new BookIssue { Id = 9, BookId = 9, ReaderId = 7, IssueDate = SeedNowUtc.AddDays(-60), Days = 20 }, + new BookIssue { Id = 10, BookId = 10, ReaderId = 8, IssueDate = SeedNowUtc.AddDays(-25), Days = 14 }, + new BookIssue { Id = 11, BookId = 1, ReaderId = 9, IssueDate = SeedNowUtc.AddDays(-5), Days = 10 }, + new BookIssue { Id = 12, BookId = 2, ReaderId = 10, IssueDate = SeedNowUtc.AddDays(-90), Days = 30 } + ]; +} diff --git a/Library/Library.Domain/IRepository.cs b/Library/Library.Domain/IRepository.cs new file mode 100644 index 000000000..52da9bbc6 --- /dev/null +++ b/Library/Library.Domain/IRepository.cs @@ -0,0 +1,49 @@ +namespace Library.Domain; + +/// +/// Обобщённый интерфейс репозитория, инкапсулирующий CRUD-операции над сущностью доменной модели +/// +/// Тип доменной сущности, над которой выполняются операции репозитория +/// Тип первичного ключа (идентификатора) сущности +public interface IRepository + where TEntity : class + where TKey : struct +{ + /// + /// Создаёт новую сущность и сохраняет её в источнике данных + /// + /// Экземпляр новой сущности для сохранения + /// Созданная сущность + public Task Create(TEntity entity); + + /// + /// Возвращает сущность по её идентификатору + /// + /// Идентификатор искомой сущности + /// + /// Сущность, если она найдена; иначе null + /// + public Task Read(TKey entityId); + + /// + /// Возвращает полный список сущностей данного типа + /// + /// Список сущностей + public Task> ReadAll(); + + /// + /// Обновляет существующую сущность в источнике данных + /// + /// Сущность с актуальными значениями полей + /// Обновлённая сущность + public Task Update(TEntity entity); + + /// + /// Удаляет сущность по её идентификатору + /// + /// Идентификатор удаляемой сущности + /// + /// true, если сущность была найдена и удалена; иначе false + /// + public Task Delete(TKey entityId); +} \ No newline at end of file diff --git a/Library/Library.Domain/Library.Domain.csproj b/Library/Library.Domain/Library.Domain.csproj index bb23fb7d6..fa71b7ae6 100644 --- a/Library/Library.Domain/Library.Domain.csproj +++ b/Library/Library.Domain/Library.Domain.csproj @@ -1,9 +1,9 @@ - - - - net8.0 - enable - enable - - - + + + + net8.0 + enable + enable + + + diff --git a/Library/Library.Domain/Models/Book.cs b/Library/Library.Domain/Models/Book.cs index b599cdfaa..aeeeaebc0 100644 --- a/Library/Library.Domain/Models/Book.cs +++ b/Library/Library.Domain/Models/Book.cs @@ -1,62 +1,62 @@ -namespace Library.Domain.Models; - -/// -/// Сущность книги из каталога библиотеки -/// -public class Book -{ - /// - /// Уникальный идентификатор - /// - public required int Id { get; set; } - - /// - /// Инвентарный номер - /// - public required string InventoryNumber { get; set; } - - /// - /// Шифр в алфавитном каталоге - /// - public required string AlphabetCode { get; set; } - - /// - /// Инициалы и фамилии авторов - /// - public string? Authors { get; set; } - - /// - /// Название - /// - public required string Title { get; set; } - - /// - /// Идентификатор вида издания - /// - public required int EditionTypeId { get; set; } - - /// - /// Вид издания - /// - public EditionType? EditionType { get; set; } - - /// - /// Идентификатор издательства - /// - public required int PublisherId { get; set; } - - /// - /// Издательство - /// - public Publisher? Publisher { get; set; } - - /// - /// Год издания - /// - public required int Year { get; set; } - - /// - /// Записи о выдаче книги - /// - public ICollection Issues { get; set; } = []; +namespace Library.Domain.Models; + +/// +/// Сущность книги, содержащая сведения из каталога библиотеки +/// +public class Book +{ + /// + /// Уникальный идентификатор + /// + public required int Id { get; set; } + + /// + /// Инвентарный номер + /// + public required string InventoryNumber { get; set; } + + /// + /// Шифр в алфавитном каталоге + /// + public required string AlphabetCode { get; set; } + + /// + /// Инициалы и фамилии авторов + /// + public string? Authors { get; set; } + + /// + /// Название + /// + public required string Title { get; set; } + + /// + /// Идентификатор вида издания + /// + public required int EditionTypeId { get; set; } + + /// + /// Вид издания + /// + public EditionType? EditionType { get; set; } + + /// + /// Идентификатор издательства + /// + public required int PublisherId { get; set; } + + /// + /// Издательство + /// + public Publisher? Publisher { get; set; } + + /// + /// Год издания + /// + public int Year { get; set; } + + /// + /// Записи о выдаче книги + /// + public ICollection Issues { get; set; } = []; } \ No newline at end of file diff --git a/Library/Library.Domain/Models/BookIssue.cs b/Library/Library.Domain/Models/BookIssue.cs index 81b8b8104..c9db32e8b 100644 --- a/Library/Library.Domain/Models/BookIssue.cs +++ b/Library/Library.Domain/Models/BookIssue.cs @@ -1,53 +1,52 @@ -namespace Library.Domain.Models; - -/// -/// Сущность выдачи книги читателю с указанием сроков и состояния возврата -/// -public class BookIssue -{ - /// - /// Уникальный идентификатор - /// - public required int Id { get; set; } - - /// - /// Идентификатор книги - /// - public required int BookId { get; set; } - - /// - /// Выданная книга - /// - public Book? Book { get; set; } - - /// - /// Идентификатор читателя - /// - public required int ReaderId { get; set; } - - /// - /// Читатель, которому была выдана книга - /// - public Reader? Reader { get; set; } - - /// - /// Дата выдачи книги - /// - public required DateTime IssueDate { get; set; } - - /// - /// Количество дней, на которое выдана книга - /// - public required int Days { get; set; } - - /// - /// Дата возврата книги (IssueDate + Days) - /// - public DateTime? ReturnDate => IssueDate.AddDays(Days); - - /// - /// Флаг просрочки срока возврата книги - /// - public bool IsOverdue(DateTime currentDate) => - ReturnDate == null && currentDate.Date > IssueDate.AddDays(Days).Date; +namespace Library.Domain.Models; + +/// +/// Сущность выдачи книги читателю с указанием сроков и состояния возврата +/// +public class BookIssue +{ + /// + /// Уникальный идентификатор + /// + public required int Id { get; set; } + + /// + /// Идентификатор книги + /// + public required int BookId { get; set; } + + /// + /// Выданная книга + /// + public Book? Book { get; set; } + + /// + /// Идентификатор читателя + /// + public required int ReaderId { get; set; } + + /// + /// Читатель, которому была выдана книга + /// + public Reader? Reader { get; set; } + + /// + /// Дата выдачи книги + /// + public required DateTime IssueDate { get; set; } + /// + /// Количество дней, на которое выдана книга + /// + public required int Days { get; set; } + + /// + /// Дата возврата книги, если null - книга не возвращена + /// + public DateTime? ReturnDate { get; set; } + + /// + /// Признак просрочки срока возврата книги + /// + public bool IsOverdue => + ReturnDate == null && DateTime.UtcNow.Date > IssueDate.Date.AddDays(Days); } \ No newline at end of file diff --git a/Library/Library.Domain/Models/EditionType.cs b/Library/Library.Domain/Models/EditionType.cs index 4eba925ba..204f9b6ce 100644 --- a/Library/Library.Domain/Models/EditionType.cs +++ b/Library/Library.Domain/Models/EditionType.cs @@ -1,17 +1,17 @@ -namespace Library.Domain.Models; - -/// -/// Справочник видов издания, используемый для классификации книг -/// -public class EditionType -{ - /// - /// Уникальный идентификатор - /// - public required int Id { get; set; } - - /// - /// Наименование вида издания - /// - public required string Name { get; set; } +namespace Library.Domain.Models; + +/// +/// Справочник видов издания, используемый для классификации книг +/// +public class EditionType +{ + /// + /// Уникальный идентификатор + /// + public required int Id { get; set; } + + /// + /// Наименование вида издания + /// + public required string Name { get; set; } } \ No newline at end of file diff --git a/Library/Library.Domain/Models/Publisher.cs b/Library/Library.Domain/Models/Publisher.cs index 5c1fedbac..1d5be54c6 100644 --- a/Library/Library.Domain/Models/Publisher.cs +++ b/Library/Library.Domain/Models/Publisher.cs @@ -1,17 +1,17 @@ -namespace Library.Domain.Models; - -/// -/// Справочник издательств, к которым относятся книги -/// -public class Publisher -{ - /// - /// Уникальный идентификатор - /// - public required int Id { get; set; } - - /// - /// Наименование издательства - /// - public required string Name { get; set; } +namespace Library.Domain.Models; + +/// +/// Справочник издательств, к которым относятся книги +/// +public class Publisher +{ + /// + /// Уникальный идентификатор + /// + public required int Id { get; set; } + + /// + /// Наименование издательства + /// + public required string Name { get; set; } } \ No newline at end of file diff --git a/Library/Library.Domain/Models/Reader.cs b/Library/Library.Domain/Models/Reader.cs index 3e4c87e4d..5d0f7d36d 100644 --- a/Library/Library.Domain/Models/Reader.cs +++ b/Library/Library.Domain/Models/Reader.cs @@ -1,37 +1,37 @@ -namespace Library.Domain.Models; - -/// -/// Сущность читателя библиотеки с персональными данными и историей выдач -/// -public class Reader -{ - /// - /// Уникальный идентификатор - /// - public required int Id { get; set; } - - /// - /// ФИО - /// - public required string FullName { get; set; } - - /// - /// Адрес - /// - public string? Address { get; set; } - - /// - /// Телефон - /// - public required string Phone { get; set; } - - /// - /// Дата регистрации - /// - public DateOnly? RegistrationDate { get; set; } - - /// - /// Выданные книги - /// - public ICollection BookIssues { get; set; } = []; +namespace Library.Domain.Models; + +/// +/// Сущность читателя библиотеки с персональными данными и историей выдач +/// +public class Reader +{ + /// + /// Уникальный идентификатор + /// + public required int Id { get; set; } + + /// + /// ФИО читателя + /// + public required string FullName { get; set; } + + /// + /// Адрес читателя + /// + public string? Address { get; set; } + + /// + /// Телефон читателя + /// + public required string Phone { get; set; } + + /// + /// Дата регистрации читателя + /// + public DateTime? RegistrationDate { get; set; } + + /// + /// Выданные читателю книги + /// + public ICollection BookIssues { get; set; } = []; } \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Library.Infrastructure.EfCore.csproj b/Library/Library.Infrastructure.EfCore/Library.Infrastructure.EfCore.csproj new file mode 100644 index 000000000..4fea24a67 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Library.Infrastructure.EfCore.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Library/Library.Infrastructure.EfCore/LibraryDbContext.cs b/Library/Library.Infrastructure.EfCore/LibraryDbContext.cs new file mode 100644 index 000000000..6b83d78fa --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/LibraryDbContext.cs @@ -0,0 +1,216 @@ +using Library.Domain.Data; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore; + +/// +/// Контекст EF Core для доменной модели библиотеки +/// +public class LibraryDbContext(DbContextOptions options, DataSeeder seeder) : DbContext(options) +{ + /// + /// Справочник издательств + /// + public DbSet Publishers => Set(); + + /// + /// Справочник видов издания + /// + public DbSet EditionTypes => Set(); + + /// + /// Каталог книг + /// + public DbSet Books => Set(); + + /// + /// Читатели библиотеки + /// + public DbSet Readers => Set(); + + /// + /// Факты выдачи книг + /// + public DbSet BookIssues => Set(); + + /// + /// Конфигурирует модель EF Core: таблицы, ключи, ограничения, связи, индексы и seed-данные + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.ToTable("Publishers"); + + entity.HasKey(x => x.Id); + entity.Property(x => x.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + + entity.Property(x => x.Name) + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true); + + entity.HasIndex(x => x.Name); + + entity.HasData(seeder.Publishers); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("EditionTypes"); + + entity.HasKey(x => x.Id); + entity.Property(x => x.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + + entity.Property(x => x.Name) + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true); + + entity.HasIndex(x => x.Name); + + entity.HasData(seeder.EditionTypes); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Books"); + + entity.HasKey(x => x.Id); + entity.Property(x => x.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + + entity.Property(x => x.InventoryNumber) + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false); + + entity.Property(x => x.AlphabetCode) + .IsRequired() + .HasMaxLength(50) + .IsUnicode(true); + + entity.Property(x => x.Authors) + .HasMaxLength(400) + .IsUnicode(true); + + entity.Property(x => x.Title) + .IsRequired() + .HasMaxLength(300) + .IsUnicode(true); + + entity.Property(x => x.EditionTypeId) + .IsRequired(); + + entity.Property(x => x.PublisherId) + .IsRequired(); + + entity.Property(x => x.Year) + .IsRequired(); + + entity.HasIndex(x => x.InventoryNumber).IsUnique(); + entity.HasIndex(x => x.AlphabetCode).IsUnique(); + entity.HasIndex(x => x.Title); + + entity.HasOne(x => x.EditionType) + .WithMany() + .HasForeignKey(x => x.EditionTypeId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(x => x.Publisher) + .WithMany() + .HasForeignKey(x => x.PublisherId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasMany(x => x.Issues) + .WithOne(x => x.Book) + .HasForeignKey(x => x.BookId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasData(seeder.Books); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Readers"); + + entity.HasKey(x => x.Id); + entity.Property(x => x.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + + entity.Property(x => x.FullName) + .IsRequired() + .HasMaxLength(250) + .IsUnicode(true); + + entity.Property(x => x.Address) + .HasMaxLength(300) + .IsUnicode(true); + + entity.Property(x => x.Phone) + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false); + + entity.Property(x => x.RegistrationDate) + .HasColumnType("datetime2"); + + entity.HasIndex(x => x.FullName); + entity.HasIndex(x => x.Phone); + + entity.HasMany(x => x.BookIssues) + .WithOne(x => x.Reader) + .HasForeignKey(x => x.ReaderId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasData(seeder.Readers); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("BookIssues"); + + entity.HasKey(x => x.Id); + entity.Property(x => x.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + + entity.Property(x => x.BookId) + .IsRequired(); + + entity.Property(x => x.ReaderId) + .IsRequired(); + + entity.Property(x => x.IssueDate) + .IsRequired() + .HasColumnType("datetime2"); + + entity.Property(x => x.Days) + .IsRequired(); + + entity.Property(x => x.ReturnDate) + .HasColumnType("datetime2"); + + entity.HasOne(x => x.Book) + .WithMany(x => x.Issues) + .HasForeignKey(x => x.BookId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(x => x.Reader) + .WithMany(x => x.BookIssues) + .HasForeignKey(x => x.ReaderId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasData(seeder.BookIssues); + }); + } +} \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.Designer.cs b/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.Designer.cs new file mode 100644 index 000000000..4c63702a0 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.Designer.cs @@ -0,0 +1,642 @@ +// +using System; +using Library.Infrastructure.EfCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Library.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(LibraryDbContext))] + [Migration("20260222160435_InitialCreateGuid")] + partial class InitialCreateGuid + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlphabetCode") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(true) + .HasColumnType("nvarchar(50)"); + + b.Property("Authors") + .HasMaxLength(400) + .IsUnicode(true) + .HasColumnType("nvarchar(400)"); + + b.Property("EditionTypeId") + .HasColumnType("int"); + + b.Property("InventoryNumber") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("PublisherId") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .IsUnicode(true) + .HasColumnType("nvarchar(300)"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlphabetCode") + .IsUnique(); + + b.HasIndex("EditionTypeId"); + + b.HasIndex("InventoryNumber") + .IsUnique(); + + b.HasIndex("PublisherId"); + + b.HasIndex("Title"); + + b.ToTable("Books", (string)null); + + b.HasData( + new + { + Id = 1, + AlphabetCode = "И-101", + Authors = "И. Ньютон", + EditionTypeId = 1, + InventoryNumber = "BK-101", + PublisherId = 5, + Title = "Математические начала", + Year = 1687 + }, + new + { + Id = 2, + AlphabetCode = "Т-210", + Authors = "А. Тьюринг", + EditionTypeId = 6, + InventoryNumber = "BK-102", + PublisherId = 4, + Title = "Вычислительные машины", + Year = 1936 + }, + new + { + Id = 3, + AlphabetCode = "К-310", + Authors = "И. Кант", + EditionTypeId = 7, + InventoryNumber = "BK-103", + PublisherId = 6, + Title = "Критика чистого разума", + Year = 1781 + }, + new + { + Id = 4, + AlphabetCode = "Р-410", + Authors = "Д. Роулинг", + EditionTypeId = 5, + InventoryNumber = "BK-104", + PublisherId = 9, + Title = "Тайная комната", + Year = 1998 + }, + new + { + Id = 5, + AlphabetCode = "М-510", + Authors = "М. Портер", + EditionTypeId = 10, + InventoryNumber = "BK-105", + PublisherId = 7, + Title = "Конкурентная стратегия", + Year = 1980 + }, + new + { + Id = 6, + AlphabetCode = "С-610", + Authors = "К. Саган", + EditionTypeId = 3, + InventoryNumber = "BK-106", + PublisherId = 1, + Title = "Космос", + Year = 1980 + }, + new + { + Id = 7, + AlphabetCode = "Ф-710", + Authors = "З. Фрейд", + EditionTypeId = 9, + InventoryNumber = "BK-107", + PublisherId = 6, + Title = "Толкование сновидений", + Year = 1899 + }, + new + { + Id = 8, + AlphabetCode = "Л-810", + Authors = "С. Лем", + EditionTypeId = 5, + InventoryNumber = "BK-108", + PublisherId = 2, + Title = "Солярис", + Year = 1961 + }, + new + { + Id = 9, + AlphabetCode = "Х-910", + Authors = "Ю. Харари", + EditionTypeId = 4, + InventoryNumber = "BK-109", + PublisherId = 6, + Title = "Sapiens", + Year = 2011 + }, + new + { + Id = 10, + AlphabetCode = "Г-999", + Authors = "А. Гауди", + EditionTypeId = 1, + InventoryNumber = "BK-110", + PublisherId = 10, + Title = "Архитектура форм", + Year = 1925 + }); + }); + + modelBuilder.Entity("Library.Domain.Models.BookIssue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookId") + .HasColumnType("int"); + + b.Property("Days") + .HasColumnType("int"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("ReaderId") + .HasColumnType("int"); + + b.Property("ReturnDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BookId"); + + b.HasIndex("ReaderId"); + + b.ToTable("BookIssues", (string)null); + + b.HasData( + new + { + Id = 1, + BookId = 1, + Days = 30, + IssueDate = new DateTime(2026, 2, 4, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 1 + }, + new + { + Id = 2, + BookId = 2, + Days = 60, + IssueDate = new DateTime(2025, 8, 3, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 1 + }, + new + { + Id = 3, + BookId = 3, + Days = 14, + IssueDate = new DateTime(2026, 1, 10, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 2 + }, + new + { + Id = 4, + BookId = 4, + Days = 10, + IssueDate = new DateTime(2026, 2, 12, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 2 + }, + new + { + Id = 5, + BookId = 5, + Days = 21, + IssueDate = new DateTime(2025, 4, 25, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 3 + }, + new + { + Id = 6, + BookId = 6, + Days = 14, + IssueDate = new DateTime(2025, 12, 31, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 4 + }, + new + { + Id = 7, + BookId = 7, + Days = 7, + IssueDate = new DateTime(2026, 2, 16, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 5 + }, + new + { + Id = 8, + BookId = 8, + Days = 30, + IssueDate = new DateTime(2025, 10, 22, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 6 + }, + new + { + Id = 9, + BookId = 9, + Days = 20, + IssueDate = new DateTime(2025, 12, 21, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 7 + }, + new + { + Id = 10, + BookId = 10, + Days = 14, + IssueDate = new DateTime(2026, 1, 25, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 8 + }, + new + { + Id = 11, + BookId = 1, + Days = 10, + IssueDate = new DateTime(2026, 2, 14, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 9 + }, + new + { + Id = 12, + BookId = 2, + Days = 30, + IssueDate = new DateTime(2025, 11, 21, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 10 + }); + }); + + modelBuilder.Entity("Library.Domain.Models.EditionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("EditionTypes", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Монография" + }, + new + { + Id = 2, + Name = "Методическое пособие" + }, + new + { + Id = 3, + Name = "Энциклопедия" + }, + new + { + Id = 4, + Name = "Биография" + }, + new + { + Id = 5, + Name = "Фэнтези" + }, + new + { + Id = 6, + Name = "Техническая литература" + }, + new + { + Id = 7, + Name = "Публицистика" + }, + new + { + Id = 8, + Name = "Поэзия" + }, + new + { + Id = 9, + Name = "Психология" + }, + new + { + Id = 10, + Name = "Бизнес-литература" + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Publishers", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Бином" + }, + new + { + Id = 2, + Name = "Инфра-М" + }, + new + { + Id = 3, + Name = "Юрайт" + }, + new + { + Id = 4, + Name = "ДМК Пресс" + }, + new + { + Id = 5, + Name = "Лань" + }, + new + { + Id = 6, + Name = "Альпина Паблишер" + }, + new + { + Id = 7, + Name = "МИФ" + }, + new + { + Id = 8, + Name = "Вильямс" + }, + new + { + Id = 9, + Name = "Самокат" + }, + new + { + Id = 10, + Name = "Энергия" + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Reader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(300) + .IsUnicode(true) + .HasColumnType("nvarchar(300)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(250) + .IsUnicode(true) + .HasColumnType("nvarchar(250)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("RegistrationDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("FullName"); + + b.HasIndex("Phone"); + + b.ToTable("Readers", (string)null); + + b.HasData( + new + { + Id = 1, + Address = "ул. Березовая, 12", + FullName = "Орлов Денис Сергеевич", + Phone = "89110000001", + RegistrationDate = new DateTime(2025, 11, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Address = "ул. Солнечная, 45", + FullName = "Мельников Артем Игоревич", + Phone = "89110000002", + RegistrationDate = new DateTime(2024, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 3, + Address = "ул. Полевая, 7", + FullName = "Белов Кирилл Андреевич", + Phone = "89110000003", + RegistrationDate = new DateTime(2024, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 4, + Address = "ул. Озерная, 21", + FullName = "Егорова Марина Олеговна", + Phone = "89110000004", + RegistrationDate = new DateTime(2025, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 5, + Address = "ул. Лесная, 3", + FullName = "Тарасов Максим Дмитриевич", + Phone = "89110000005", + RegistrationDate = new DateTime(2025, 4, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 6, + Address = "ул. Школьная, 9", + FullName = "Крылова Анастасия Павловна", + Phone = "89110000006", + RegistrationDate = new DateTime(2025, 6, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 7, + Address = "ул. Центральная, 15", + FullName = "Никитин Роман Евгеньевич", + Phone = "89110000007", + RegistrationDate = new DateTime(2025, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 8, + Address = "ул. Мира, 19", + FullName = "Волкова Дарья Ильинична", + Phone = "89110000008", + RegistrationDate = new DateTime(2025, 9, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 9, + Address = "ул. Новая, 8", + FullName = "Зайцев Павел Николаевич", + Phone = "89110000009", + RegistrationDate = new DateTime(2025, 10, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 10, + Address = "ул. Южная, 14", + FullName = "Громова София Артемовна", + Phone = "89110000010", + RegistrationDate = new DateTime(2025, 12, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.HasOne("Library.Domain.Models.EditionType", "EditionType") + .WithMany() + .HasForeignKey("EditionTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Library.Domain.Models.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EditionType"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("Library.Domain.Models.BookIssue", b => + { + b.HasOne("Library.Domain.Models.Book", "Book") + .WithMany("Issues") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Library.Domain.Models.Reader", "Reader") + .WithMany("BookIssues") + .HasForeignKey("ReaderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Reader"); + }); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Library.Domain.Models.Reader", b => + { + b.Navigation("BookIssues"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.cs b/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.cs new file mode 100644 index 000000000..aea3c7915 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Migrations/20260222160435_InitialCreateGuid.cs @@ -0,0 +1,282 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Library.Infrastructure.EfCore.Migrations +{ + /// + public partial class InitialCreateGuid : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EditionTypes", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EditionTypes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Publishers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Publishers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Readers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FullName = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false), + Address = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: true), + Phone = table.Column(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false), + RegistrationDate = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Readers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + InventoryNumber = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + AlphabetCode = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Authors = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), + Title = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + EditionTypeId = table.Column(type: "int", nullable: false), + PublisherId = table.Column(type: "int", nullable: false), + Year = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_EditionTypes_EditionTypeId", + column: x => x.EditionTypeId, + principalTable: "EditionTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Books_Publishers_PublisherId", + column: x => x.PublisherId, + principalTable: "Publishers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BookIssues", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + BookId = table.Column(type: "int", nullable: false), + ReaderId = table.Column(type: "int", nullable: false), + IssueDate = table.Column(type: "datetime2", nullable: false), + Days = table.Column(type: "int", nullable: false), + ReturnDate = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BookIssues", x => x.Id); + table.ForeignKey( + name: "FK_BookIssues_Books_BookId", + column: x => x.BookId, + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_BookIssues_Readers_ReaderId", + column: x => x.ReaderId, + principalTable: "Readers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.InsertData( + table: "EditionTypes", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Монография" }, + { 2, "Методическое пособие" }, + { 3, "Энциклопедия" }, + { 4, "Биография" }, + { 5, "Фэнтези" }, + { 6, "Техническая литература" }, + { 7, "Публицистика" }, + { 8, "Поэзия" }, + { 9, "Психология" }, + { 10, "Бизнес-литература" } + }); + + migrationBuilder.InsertData( + table: "Publishers", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Бином" }, + { 2, "Инфра-М" }, + { 3, "Юрайт" }, + { 4, "ДМК Пресс" }, + { 5, "Лань" }, + { 6, "Альпина Паблишер" }, + { 7, "МИФ" }, + { 8, "Вильямс" }, + { 9, "Самокат" }, + { 10, "Энергия" } + }); + + migrationBuilder.InsertData( + table: "Readers", + columns: new[] { "Id", "Address", "FullName", "Phone", "RegistrationDate" }, + values: new object[,] + { + { 1, "ул. Березовая, 12", "Орлов Денис Сергеевич", "89110000001", new DateTime(2025, 11, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 2, "ул. Солнечная, 45", "Мельников Артем Игоревич", "89110000002", new DateTime(2024, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 3, "ул. Полевая, 7", "Белов Кирилл Андреевич", "89110000003", new DateTime(2024, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 4, "ул. Озерная, 21", "Егорова Марина Олеговна", "89110000004", new DateTime(2025, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 5, "ул. Лесная, 3", "Тарасов Максим Дмитриевич", "89110000005", new DateTime(2025, 4, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 6, "ул. Школьная, 9", "Крылова Анастасия Павловна", "89110000006", new DateTime(2025, 6, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 7, "ул. Центральная, 15", "Никитин Роман Евгеньевич", "89110000007", new DateTime(2025, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 8, "ул. Мира, 19", "Волкова Дарья Ильинична", "89110000008", new DateTime(2025, 9, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 9, "ул. Новая, 8", "Зайцев Павел Николаевич", "89110000009", new DateTime(2025, 10, 19, 0, 0, 0, 0, DateTimeKind.Utc) }, + { 10, "ул. Южная, 14", "Громова София Артемовна", "89110000010", new DateTime(2025, 12, 19, 0, 0, 0, 0, DateTimeKind.Utc) } + }); + + migrationBuilder.InsertData( + table: "Books", + columns: new[] { "Id", "AlphabetCode", "Authors", "EditionTypeId", "InventoryNumber", "PublisherId", "Title", "Year" }, + values: new object[,] + { + { 1, "И-101", "И. Ньютон", 1, "BK-101", 5, "Математические начала", 1687 }, + { 2, "Т-210", "А. Тьюринг", 6, "BK-102", 4, "Вычислительные машины", 1936 }, + { 3, "К-310", "И. Кант", 7, "BK-103", 6, "Критика чистого разума", 1781 }, + { 4, "Р-410", "Д. Роулинг", 5, "BK-104", 9, "Тайная комната", 1998 }, + { 5, "М-510", "М. Портер", 10, "BK-105", 7, "Конкурентная стратегия", 1980 }, + { 6, "С-610", "К. Саган", 3, "BK-106", 1, "Космос", 1980 }, + { 7, "Ф-710", "З. Фрейд", 9, "BK-107", 6, "Толкование сновидений", 1899 }, + { 8, "Л-810", "С. Лем", 5, "BK-108", 2, "Солярис", 1961 }, + { 9, "Х-910", "Ю. Харари", 4, "BK-109", 6, "Sapiens", 2011 }, + { 10, "Г-999", "А. Гауди", 1, "BK-110", 10, "Архитектура форм", 1925 } + }); + + migrationBuilder.InsertData( + table: "BookIssues", + columns: new[] { "Id", "BookId", "Days", "IssueDate", "ReaderId", "ReturnDate" }, + values: new object[,] + { + { 1, 1, 30, new DateTime(2026, 2, 4, 0, 0, 0, 0, DateTimeKind.Utc), 1, null }, + { 2, 2, 60, new DateTime(2025, 8, 3, 0, 0, 0, 0, DateTimeKind.Utc), 1, null }, + { 3, 3, 14, new DateTime(2026, 1, 10, 0, 0, 0, 0, DateTimeKind.Utc), 2, null }, + { 4, 4, 10, new DateTime(2026, 2, 12, 0, 0, 0, 0, DateTimeKind.Utc), 2, null }, + { 5, 5, 21, new DateTime(2025, 4, 25, 0, 0, 0, 0, DateTimeKind.Utc), 3, null }, + { 6, 6, 14, new DateTime(2025, 12, 31, 0, 0, 0, 0, DateTimeKind.Utc), 4, null }, + { 7, 7, 7, new DateTime(2026, 2, 16, 0, 0, 0, 0, DateTimeKind.Utc), 5, null }, + { 8, 8, 30, new DateTime(2025, 10, 22, 0, 0, 0, 0, DateTimeKind.Utc), 6, null }, + { 9, 9, 20, new DateTime(2025, 12, 21, 0, 0, 0, 0, DateTimeKind.Utc), 7, null }, + { 10, 10, 14, new DateTime(2026, 1, 25, 0, 0, 0, 0, DateTimeKind.Utc), 8, null }, + { 11, 1, 10, new DateTime(2026, 2, 14, 0, 0, 0, 0, DateTimeKind.Utc), 9, null }, + { 12, 2, 30, new DateTime(2025, 11, 21, 0, 0, 0, 0, DateTimeKind.Utc), 10, null } + }); + + migrationBuilder.CreateIndex( + name: "IX_BookIssues_BookId", + table: "BookIssues", + column: "BookId"); + + migrationBuilder.CreateIndex( + name: "IX_BookIssues_ReaderId", + table: "BookIssues", + column: "ReaderId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_AlphabetCode", + table: "Books", + column: "AlphabetCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Books_EditionTypeId", + table: "Books", + column: "EditionTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_InventoryNumber", + table: "Books", + column: "InventoryNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Books_PublisherId", + table: "Books", + column: "PublisherId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_Title", + table: "Books", + column: "Title"); + + migrationBuilder.CreateIndex( + name: "IX_EditionTypes_Name", + table: "EditionTypes", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_Publishers_Name", + table: "Publishers", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_Readers_FullName", + table: "Readers", + column: "FullName"); + + migrationBuilder.CreateIndex( + name: "IX_Readers_Phone", + table: "Readers", + column: "Phone"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BookIssues"); + + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Readers"); + + migrationBuilder.DropTable( + name: "EditionTypes"); + + migrationBuilder.DropTable( + name: "Publishers"); + } + } +} diff --git a/Library/Library.Infrastructure.EfCore/Migrations/LibraryDbContextModelSnapshot.cs b/Library/Library.Infrastructure.EfCore/Migrations/LibraryDbContextModelSnapshot.cs new file mode 100644 index 000000000..542be0894 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Migrations/LibraryDbContextModelSnapshot.cs @@ -0,0 +1,639 @@ +// +using System; +using Library.Infrastructure.EfCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Library.Infrastructure.EfCore.Migrations +{ + [DbContext(typeof(LibraryDbContext))] + partial class LibraryDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlphabetCode") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(true) + .HasColumnType("nvarchar(50)"); + + b.Property("Authors") + .HasMaxLength(400) + .IsUnicode(true) + .HasColumnType("nvarchar(400)"); + + b.Property("EditionTypeId") + .HasColumnType("int"); + + b.Property("InventoryNumber") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("PublisherId") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .IsUnicode(true) + .HasColumnType("nvarchar(300)"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlphabetCode") + .IsUnique(); + + b.HasIndex("EditionTypeId"); + + b.HasIndex("InventoryNumber") + .IsUnique(); + + b.HasIndex("PublisherId"); + + b.HasIndex("Title"); + + b.ToTable("Books", (string)null); + + b.HasData( + new + { + Id = 1, + AlphabetCode = "И-101", + Authors = "И. Ньютон", + EditionTypeId = 1, + InventoryNumber = "BK-101", + PublisherId = 5, + Title = "Математические начала", + Year = 1687 + }, + new + { + Id = 2, + AlphabetCode = "Т-210", + Authors = "А. Тьюринг", + EditionTypeId = 6, + InventoryNumber = "BK-102", + PublisherId = 4, + Title = "Вычислительные машины", + Year = 1936 + }, + new + { + Id = 3, + AlphabetCode = "К-310", + Authors = "И. Кант", + EditionTypeId = 7, + InventoryNumber = "BK-103", + PublisherId = 6, + Title = "Критика чистого разума", + Year = 1781 + }, + new + { + Id = 4, + AlphabetCode = "Р-410", + Authors = "Д. Роулинг", + EditionTypeId = 5, + InventoryNumber = "BK-104", + PublisherId = 9, + Title = "Тайная комната", + Year = 1998 + }, + new + { + Id = 5, + AlphabetCode = "М-510", + Authors = "М. Портер", + EditionTypeId = 10, + InventoryNumber = "BK-105", + PublisherId = 7, + Title = "Конкурентная стратегия", + Year = 1980 + }, + new + { + Id = 6, + AlphabetCode = "С-610", + Authors = "К. Саган", + EditionTypeId = 3, + InventoryNumber = "BK-106", + PublisherId = 1, + Title = "Космос", + Year = 1980 + }, + new + { + Id = 7, + AlphabetCode = "Ф-710", + Authors = "З. Фрейд", + EditionTypeId = 9, + InventoryNumber = "BK-107", + PublisherId = 6, + Title = "Толкование сновидений", + Year = 1899 + }, + new + { + Id = 8, + AlphabetCode = "Л-810", + Authors = "С. Лем", + EditionTypeId = 5, + InventoryNumber = "BK-108", + PublisherId = 2, + Title = "Солярис", + Year = 1961 + }, + new + { + Id = 9, + AlphabetCode = "Х-910", + Authors = "Ю. Харари", + EditionTypeId = 4, + InventoryNumber = "BK-109", + PublisherId = 6, + Title = "Sapiens", + Year = 2011 + }, + new + { + Id = 10, + AlphabetCode = "Г-999", + Authors = "А. Гауди", + EditionTypeId = 1, + InventoryNumber = "BK-110", + PublisherId = 10, + Title = "Архитектура форм", + Year = 1925 + }); + }); + + modelBuilder.Entity("Library.Domain.Models.BookIssue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookId") + .HasColumnType("int"); + + b.Property("Days") + .HasColumnType("int"); + + b.Property("IssueDate") + .HasColumnType("datetime2"); + + b.Property("ReaderId") + .HasColumnType("int"); + + b.Property("ReturnDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BookId"); + + b.HasIndex("ReaderId"); + + b.ToTable("BookIssues", (string)null); + + b.HasData( + new + { + Id = 1, + BookId = 1, + Days = 30, + IssueDate = new DateTime(2026, 2, 4, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 1 + }, + new + { + Id = 2, + BookId = 2, + Days = 60, + IssueDate = new DateTime(2025, 8, 3, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 1 + }, + new + { + Id = 3, + BookId = 3, + Days = 14, + IssueDate = new DateTime(2026, 1, 10, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 2 + }, + new + { + Id = 4, + BookId = 4, + Days = 10, + IssueDate = new DateTime(2026, 2, 12, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 2 + }, + new + { + Id = 5, + BookId = 5, + Days = 21, + IssueDate = new DateTime(2025, 4, 25, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 3 + }, + new + { + Id = 6, + BookId = 6, + Days = 14, + IssueDate = new DateTime(2025, 12, 31, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 4 + }, + new + { + Id = 7, + BookId = 7, + Days = 7, + IssueDate = new DateTime(2026, 2, 16, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 5 + }, + new + { + Id = 8, + BookId = 8, + Days = 30, + IssueDate = new DateTime(2025, 10, 22, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 6 + }, + new + { + Id = 9, + BookId = 9, + Days = 20, + IssueDate = new DateTime(2025, 12, 21, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 7 + }, + new + { + Id = 10, + BookId = 10, + Days = 14, + IssueDate = new DateTime(2026, 1, 25, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 8 + }, + new + { + Id = 11, + BookId = 1, + Days = 10, + IssueDate = new DateTime(2026, 2, 14, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 9 + }, + new + { + Id = 12, + BookId = 2, + Days = 30, + IssueDate = new DateTime(2025, 11, 21, 0, 0, 0, 0, DateTimeKind.Utc), + ReaderId = 10 + }); + }); + + modelBuilder.Entity("Library.Domain.Models.EditionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("EditionTypes", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Монография" + }, + new + { + Id = 2, + Name = "Методическое пособие" + }, + new + { + Id = 3, + Name = "Энциклопедия" + }, + new + { + Id = 4, + Name = "Биография" + }, + new + { + Id = 5, + Name = "Фэнтези" + }, + new + { + Id = 6, + Name = "Техническая литература" + }, + new + { + Id = 7, + Name = "Публицистика" + }, + new + { + Id = 8, + Name = "Поэзия" + }, + new + { + Id = 9, + Name = "Психология" + }, + new + { + Id = 10, + Name = "Бизнес-литература" + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Publishers", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "Бином" + }, + new + { + Id = 2, + Name = "Инфра-М" + }, + new + { + Id = 3, + Name = "Юрайт" + }, + new + { + Id = 4, + Name = "ДМК Пресс" + }, + new + { + Id = 5, + Name = "Лань" + }, + new + { + Id = 6, + Name = "Альпина Паблишер" + }, + new + { + Id = 7, + Name = "МИФ" + }, + new + { + Id = 8, + Name = "Вильямс" + }, + new + { + Id = 9, + Name = "Самокат" + }, + new + { + Id = 10, + Name = "Энергия" + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Reader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(300) + .IsUnicode(true) + .HasColumnType("nvarchar(300)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(250) + .IsUnicode(true) + .HasColumnType("nvarchar(250)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("RegistrationDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("FullName"); + + b.HasIndex("Phone"); + + b.ToTable("Readers", (string)null); + + b.HasData( + new + { + Id = 1, + Address = "ул. Березовая, 12", + FullName = "Орлов Денис Сергеевич", + Phone = "89110000001", + RegistrationDate = new DateTime(2025, 11, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 2, + Address = "ул. Солнечная, 45", + FullName = "Мельников Артем Игоревич", + Phone = "89110000002", + RegistrationDate = new DateTime(2024, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 3, + Address = "ул. Полевая, 7", + FullName = "Белов Кирилл Андреевич", + Phone = "89110000003", + RegistrationDate = new DateTime(2024, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 4, + Address = "ул. Озерная, 21", + FullName = "Егорова Марина Олеговна", + Phone = "89110000004", + RegistrationDate = new DateTime(2025, 2, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 5, + Address = "ул. Лесная, 3", + FullName = "Тарасов Максим Дмитриевич", + Phone = "89110000005", + RegistrationDate = new DateTime(2025, 4, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 6, + Address = "ул. Школьная, 9", + FullName = "Крылова Анастасия Павловна", + Phone = "89110000006", + RegistrationDate = new DateTime(2025, 6, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 7, + Address = "ул. Центральная, 15", + FullName = "Никитин Роман Евгеньевич", + Phone = "89110000007", + RegistrationDate = new DateTime(2025, 8, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 8, + Address = "ул. Мира, 19", + FullName = "Волкова Дарья Ильинична", + Phone = "89110000008", + RegistrationDate = new DateTime(2025, 9, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 9, + Address = "ул. Новая, 8", + FullName = "Зайцев Павел Николаевич", + Phone = "89110000009", + RegistrationDate = new DateTime(2025, 10, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }, + new + { + Id = 10, + Address = "ул. Южная, 14", + FullName = "Громова София Артемовна", + Phone = "89110000010", + RegistrationDate = new DateTime(2025, 12, 19, 0, 0, 0, 0, DateTimeKind.Utc) + }); + }); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.HasOne("Library.Domain.Models.EditionType", "EditionType") + .WithMany() + .HasForeignKey("EditionTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Library.Domain.Models.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EditionType"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("Library.Domain.Models.BookIssue", b => + { + b.HasOne("Library.Domain.Models.Book", "Book") + .WithMany("Issues") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Library.Domain.Models.Reader", "Reader") + .WithMany("BookIssues") + .HasForeignKey("ReaderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Reader"); + }); + + modelBuilder.Entity("Library.Domain.Models.Book", b => + { + b.Navigation("Issues"); + }); + + modelBuilder.Entity("Library.Domain.Models.Reader", b => + { + b.Navigation("BookIssues"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Library/Library.Infrastructure.EfCore/Repositories/BookIssueRepository.cs b/Library/Library.Infrastructure.EfCore/Repositories/BookIssueRepository.cs new file mode 100644 index 000000000..54a53293a --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Repositories/BookIssueRepository.cs @@ -0,0 +1,75 @@ +using Library.Domain; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий фактов выдачи книг BookIssue +/// +public class BookIssueRepository(LibraryDbContext db) : IRepository +{ + /// + /// Создаёт новую выдачу книги и сохраняет её в базе данных + /// + public async Task Create(BookIssue entity) + { + db.BookIssues.Add(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Возвращает факт выдачи книги по идентификатору вместе с читателем и книгой + /// + public async Task Read(int entityId) + { + return await db.BookIssues + .Include(x => x.Reader) + .Include(x => x.Book) + .ThenInclude(b => b!.Publisher) + .Include(x => x.Book) + .ThenInclude(b => b!.EditionType) + .FirstOrDefaultAsync(x => x.Id == entityId); + } + + /// + /// Возвращает список всех фактов выдачи книг вместе с читателями и книгами + /// + public async Task> ReadAll() + { + return await db.BookIssues + .AsNoTracking() + .Include(x => x.Reader) + .Include(x => x.Book) + .ThenInclude(b => b!.Publisher) + .Include(x => x.Book) + .ThenInclude(b => b!.EditionType) + .OrderBy(x => x.Id) + .ToListAsync(); + } + + /// + /// Обновляет факт выдачи книги + /// + public async Task Update(BookIssue entity) + { + db.BookIssues.Update(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет факт выдачи книги по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await db.BookIssues.FirstOrDefaultAsync(x => x.Id == entityId); + if (entity is null) + return false; + + db.BookIssues.Remove(entity); + await db.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Repositories/BookRepository.cs b/Library/Library.Infrastructure.EfCore/Repositories/BookRepository.cs new file mode 100644 index 000000000..24a120659 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Repositories/BookRepository.cs @@ -0,0 +1,70 @@ +using Library.Domain; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий книг Book +/// +public class BookRepository(LibraryDbContext db) : IRepository +{ + /// + /// Создаёт новую книгу и сохраняет её в базе данных + /// + public async Task Create(Book entity) + { + db.Books.Add(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Возвращает книгу по идентификатору вместе с издательством и видом издания + /// + public async Task Read(int entityId) + { + return await db.Books + .Include(x => x.Publisher) + .Include(x => x.EditionType) + .Include(x => x.Issues) + .FirstOrDefaultAsync(x => x.Id == entityId); + } + + /// + /// Возвращает список всех книг вместе с издательством и видом издания + /// + public async Task> ReadAll() + { + return await db.Books + .AsNoTracking() + .Include(x => x.Publisher) + .Include(x => x.EditionType) + .OrderBy(x => x.Id) + .ToListAsync(); + } + + /// + /// Обновляет существующую книгу + /// + public async Task Update(Book entity) + { + db.Books.Update(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет книгу по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await db.Books.FirstOrDefaultAsync(x => x.Id == entityId); + if (entity is null) + return false; + + db.Books.Remove(entity); + await db.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Repositories/EditionTypeRepository.cs b/Library/Library.Infrastructure.EfCore/Repositories/EditionTypeRepository.cs new file mode 100644 index 000000000..f6170b913 --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Repositories/EditionTypeRepository.cs @@ -0,0 +1,65 @@ +using Library.Domain; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий видов изданий EditionType +/// +public class EditionTypeRepository(LibraryDbContext db) : IRepository +{ + /// + /// Создаёт новый вид издания и сохраняет его в базе данных + /// + public async Task Create(EditionType entity) + { + db.EditionTypes.Add(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Возвращает вид издания по идентификатору + /// + public async Task Read(int entityId) + { + return await db.EditionTypes + .FirstOrDefaultAsync(x => x.Id == entityId); + } + + /// + /// Возвращает список всех видов изданий + /// + public async Task> ReadAll() + { + return await db.EditionTypes + .AsNoTracking() + .OrderBy(x => x.Id) + .ToListAsync(); + } + + /// + /// Обновляет существующий вид издания + /// + public async Task Update(EditionType entity) + { + db.EditionTypes.Update(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет вид издания по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await db.EditionTypes.FirstOrDefaultAsync(x => x.Id == entityId); + if (entity is null) + return false; + + db.EditionTypes.Remove(entity); + await db.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Repositories/PublisherRepository.cs b/Library/Library.Infrastructure.EfCore/Repositories/PublisherRepository.cs new file mode 100644 index 000000000..d57a1297a --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Repositories/PublisherRepository.cs @@ -0,0 +1,65 @@ +using Library.Domain; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий издательств Publisher +/// +public class PublisherRepository(LibraryDbContext db) : IRepository +{ + /// + /// Создаёт новое издательство и сохраняет его в базе данных + /// + public async Task Create(Publisher entity) + { + db.Publishers.Add(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Возвращает издательство по идентификатору + /// + public async Task Read(int entityId) + { + return await db.Publishers + .FirstOrDefaultAsync(x => x.Id == entityId); + } + + /// + /// Возвращает список всех издательств + /// + public async Task> ReadAll() + { + return await db.Publishers + .AsNoTracking() + .OrderBy(x => x.Id) + .ToListAsync(); + } + + /// + /// Обновляет существующее издательство + /// + public async Task Update(Publisher entity) + { + db.Publishers.Update(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет издательство по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await db.Publishers.FirstOrDefaultAsync(x => x.Id == entityId); + if (entity is null) + return false; + + db.Publishers.Remove(entity); + await db.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Library/Library.Infrastructure.EfCore/Repositories/ReaderRepository.cs b/Library/Library.Infrastructure.EfCore/Repositories/ReaderRepository.cs new file mode 100644 index 000000000..6c659cd9f --- /dev/null +++ b/Library/Library.Infrastructure.EfCore/Repositories/ReaderRepository.cs @@ -0,0 +1,77 @@ +using Library.Domain; +using Library.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Library.Infrastructure.EfCore.Repositories; + +/// +/// Репозиторий читателей Reader +/// +public class ReaderRepository(LibraryDbContext db) : IRepository +{ + /// + /// Создаёт нового читателя и сохраняет его в базе данных + /// + public async Task Create(Reader entity) + { + db.Readers.Add(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Возвращает читателя по идентификатору вместе с историей выдач и книгами + /// + public async Task Read(int entityId) + { + return await db.Readers + .Include(r => r.BookIssues) + .ThenInclude(i => i.Book) + .ThenInclude(b => b!.Publisher) + .Include(r => r.BookIssues) + .ThenInclude(i => i.Book) + .ThenInclude(b => b!.EditionType) + .FirstOrDefaultAsync(r => r.Id == entityId); + } + + /// + /// Возвращает список всех читателей + /// + public async Task> ReadAll() + { + return await db.Readers + .AsNoTracking() + .Include(r => r.BookIssues) + .ThenInclude(i => i.Book) + .ThenInclude(b => b!.Publisher) + .Include(r => r.BookIssues) + .ThenInclude(i => i.Book) + .ThenInclude(b => b!.EditionType) + .OrderBy(x => x.Id) + .ToListAsync(); + } + + /// + /// Обновляет данные читателя + /// + public async Task Update(Reader entity) + { + db.Readers.Update(entity); + await db.SaveChangesAsync(); + return entity; + } + + /// + /// Удаляет читателя по идентификатору + /// + public async Task Delete(int entityId) + { + var entity = await db.Readers.FirstOrDefaultAsync(x => x.Id == entityId); + if (entity is null) + return false; + + db.Readers.Remove(entity); + await db.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/Library/Library.ServiceDefaults/Extensions.cs b/Library/Library.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..b72c8753c --- /dev/null +++ b/Library/Library.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Library/Library.ServiceDefaults/Library.ServiceDefaults.csproj b/Library/Library.ServiceDefaults/Library.ServiceDefaults.csproj new file mode 100644 index 000000000..1b6e209a7 --- /dev/null +++ b/Library/Library.ServiceDefaults/Library.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/Library/Library.Tests/Library.Tests.csproj b/Library/Library.Tests/Library.Tests.csproj index 6f0ecb3b5..f2130cc11 100644 --- a/Library/Library.Tests/Library.Tests.csproj +++ b/Library/Library.Tests/Library.Tests.csproj @@ -1,27 +1,27 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index c6384e67e..67908a481 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -1,151 +1,145 @@ -using Library.Domain.Data; - -namespace Library.Tests; - -/// -/// Набор unit тестов для тестирования доменной области -/// -public class LibraryTests -{ - /// - /// Тестовые данные библиотеки - /// - private readonly DataSeeder _dataSeeder = new(); - - /// - /// Контрольная дата, используемая в тестах - /// - private readonly DateTime _now = new(2026, 2, 19); - - /// - /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам - /// - [Fact] - public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() - { - var actualBookIds = _dataSeeder.BookIssues - .Where(bi => _now.Date < bi.ReturnDate) - .Join(_dataSeeder.Books, - bi => bi.BookId, - b => b.Id, - (bi, b) => new { bi, b }) - .OrderBy(x => x.b.Title) - .Select(x => x.b.Id) - .ToList(); - - var expectedBookIds = new List { 1, 1, 4, 7 }; - - Assert.Equal(expectedBookIds, actualBookIds); - } - - /// - /// Топ 5 читателей за последний год по количеству выдач и сравнивает по Id и по количествам - /// - [Fact] - public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() - { - var periodStart = _now.AddYears(-1); - var periodEnd = _now; - - var topReaders = _dataSeeder.BookIssues - .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) - .GroupBy(bi => bi.ReaderId) - .Select(g => new { ReaderId = g.Key, Count = g.Count() }) - .Join(_dataSeeder.Readers, g => g.ReaderId, r => r.Id, (g, r) => new { r.Id, r.FullName, g.Count }) - .OrderByDescending(x => x.Count) - .ThenBy(x => x.FullName) - .Take(5) - .ToList(); - - var actualIds = topReaders.Select(x => x.Id).ToList(); - var actualCounts = topReaders.Select(x => x.Count).ToList(); - - var expectedIds = new List { 2, 1, 3, 8, 10 }; - var expectedCounts = new List { 2, 2, 1, 1, 1 }; - - Assert.Equal(expectedIds, actualIds); - Assert.Equal(expectedCounts, actualCounts); - } - - /// - /// Читатели, у которых есть выдачи с максимальным количеством дней, и сортирует их по ФИО - /// - [Fact] - public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() - { - var maxDays = _dataSeeder.BookIssues.Max(bi => bi.Days); - - var readersWithMaxDays = _dataSeeder.BookIssues - .Where(bi => bi.Days == maxDays) - .Select(bi => bi.ReaderId) - .Distinct() - .Join(_dataSeeder.Readers, id => id, r => r.Id, (id, r) => new { r.Id, r.FullName }) - .OrderBy(r => r.FullName) - .Select(r => r.Id) - .ToList(); - - var expectedId = 1; - var expectedDays = 60; - - Assert.Single(readersWithMaxDays); - Assert.Equal(expectedDays, maxDays); - Assert.Equal(expectedId, readersWithMaxDays[0]); - } - - /// - /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам - /// - [Fact] - public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() - { - var lastYearStart = _now.AddYears(-1); - var lastYearEnd = _now; - - var topPublishers = _dataSeeder.BookIssues - .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) - .Join(_dataSeeder.Books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) - .GroupBy(pid => pid) - .Select(g => new { PublisherId = g.Key, Count = g.Count() }) - .Join(_dataSeeder.Publishers, g => g.PublisherId, p => p.Id, (g, p) => new { p.Id, p.Name, g.Count }) - .OrderByDescending(x => x.Count) - .ThenBy(x => x.Name) - .Take(5) - .ToList(); - - var actualPublisherIds = topPublishers.Select(x => x.Id).ToList(); - var actualCounts = topPublishers.Select(x => x.Count).ToList(); - - var expectedPublisherIds = new List { 6, 4, 5, 1, 2 }; - var expectedCounts = new List { 3, 2, 2, 1, 1 }; - - Assert.Equal(expectedPublisherIds, actualPublisherIds); - Assert.Equal(expectedCounts, actualCounts); - } - - /// - /// Топ 5 наименее популярных книг за последний год сравнение по Id и количествам - /// - [Fact] - public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() - { - var lastYearStart = _now.AddYears(-1); - var lastYearEnd = _now; - - var bookCounts = _dataSeeder.Books - .GroupJoin( - _dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), - b => b.Id, - bi => bi.BookId, - (b, issues) => new { Book = b, Count = issues.Count() } - ) - .OrderBy(x => x.Count) - .ThenBy(x => x.Book.Title, StringComparer.Ordinal) - .Take(5) - .ToList(); - - var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); - var expectedBookIds = new List { 9, 10, 5, 6, 3 }; - - Assert.Equal(expectedBookIds, actualBookIds); - } -} +using Library.Domain.Data; + +namespace Library.Tests; + +/// +/// Набор unit тестов для тестирования доменной области +/// +public class LibraryTests(DataSeeder dataSeeder) : IClassFixture +{ + /// + /// Проверяет что активные выдачи сортируются по названию книги и возвращают ожидаемый порядок идентификаторов книг + /// + [Fact] + public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() + { + var actualBookIds = dataSeeder.BookIssues + .Where(bi => bi.ReturnDate == null) + .Join(dataSeeder.Books, + bi => bi.BookId, + b => b.Id, + (bi, b) => new { bi, b }) + .OrderBy(x => x.b.Title) + .Select(x => x.b.Id) + .ToList(); + + var expectedBookIds = new List { 10, 2, 2, 5, 6, 3, 1, 1, 8, 4, 7, 9}; + + Assert.Equal(expectedBookIds, actualBookIds); + } + + /// + /// Топ 5 читателей за последний год по количеству выдач и сравнивает по Id и по количествам + /// + [Fact] + public void Top5Readers_ByIssuesCountInPeriod_ReturnsExpectedTop5() + { + var periodStart = DataSeeder.SeedNowUtc.AddYears(-1); + var periodEnd = DataSeeder.SeedNowUtc; + + var topReaders = dataSeeder.BookIssues + .Where(bi => bi.IssueDate >= periodStart && bi.IssueDate <= periodEnd) + .GroupBy(bi => bi.ReaderId) + .Select(g => new { ReaderId = g.Key, Count = g.Count() }) + .Join(dataSeeder.Readers, g => g.ReaderId, r => r.Id, (g, r) => new { r.Id, r.FullName, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.FullName) + .Take(5) + .ToList(); + + var actualIds = topReaders.Select(x => x.Id).ToList(); + var actualCounts = topReaders.Select(x => x.Count).ToList(); + + var expectedIds = new List { 2, 1, 3, 8, 10 }; + var expectedCounts = new List { 2, 2, 1, 1, 1 }; + + Assert.Equal(expectedIds, actualIds); + Assert.Equal(expectedCounts, actualCounts); + } + + /// + /// Читатели, у которых есть выдачи с максимальным количеством дней, и сортирует их по ФИО + /// + [Fact] + public void Readers_ByMaxLoanDaysOrderedByFullName_ReturnsExpected() + { + var maxDays = dataSeeder.BookIssues.Max(bi => bi.Days); + + var readersWithMaxDays = dataSeeder.BookIssues + .Where(bi => bi.Days == maxDays) + .Select(bi => bi.ReaderId) + .Distinct() + .Join(dataSeeder.Readers, id => id, r => r.Id, (id, r) => new { r.Id, r.FullName }) + .OrderBy(r => r.FullName) + .Select(r => r.Id) + .ToList(); + + var expectedId = 1; + var expectedDays = 60; + + Assert.Single(readersWithMaxDays); + Assert.Equal(expectedDays, maxDays); + Assert.Equal(expectedId, readersWithMaxDays[0]); + } + + /// + /// Топ 5 издательств за последний год по количеству выдач и сравнивает по Id и количествам + /// + [Fact] + public void Top5Publishers_ByIssuesCountLastYear_ReturnsExpectedTop5() + { + var lastYearStart = DataSeeder.SeedNowUtc.AddYears(-1); + var lastYearEnd = DataSeeder.SeedNowUtc; + + var topPublishers = dataSeeder.BookIssues + .Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd) + .Join(dataSeeder.Books, bi => bi.BookId, b => b.Id, (bi, b) => b.PublisherId) + .GroupBy(pid => pid) + .Select(g => new { PublisherId = g.Key, Count = g.Count() }) + .Join(dataSeeder.Publishers, g => g.PublisherId, p => p.Id, (g, p) => new { p.Id, p.Name, g.Count }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Name) + .Take(5) + .ToList(); + + var actualPublisherIds = topPublishers.Select(x => x.Id).ToList(); + var actualCounts = topPublishers.Select(x => x.Count).ToList(); + + var expectedPublisherIds = new List { 6, 4, 5, 1, 2 }; + var expectedCounts = new List { 3, 2, 2, 1, 1 }; + + Assert.Equal(expectedPublisherIds, actualPublisherIds); + Assert.Equal(expectedCounts, actualCounts); + } + + /// + /// Топ 5 наименее популярных книг за последний год сравнение по Id и количествам + /// + [Fact] + public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() + { + var lastYearStart = DataSeeder.SeedNowUtc.AddYears(-1); + var lastYearEnd = DataSeeder.SeedNowUtc; + + var bookCounts = dataSeeder.Books + .GroupJoin( + dataSeeder.BookIssues.Where(bi => bi.IssueDate >= lastYearStart && bi.IssueDate <= lastYearEnd), + b => b.Id, + bi => bi.BookId, + (b, issues) => new { Book = b, Count = issues.Count() } + ) + .OrderBy(x => x.Count) + .ThenBy(x => x.Book.Title) + .Take(5) + .ToList(); + + var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); + var actualCounts = bookCounts.Select(x => x.Count).ToList(); + + var expectedBookIds = new List { 10, 5, 6, 3, 8 }; + var expectedCounts = new List { 1, 1, 1, 1, 1 }; + + Assert.Equal(expectedBookIds, actualBookIds); + Assert.Equal(expectedCounts, actualCounts); + } +} \ No newline at end of file diff --git a/Library/Library.sln b/Library/Library.sln new file mode 100644 index 000000000..2721414a1 --- /dev/null +++ b/Library/Library.sln @@ -0,0 +1,67 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Domain", "Library.Domain\Library.Domain.csproj", "{179BC2B9-1601-4FD6-9331-690CA8B4BAF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Tests", "Library.Tests\Library.Tests.csproj", "{10C8F874-23B9-4540-84B7-55A0DA278010}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Infrastructure.EfCore", "Library.Infrastructure.EfCore\Library.Infrastructure.EfCore.csproj", "{DFC5F374-EB46-4CBE-BB4F-8106071FCA7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Application.Contracts", "Library.Application.Contracts\Library.Application.Contracts.csproj", "{A16E9D8D-0BD5-4F7C-8D49-CBDFD6D6B8C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Application", "Library.Application\Library.Application.csproj", "{BCF6CEF9-330E-4B85-8CD4-E732BAC016F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.Api.Host", "Library.Api.Host\Library.Api.Host.csproj", "{F4273AF0-9615-2612-F2B3-73A67646A811}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.AppHost", "Library.AppHost\Library.AppHost.csproj", "{E85C0045-42C2-4F49-8EE8-23C974AFCC73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.ServiceDefaults", "Library.ServiceDefaults\Library.ServiceDefaults.csproj", "{9D0FB5CC-97E4-B8BE-2B2C-2062B059C91A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {179BC2B9-1601-4FD6-9331-690CA8B4BAF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {179BC2B9-1601-4FD6-9331-690CA8B4BAF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {179BC2B9-1601-4FD6-9331-690CA8B4BAF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {179BC2B9-1601-4FD6-9331-690CA8B4BAF1}.Release|Any CPU.Build.0 = Release|Any CPU + {10C8F874-23B9-4540-84B7-55A0DA278010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10C8F874-23B9-4540-84B7-55A0DA278010}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10C8F874-23B9-4540-84B7-55A0DA278010}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10C8F874-23B9-4540-84B7-55A0DA278010}.Release|Any CPU.Build.0 = Release|Any CPU + {DFC5F374-EB46-4CBE-BB4F-8106071FCA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFC5F374-EB46-4CBE-BB4F-8106071FCA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFC5F374-EB46-4CBE-BB4F-8106071FCA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFC5F374-EB46-4CBE-BB4F-8106071FCA7A}.Release|Any CPU.Build.0 = Release|Any CPU + {A16E9D8D-0BD5-4F7C-8D49-CBDFD6D6B8C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A16E9D8D-0BD5-4F7C-8D49-CBDFD6D6B8C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A16E9D8D-0BD5-4F7C-8D49-CBDFD6D6B8C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A16E9D8D-0BD5-4F7C-8D49-CBDFD6D6B8C9}.Release|Any CPU.Build.0 = Release|Any CPU + {BCF6CEF9-330E-4B85-8CD4-E732BAC016F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCF6CEF9-330E-4B85-8CD4-E732BAC016F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCF6CEF9-330E-4B85-8CD4-E732BAC016F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCF6CEF9-330E-4B85-8CD4-E732BAC016F5}.Release|Any CPU.Build.0 = Release|Any CPU + {F4273AF0-9615-2612-F2B3-73A67646A811}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4273AF0-9615-2612-F2B3-73A67646A811}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4273AF0-9615-2612-F2B3-73A67646A811}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4273AF0-9615-2612-F2B3-73A67646A811}.Release|Any CPU.Build.0 = Release|Any CPU + {E85C0045-42C2-4F49-8EE8-23C974AFCC73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E85C0045-42C2-4F49-8EE8-23C974AFCC73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E85C0045-42C2-4F49-8EE8-23C974AFCC73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E85C0045-42C2-4F49-8EE8-23C974AFCC73}.Release|Any CPU.Build.0 = Release|Any CPU + {9D0FB5CC-97E4-B8BE-2B2C-2062B059C91A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D0FB5CC-97E4-B8BE-2B2C-2062B059C91A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D0FB5CC-97E4-B8BE-2B2C-2062B059C91A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D0FB5CC-97E4-B8BE-2B2C-2062B059C91A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {15B4E4AC-EC66-42A5-AAC4-9C56CAA5C661} + EndGlobalSection +EndGlobal From 0b6b1098bc4f1e63522971107de308e712c6eb02 Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:44:05 +0400 Subject: [PATCH 09/10] attempt fix sorting in tests --- Library/Library.Tests/LibraryTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 67908a481..9696060d5 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -19,7 +19,7 @@ public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() bi => bi.BookId, b => b.Id, (bi, b) => new { bi, b }) - .OrderBy(x => x.b.Title) + .OrderBy(x => x.b.Title, StringComparer.InvariantCulture) .Select(x => x.b.Id) .ToList(); @@ -129,7 +129,7 @@ public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() (b, issues) => new { Book = b, Count = issues.Count() } ) .OrderBy(x => x.Count) - .ThenBy(x => x.Book.Title) + .ThenBy(x => x.Book.Title, StringComparer.InvariantCulture) .Take(5) .ToList(); From fe2b488fde56e5fdba079a7dcfc05303acc49447 Mon Sep 17 00:00:00 2001 From: Ryedis <134209766+Ryedis@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:57:44 +0400 Subject: [PATCH 10/10] fixed Expected values in tests --- Library/Library.Tests/LibraryTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Library.Tests/LibraryTests.cs b/Library/Library.Tests/LibraryTests.cs index 9696060d5..0a49c0d01 100644 --- a/Library/Library.Tests/LibraryTests.cs +++ b/Library/Library.Tests/LibraryTests.cs @@ -23,7 +23,7 @@ public void IssuedBooks_OrderByBookTitle_ReturnsActiveIssuesOrderedByTitle() .Select(x => x.b.Id) .ToList(); - var expectedBookIds = new List { 10, 2, 2, 5, 6, 3, 1, 1, 8, 4, 7, 9}; + var expectedBookIds = new List { 9, 10, 2, 2, 5, 6, 3, 1, 1, 8, 4, 7}; Assert.Equal(expectedBookIds, actualBookIds); } @@ -136,7 +136,7 @@ public void Bottom5Books_ByIssuesCountLastYear_ReturnsExpectedBottom5() var actualBookIds = bookCounts.Select(x => x.Book.Id).ToList(); var actualCounts = bookCounts.Select(x => x.Count).ToList(); - var expectedBookIds = new List { 10, 5, 6, 3, 8 }; + var expectedBookIds = new List { 9, 10, 5, 6, 3 }; var expectedCounts = new List { 1, 1, 1, 1, 1 }; Assert.Equal(expectedBookIds, actualBookIds);