diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 000000000..6f2699892
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +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
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.sln b/Library.sln
new file mode 100644
index 000000000..6550f8d0c
--- /dev/null
+++ b/Library.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
diff --git a/Library/Library.Domain/Data/DataSeeder.cs b/Library/Library.Domain/Data/DataSeeder.cs
new file mode 100644
index 000000000..226a31ba2
--- /dev/null
+++ b/Library/Library.Domain/Data/DataSeeder.cs
@@ -0,0 +1,84 @@
+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 }
+ };
+}
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/Library.Domain/Models/Book.cs b/Library/Library.Domain/Models/Book.cs
new file mode 100644
index 000000000..b599cdfaa
--- /dev/null
+++ b/Library/Library.Domain/Models/Book.cs
@@ -0,0 +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; } = [];
+}
\ No newline at end of file
diff --git a/Library/Library.Domain/Models/BookIssue.cs b/Library/Library.Domain/Models/BookIssue.cs
new file mode 100644
index 000000000..81b8b8104
--- /dev/null
+++ b/Library/Library.Domain/Models/BookIssue.cs
@@ -0,0 +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; }
+
+ ///
+ /// Дата возврата книги (IssueDate + Days)
+ ///
+ public DateTime? ReturnDate => IssueDate.AddDays(Days);
+
+ ///
+ /// Флаг просрочки срока возврата книги
+ ///
+ public bool IsOverdue(DateTime currentDate) =>
+ ReturnDate == null && currentDate.Date > IssueDate.AddDays(Days).Date;
+}
\ No newline at end of file
diff --git a/Library/Library.Domain/Models/EditionType.cs b/Library/Library.Domain/Models/EditionType.cs
new file mode 100644
index 000000000..4eba925ba
--- /dev/null
+++ b/Library/Library.Domain/Models/EditionType.cs
@@ -0,0 +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/Library.Domain/Models/Publisher.cs b/Library/Library.Domain/Models/Publisher.cs
new file mode 100644
index 000000000..5c1fedbac
--- /dev/null
+++ b/Library/Library.Domain/Models/Publisher.cs
@@ -0,0 +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/Library.Domain/Models/Reader.cs b/Library/Library.Domain/Models/Reader.cs
new file mode 100644
index 000000000..3e4c87e4d
--- /dev/null
+++ b/Library/Library.Domain/Models/Reader.cs
@@ -0,0 +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; } = [];
+}
\ 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..c6384e67e
--- /dev/null
+++ b/Library/Library.Tests/LibraryTests.cs
@@ -0,0 +1,151 @@
+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);
+ }
+}