diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 000000000..f08487dd1
--- /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/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/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
new file mode 100644
index 000000000..0f3589f95
--- /dev/null
+++ b/Library/Library.Domain/Data/DataSeeder.cs
@@ -0,0 +1,103 @@
+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
new file mode 100644
index 000000000..fa71b7ae6
--- /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..aeeeaebc0
--- /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 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..c9db32e8b
--- /dev/null
+++ b/Library/Library.Domain/Models/BookIssue.cs
@@ -0,0 +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; }
+
+ ///
+ /// Дата возврата книги, если 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
new file mode 100644
index 000000000..204f9b6ce
--- /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..1d5be54c6
--- /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..5d0f7d36d
--- /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 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