Система управления библиотекой книг на C#
Система управления библиотекой книг на 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
public class Book
{
public int Id { get; set; }
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 Author Author { get; set; } = null!;
public int GenreId { get; set; }
public Genre Genre { get; set; } = null!;
public ICollection<Loan> Loans { get; set; } = new List<Loan>();
}
Класс 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
public class Loan
{
public int Id { get; set; }
public DateTime IssueDate { get; set; }
public DateTime? ReturnDate { get; set; }
public bool IsReturned => ReturnDate.HasValue;
// Внешние ключи
public int BookId { get; set; }
public Book Book { get; set; } = null!;
public int ReaderId { get; set; }
public Reader Reader { get; set; } = null!;
}
Эти классы образуют граф объектов, где связи между сущностями выражены через навигационные свойства (Author, Genre, Loans и т.д.). Такая структура позволяет легко переходить от одной сущности к связанной без написания дополнительных SQL-запросов.
Настройка Entity Framework Core
Entity Framework Core (EF Core) — это объектно-реляционный маппер (ORM), входящий в экосистему .NET. Он позволяет работать с базой данных через объекты C#, автоматически генерируя SQL-запросы.
Контекст базы данных
Контекст — это основной класс EF Core, представляющий сессию работы с базой данных. Он наследуется от DbContext и содержит DbSet<T> для каждой сущности.
public class LibraryContext : DbContext
{
public DbSet<Book> Books { get; set; } = null!;
public DbSet<Author> Authors { get; set; } = null!;
public DbSet<Genre> Genres { get; set; } = null!;
public DbSet<Reader> Readers { get; set; } = null!;
public DbSet<Loan> Loans { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=LibraryDb;Trusted_Connection=true;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Настройка отношений
modelBuilder.Entity<Book>()
.HasOne(b => b.Author)
.WithMany(a => a.Books)
.HasForeignKey(b => b.AuthorId);
modelBuilder.Entity<Book>()
.HasOne(b => b.Genre)
.WithMany(g => g.Books)
.HasForeignKey(b => b.GenreId);
modelBuilder.Entity<Loan>()
.HasOne(l => l.Book)
.WithMany(b => b.Loans)
.HasForeignKey(l => l.BookId);
modelBuilder.Entity<Loan>()
.HasOne(l => l.Reader)
.WithMany(r => r.Loans)
.HasForeignKey(l => l.ReaderId);
}
}
Метод OnModelCreating используется для явного указания связей между таблицами. Хотя EF Core может вывести многие связи автоматически, явная конфигурация повышает надёжность и читаемость кода.
Реализация CRUD-операций
CRUD-операции — это базовые действия: создание, чтение, обновление и удаление записей. В ASP.NET Core они обычно реализуются в контроллерах.
Пример контроллера книг
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly LibraryContext _context;
public BooksController(LibraryContext context)
{
_context = context;
}
// GET: api/books
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books
.Include(b => b.Author)
.Include(b => b.Genre)
.ToListAsync();
}
// GET: api/books/5
[HttpGet("{id}")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books
.Include(b => b.Author)
.Include(b => b.Genre)
.FirstOrDefaultAsync(b => b.Id == id);
if (book == null)
return NotFound();
return book;
}
// POST: api/books
[HttpPost]
public async Task<ActionResult<Book>> CreateBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}
// PUT: api/books/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdateBook(int id, Book book)
{
if (id != book.Id)
return BadRequest();
_context.Entry(book).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(id))
return NotFound();
else
throw;
}
return NoContent();
}
// DELETE: api/books/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
return NotFound();
_context.Books.Remove(book);
await _context.SaveChangesAsync();
return NoContent();
}
private bool BookExists(int id) => _context.Books.Any(e => e.Id == id);
}
Ключевые моменты:
- Использование
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 книг;
- при возврате книги обновляется статус выдачи.
Эти правила лучше выносить в отдельные сервисы.
Пример сервиса выдачи
public class LoanService
{
private readonly LibraryContext _context;
public LoanService(LibraryContext context)
{
_context = context;
}
public async Task<Result> IssueBookAsync(int bookId, int readerId)
{
var activeLoan = await _context.Loans
.FirstOrDefaultAsync(l => l.BookId == bookId && !l.IsReturned);
if (activeLoan != null)
return Result.Failure("Книга уже выдана");
var loan = new Loan
{
BookId = bookId,
ReaderId = readerId,
IssueDate = DateTime.UtcNow
};
_context.Loans.Add(loan);
await _context.SaveChangesAsync();
return Result.Success();
}
public async Task<Result> ReturnBookAsync(int loanId)
{
var loan = await _context.Loans.FindAsync(loanId);
if (loan == null || loan.IsReturned)
return Result.Failure("Неверный ID выдачи или книга уже возвращена");
loan.ReturnDate = DateTime.UtcNow;
await _context.SaveChangesAsync();
return Result.Success();
}
}
Здесь 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-тестами. Например, тест выдачи книги:
[Fact]
public async Task IssueBook_WhenBookIsAvailable_ShouldCreateLoan()
{
// Arrange
var context = CreateInMemoryDbContext();
var service = new LoanService(context);
// Act
var result = await service.IssueBookAsync(bookId: 1, readerId: 1);
// Assert
Assert.True(result.IsSuccess);
Assert.Single(context.Loans);
}
Использование 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.NewGuid().ToString();
public bool IsAvailable { get; set; } = true;
public ICollection<Loan> Loans { get; set; } = new List<Loan>();
}
Теперь выдача книги происходит не по BookId, а по BookCopyId. Это позволяет точно отслеживать физическое состояние каждого экземпляра (например, повреждён ли он), а также управлять доступностью отдельных копий.
Уведомления о просроченных книгах
Библиотека может автоматически отправлять напоминания читателям, не вернувшим книги в срок. Реализация требует:
- фоновой службы (hosted service);
- интерфейса уведомлений (
INotificationService); - логики проверки просрочки.
public class OverdueNotificationService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OverdueNotificationService> _logger;
public OverdueNotificationService(IServiceProvider serviceProvider, ILogger<OverdueNotificationService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
var overdueLoans = await context.Loans
.Where(l => !l.IsReturned && l.IssueDate.AddDays(14) < DateTime.UtcNow)
.Include(l => l.Reader)
.ToListAsync(stoppingToken);
foreach (var loan in overdueLoans)
{
await notificationService.SendEmailAsync(
loan.Reader.Email,
"Напоминание о возврате книги",
$"Уважаемый {loan.Reader.FullName}, пожалуйста, верните книгу «{loan.Book.Title}»."
);
}
await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
}
}
}
Регистрация фоновой службы в Program.cs:
builder.Services.AddHostedService<OverdueNotificationService>();
Такой подход демонстрирует применение шаблона Background Service, который является стандартным способом выполнения периодических задач в ASP.NET Core.
Поиск по ISBN и автору через API
Для удобства пользователей добавляются специализированные методы поиска:
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<Book>>> SearchBooks(
[FromQuery] string? title = null,
[FromQuery] string? author = null,
[FromQuery] string? isbn = null)
{
var query = _context.Books.AsQueryable();
if (!string.IsNullOrEmpty(title))
query = query.Where(b => b.Title.Contains(title));
if (!string.IsNullOrEmpty(author))
query = query.Where(b => b.Author.FirstName.Contains(author) || b.Author.LastName.Contains(author));
if (!string.IsNullOrEmpty(isbn))
query = query.Where(b => b.ISBN == isbn);
return await query
.Include(b => b.Author)
.Include(b => b.Genre)
.ToListAsync();
}
Этот эндпоинт позволяет гибко комбинировать параметры поиска и возвращает только релевантные результаты.
Локализация и интернационализация
Если система предназначена для международного использования, следует реализовать поддержку нескольких языков. ASP.NET Core предоставляет встроенные механизмы локализации через:
- файлы ресурсов (
.resx); - middleware
RequestLocalization; - атрибуты
[Display(Name = "...")]с ключами.
Пример настройки:
// Program.cs
var supportedCultures = new[] { "ru", "en" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
После этого все строки интерфейса (например, названия полей в формах) могут загружаться из соответствующих .resx-файлов.
Безопасность и производительность
Защита от overposting
При использовании автоматической привязки модели ([FromBody] Book book) существует риск overposting — когда клиент отправляет поля, которые не должны обновляться (например, Id или AuthorId). Решение — использовать DTO (Данные 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 кэша:
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
var cacheKey = "all-books";
if (_cache.TryGetValue(cacheKey, out List<Book>? cachedBooks))
{
return cachedBooks;
}
var books = await _context.Books
.Include(b => b.Author)
.Include(b => b.Genre)
.ToListAsync();
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10));
_cache.Set(cacheKey, books, cacheEntryOptions);
return books;
}
Регистрация кэша:
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.AddHealthChecks()
.AddDbContextCheck<LibraryContext>();
app.MapHealthChecks("/health");
Запрос к /health вернёт Healthy, если база данных доступна, и Unhealthy в противном случае. Это особенно полезно при использовании оркестраторов вроде Kubernetes.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Проблема — Пользователи должны иметь возможность регистрироваться, входить в систему и получать доступ к персонализированному контенту или функционалу. Простой консольный чат на C — это учебное приложение, демонстрирующее базовые принципы сетевого взаимодействия между клиентом и сервером с использованием сокетов. Перед началом работы обязательно изучите главу Turtle . Scratch — визуальная образовательная среда программирования, разработанная MIT Media Lab. Особенности реализации — set -euo pipefail — обязательная практика для production-скриптов; - shift $((OPTIND - 1)) корректно обрабатывает как script.sh -c ., так и script.sh . -c; - -C и… echo off rem — Отключает вывод каждой команды (как set -x в bash) rem — в начале первой строки подавляет вывод её самой Примечание — использует XML-документацию, встроенную в модули. В PowerShell 7+ справка по умолчанию загружается из интернета, если локальные файлы отсутствуют. Примечание — для большинства случаев достаточно , но оно не поддерживает функции и некоторые нестандартные объекты (например, до ES2024 — поддержка есть, но не во всех средах выполнения, например,… ✅ Такой подход даёт полную типобезопасность без и без / . Подходит для лёгких сценариев или когда внешние зависимости нежелательны. ✅ Работает, если связь или гарантируется единственность. ⚠️ Для продакшена рекомендуются Jackson ( ) или Gson (более производительные и типобезопасные). удобен для прототипирования. Генератор случайных паролей — это утилита, создающая строки с заданными криптографическими свойствами — длина, наличие заглавных и строчных букв, цифр, специальных символов.Готовые решения
Простой консольный чат на CSharp
Примеры фигур Turtle на Python
Примеры скриптов Scratch
Примеры скриптов в Linux
Примеры команд в cmd
Примеры команд в PowerShell
Примеры решений в JavaScript
Примеры решений в TypeScript
Примеры запросов в SQL
Примеры решений в Java
Генератор случайных паролей на CSharp