Система управления библиотекой книг на C#

Приветствую! Здесь вы наверняка найдете, что ищете. Примеры в лаборатории рассчитаны на то, что мы разбираем что-то конкретное.

Текущая статья посвящена учебной системе управления библиотекой на C# — сущности, CRUD и слои приложения.

Поэтому за теорией по текущей теме вам — в энциклопедию. Если ещё не погружались, то маршрут прост:

  1. Основы
  2. Система и сеть
  3. Данные и разметка
  4. Код и разработка
  5. Языки
  6. Искусственный интеллект
  7. Проект
  8. Инфраструктура и безопасность
  9. Спин-офф

Обязательно пройдитесь.

А теперь приступим к нашему предмету.

Теория и соседние материалы

Система управления библиотекой книг на C#

Система управления библиотекой книг — это классический учебный проект, который демонстрирует применение ключевых концепций современной разработки на платформе .NET. В данном примере реализуется полноценное веб-приложение с использованием ASP.NET Core, Entity Framework Core и LINQ. Такая система позволяет управлять каталогом книг, отслеживать выдачу экземпляров читателям, контролировать сроки возврата и формировать отчёты.

Проект охватывает такие аспекты, как:

  • проектирование доменной модели;
  • работа с реляционной базой данных через ORM;
  • реализация CRUD-операций (Create, Read, Update, Delete);
  • применение шаблонов MVC и Repository;
  • использование LINQ для фильтрации, сортировки и агрегации данных;
  • построение RESTful API или серверного рендеринга представлений;
  • обеспечение согласованности данных и обработка ошибок.

Доменная модель

Доменная модель — это набор классов, отражающих сущности предметной области и их взаимосвязи. В системе управления библиотекой можно выделить следующие основные сущности:

  • Book — книга в каталоге;
  • Author — автор книги;
  • Genre — жанр книги;
  • Reader — читатель (пользователь библиотеки);
  • Loan — запись о выдаче книги читателю.

Каждая сущность представляет собой отдельный класс с набором свойств и связей.


Класс Book


Класс Author

public class Author
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    
    public ICollection<Book> Books { get; set; } = new List<Book>();
}

Класс Genre

public class Genre
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    
    public ICollection<Book> Books { get; set; } = new List<Book>();
}

Класс Reader

public class Reader
{
    public int Id { get; set; }
    public string FullName { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime RegistrationDate { get; set; }
    
    public ICollection<Loan> Loans { get; set; } = new List<Loan>();
}

Класс Loan

Эти классы образуют граф объектов, где связи между сущностями выражены через навигационные свойства (Author, Genre, Loans и т.д.). Такая структура позволяет легко переходить от одной сущности к связанной без написания дополнительных SQL-запросов.


Настройка Entity Framework Core

Entity Framework Core (EF Core) — это объектно-реляционный маппер (ORM), входящий в экосистему .NET. Он позволяет работать с базой данных через объекты C#, автоматически генерируя SQL-запросы.


Контекст базы данных

Контекст — это основной класс EF Core, представляющий сессию работы с базой данных. Он наследуется от DbContext и содержит DbSet<T> для каждой сущности.

Метод OnModelCreating используется для явного указания связей между таблицами. Хотя EF Core может вывести многие связи автоматически, явная конфигурация повышает надёжность и читаемость кода.


Реализация CRUD-операций

CRUD-операции — это базовые действия: создание, чтение, обновление и удаление записей. В ASP.NET Core они обычно реализуются в контроллерах.


Пример контроллера книг

Ключевые моменты:

  • Использование Include() для загрузки связанных данных (автора и жанра);
  • Асинхронные методы (async/await) для эффективной работы с I/O;
  • Обработка ошибок: NotFound(), BadRequest(), исключения параллелизма.

Работа с данными через LINQ

LINQ (Language Integrated Query) — это мощный механизм языка C# для запросов к коллекциям и базам данных. В контексте EF Core LINQ-выражения транслируются в SQL.


Примеры практических запросов

Поиск всех невозвращённых книг

var overdueLoans = await _context.Loans
    .Where(l => !l.IsReturned)
    .Include(l => l.Book)
    .Include(l => l.Reader)
    .ToListAsync();

Книги, выданные конкретному читателю

var loansByReader = await _context.Loans
    .Where(l => l.ReaderId == readerId && !l.IsReturned)
    .Select(l => l.Book)
    .ToListAsync();

Подсчёт количества книг по жанрам

var genreCounts = await _context.Books
    .GroupBy(b => b.Genre.Name)
    .Select(g => new { Genre = g.Key, Count = g.Count() })
    .ToListAsync();

Поиск книг по ключевому слову в названии

var books = await _context.Books
    .Where(b => b.Title.Contains(searchTerm))
    .Include(b => b.Author)
    .ToListAsync();

Самые популярные книги (по количеству выдач)

var popularBooks = await _context.Books
    .Select(b => new
    {
        Book = b,
        LoanCount = b.Loans.Count
    })
    .OrderByDescending(x => x.LoanCount)
    .Take(10)
    .ToListAsync();

LINQ делает код декларативным: вместо описания как получить данные, разработчик описывает что нужно получить. Это повышает читаемость и снижает вероятность ошибок.


Архитектурные соображения

Хотя прямое использование DbContext в контроллере допустимо для простых приложений, в реальных проектах рекомендуется применять паттерны:


Паттерн Repository

Создаётся интерфейс IBookRepository и его реализация BookRepository, инкапсулирующая все операции с книгами. Это упрощает тестирование и замену реализации.

public interface IBookRepository
{
    Task<IEnumerable<Book>> GetAllAsync();
    Task<Book?> GetByIdAsync(int id);
    Task AddAsync(Book book);
    Task UpdateAsync(Book book);
    Task DeleteAsync(int id);
}

Dependency Injection

ASP.NET Core поддерживает внедрение зависимостей "из коробки". Репозитории регистрируются в контейнере:

// Program.cs
builder.Services.AddDbContext<LibraryContext>(/* .. */);
builder.Services.AddScoped<IBookRepository, BookRepository>();

Контроллер получает репозиторий через конструктор:

public BooksController(IBookRepository repository)
{
    _repository = repository;
}

Такой подход делает код более модульным и тестируемым.


Обработка бизнес-логики

Простое хранение данных — лишь часть системы. Библиотека требует реализации бизнес-правил:

  • одна книга не может быть выдана двум читателям одновременно;
  • читатель не может взять больше N книг;
  • при возврате книги обновляется статус выдачи.

Эти правила лучше выносить в отдельные сервисы.


Пример сервиса выдачи

Здесь Result — пользовательский тип, инкапсулирующий успешный результат или ошибку (паттерн "Railway Oriented Programming").


Валидация и безопасность

Важно проверять входные данные:

  • ISBN должен соответствовать формату;
  • год издания не может быть в будущем;
  • email читателя должен быть валидным.

Для этого используются атрибуты валидации:

public class Book
{
    [Required, MaxLength(200)]
    public string Title { get; set; } = string.Empty;

    [Range(1000, 3000)]
    public int Year { get; set; }

    [RegularExpression(@"^\d{10}(\d{3})?$")]
    public string ISBN { get; set; } = string.Empty;
}

В контроллере проверяется состояние модели:

if (!ModelState.IsValid)
    return BadRequest(ModelState);

Также необходимо защищать API от SQL-инъекций, но EF Core автоматически параметризует все запросы, поэтому риск минимален при использовании LINQ.


Тестирование

Проект следует покрывать unit- и integration-тестами. Например, тест выдачи книги:

Использование in-memory базы данных (UseInMemoryDatabase) ускоряет выполнение тестов.


Расширение функциональности

Систему можно развивать в разных направлениях:

  • добавление аутентификации и ролей (библиотекарь, читатель);
  • экспорт отчётов в PDF или Excel;
  • интеграция с внешними API (например, OpenLibrary для автозаполнения данных по ISBN);
  • фоновые задачи для уведомления о просроченных книгах;
  • кэширование часто запрашиваемых данных;
  • локализация интерфейса.

Дополнительные возможности и расширения

Система управления библиотекой книг предоставляет широкие возможности для расширения функциональности. Ниже приведены направления, в которых можно развивать проект, сохраняя его архитектурную целостность.


Поддержка нескольких экземпляров одной книги

В текущей модели каждая запись Book представляет один экземпляр. Однако в реальной библиотеке может быть несколько копий одного издания. Для поддержки этого сценария вводится новая сущность — BookCopy.

public class BookCopy
{
    public int Id { get; set; }
    public int BookId { get; set; }
    public Book Book { get; set; } = null!;
    public string InventoryNumber { get; set; } = Guid.NewGuidToString();
    public bool IsAvailable { get; set; } = true;
    
    public ICollection<Loan> Loans { get; set; } = new List<Loan>();
}

Теперь выдача книги происходит не по BookId, а по BookCopyId. Это позволяет точно отслеживать физическое состояние каждого экземпляра (например, повреждён ли он), а также управлять доступностью отдельных копий.


Уведомления о просроченных книгах

Библиотека может автоматически отправлять напоминания читателям, не вернувшим книги в срок. Реализация требует:

  • фоновой службы (hosted service);
  • интерфейса уведомлений (INotificationService);
  • логики проверки просрочки.

Регистрация фоновой службы в Program.cs:

builder.Services.AddHostedService<OverdueNotificationService>();

Такой подход демонстрирует применение шаблона Background Service, который является стандартным способом выполнения периодических задач в ASP.NET Core.


Поиск по ISBN и автору через API

Для удобства пользователей добавляются специализированные методы поиска:

Этот эндпоинт позволяет гибко комбинировать параметры поиска и возвращает только релевантные результаты.


Локализация и интернационализация

Если система предназначена для международного использования, следует реализовать поддержку нескольких языков. ASP.NET Core предоставляет встроенные механизмы локализации через:

  • файлы ресурсов (.resx);
  • middleware RequestLocalization;
  • атрибуты [Display(Name = "..")] с ключами.

Пример настройки:

// Program.cs
var supportedCultures = new[] { "ru", "en" };
var localizationOptions = new RequestLocalizationOptionsSetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

После этого все строки интерфейса (например, названия полей в формах) могут загружаться из соответствующих .resx-файлов.


Безопасность и производительность

Защита от overposting

При использовании автоматической привязки модели ([FromBody] Book book) существует риск overposting — когда клиент отправляет поля, которые не должны обновляться (например, Id или AuthorId). Решение — использовать DTO (Data Transfer Object).

public class CreateBookDto
{
    public string Title { get; set; } = string.Empty;
    public int Year { get; set; }
    public string ISBN { get; set; } = string.Empty;
    public int AuthorId { get; set; }
    public int GenreId { get; set; }
}

Контроллер работает только с этим DTO, а маппинг в сущность Book выполняется явно:

var book = new Book
{
    Title = dto.Title,
    Year = dto.Year,
    ISBN = dto.ISBN,
    AuthorId = dto.AuthorId,
    GenreId = dto.GenreId
};

Это исключает возможность несанкционированного изменения внутренних полей.


Кэширование часто запрашиваемых данных

Если каталог книг редко изменяется, но часто читается, имеет смысл кэшировать результаты. ASP.NET Core предлагает несколько уровней кэширования:

  • In-memory cache — для одного сервера;
  • Distributed cache (Redis, SQL Server) — для масштабируемых систем.

Пример использования in-memory кэша:

Регистрация кэша:

builder.Services.AddMemoryCache();

Кэширование значительно снижает нагрузку на базу данных при высокой частоте запросов.


Развертывание и мониторинг

Конфигурация через appsettings.json

Параметры подключения к базе данных, сроки выдачи книг, email-сервера — всё это должно храниться в конфигурационных файлах, а не быть зашито в код.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=LibraryDb;Trusted_Connection=true;"
  },
  "LibrarySettings": {
    "LoanPeriodDays": 14,
    "MaxBooksPerReader": 5
  }
}

Использование в коде:

var loanPeriod = configuration.GetValue<int>("LibrarySettings:LoanPeriodDays");

Это позволяет легко адаптировать приложение под разные окружения (разработка, тестирование, продакшн).


Логирование

ASP.NET Core интегрирован с системой логирования. В контроллерах можно внедрять ILogger<T>:

public BooksController(LibraryContext context, ILogger<BooksController> logger)
{
    _context = context;
    _logger = logger;
}

[HttpPost]
public async Task<ActionResult<Book>> CreateBook(Book book)
{
    _logger.LogInformation("Creating a new book: {Title}", book.Title);
    // ..
}

Логи могут отправляться в файлы, консоль, или внешние системы (например, Elasticsearch + Kibana).


Health Checks

Для мониторинга работоспособности приложения ASP.NET Core предоставляет механизм health checks:

// Program.cs
builder.Services.AddHealthChecksAddDbContextCheck<LibraryContext>();

app.MapHealthChecks("/health");

Запрос к /health вернёт Healthy, если база данных доступна, и Unhealthy в противном случае. Это особенно полезно при использовании оркестраторов вроде Kubernetes.