Back to: ASP.NET Core Tutorials For Beginners and Professionals
Library Management System using ASP.NET Core MVC and EF Core
Let’s build a Real-time Library Management System Application using Entity Framework (EF) Core and ASP.NET Core MVC. In this article, I will discuss building a robust Library Management System using ASP.NET Core MVC, Entity Framework (EF) Core, and Data Annotations. We will create a Library Management System that allows administrators to manage books. Each book has properties like BookId, Title, Author, ISBN, PublishedDate, and IsAvailable.
- Security Concern: Properties like BookId and IsAvailable should not be modifiable via user input to prevent unauthorized changes.
- Validation Requirement: When adding or editing a book, essential fields like Title, Author, and ISBN must be provided.
Pages to be Developed for the Library Management System:
First, let us understand the different pages we will develop as part of this Library Management System Application.
Home or Index Page or Listing Page:
This page is displayed when the admin logs in. It serves as the main landing page where administrators can access various functionalities of the library management system, such as viewing a list of available books, adding new books, or managing borrowing/returning actions.
Book Details Page:
This page provides detailed information about a specific book, including its properties such as BookId, Title, Author, ISBN, PublishedDate, and its availability status. Administrators can view all relevant details but not directly modify specific properties like BookId or IsAvailable.
Create Book Page:
This page allows the administrator to add a new book to the library’s catalog. The administrator must provide the required information, such as Title, Author, and ISBN. Validation ensures that all necessary fields are filled in before submitting the form.
Edit Book Page:
This page allows the administrator to update the details of an existing book. Similar to the create page, important properties like Title, Author, and ISBN are editable, while fields such as BookId and IsAvailable are protected from modification.
Delete Book Page:
This page allows the administrator to remove a book from the library’s catalog. To prevent accidental deletions, it usually displays a confirmation prompt before permanently deleting the book.
Not Found Page:
This page is shown when the system cannot find a specific book or borrow record the admin is trying to access. For example, if a BookId is invalid or missing, the system will redirect to this page with a suitable error message.
Borrow Book Page:
This page allows the admin to process a borrowing request for a specific book. It displays the book’s details and requires information about the borrower (such as their name and contact information). If the book is available, it can be marked as borrowed, and a borrow record will be created.
Return Book Page:
This page facilitates the return process for a borrowed book. It displays details of the borrow record, such as the borrower’s name and the borrow date, and allows the administrator to mark the book as returned.
Book Not Available for Borrow Page:
This page is displayed when an attempt is made to borrow a book that is already borrowed by someone else. It informs the admin that the book is currently unavailable and cannot be borrowed until it is returned.
Book Already Returned Page:
This page is shown when an admin attempts to process a return for a book already returned. It notifies the admin that the return action is unnecessary as the borrowing record indicates that the book has already been returned.
Each of these pages is integral to managing book borrowing, returning, and tracking within the library system. It provides essential functionalities while ensuring proper validation and security.
Implementing the Library Management System Project:
Now, let us proceed and implement the above Library Management System Project using ASP.NET Core MVC and Entity Framework Core step by step by following the Industry Coding standard and proper exception handling and Validations for Data Integrity and Security. We will use Entity Framework Core Code First Approach, and the database will be SQL Server.
Setting Up the ASP.NET Core MVC Project
Create a new ASP.NET Core MVC project named LibraryManagement. Once you create the Project, please add the Microsoft EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools Packages either using the Package Manager solution or by executing the following commands in the Package Manager Console.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Creating Book Model
Create a new class file named Book.cs inside the Models folder and add the following code. This file defines the Book model representing books in the library. It includes properties like BookId, Title, Author, ISBN, PublishedDate, and IsAvailable. Attributes like [BindNever] and [Required] control model binding behavior for security and validation purposes.
using Microsoft.AspNetCore.Mvc.ModelBinding; using System.ComponentModel.DataAnnotations; namespace LibraryManagement.Models { public class Book { [BindNever] public int BookId { get; set; } // Primary key, not bound from user input [Required(ErrorMessage = "The Title field is required.")] [StringLength(100, ErrorMessage = "Title cannot exceed 100 characters.")] public string Title { get; set; } [Required(ErrorMessage = "The Author field is required.")] [StringLength(100, ErrorMessage = "Author name cannot exceed 100 characters.")] public string Author { get; set; } [Required(ErrorMessage = "The ISBN field is required.")] [RegularExpression(@"^\d{3}-\d{10}$", ErrorMessage = "ISBN must be in the format XXX-XXXXXXXXXX.")] public string ISBN { get; set; } [Required(ErrorMessage = "The Published Date field is required.")] [DataType(DataType.Date)] [Display(Name = "Published Date")] public DateTime PublishedDate { get; set; } [BindNever] [Display(Name = "Available")] public bool IsAvailable { get; set; } = true; // Default to available // Navigation Property [BindNever] public ICollection<BorrowRecord>? BorrowRecords { get; set; } } }
Code Explanation:
- BookId, IsAvailable, and BorrowRecords are marked with [BindNever] to prevent them from being set via form submissions.
- Title, Author, and ISBN are marked with [Required] to ensure they are present in the incoming request.
- Additional validation attributes like [StringLength] and [RegularExpression] enforce data integrity.
Creating BorrowRecord Model
Create a new class file named BorrowRecord.cs inside the Models folder and add the following code. This class defines the BorrowRecord model for tracking book borrowing and return. It includes BorrowRecordId, BookId, BorrowerName, BorrowerEmail, Phone, BorrowDate, and ReturnDate properties. This model links a book to its borrower and records transaction dates.
using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace LibraryManagement.Models { public class BorrowRecord { [Key] public int BorrowRecordId { get; set; } //PK [Required] public int BookId { get; set; } //FK [Required(ErrorMessage = "Please enter Borrower Name")] public string BorrowerName { get; set; } [Required(ErrorMessage = "Please enter Borrower Email Address")] [EmailAddress(ErrorMessage = "Please enter a Email Address")] public string BorrowerEmail { get; set; } [Required(ErrorMessage = "Please enter Borrower Phone Number")] [Phone(ErrorMessage = "Please enter a Valid Phone Number")] public string Phone { get; set; } [BindNever] [DataType(DataType.DateTime)] public DateTime BorrowDate { get; set; } = DateTime.UtcNow; [DataType(DataType.DateTime)] public DateTime? ReturnDate { get; set; } // Navigation Properties [BindNever] public Book Book { get; set; } } }
Configuring the Database Context
Create a new class file named LibraryContext.cs inside the Models folder, and then copy and paste the following code. It acts as the database context class inheriting from DbContext. It defines DbSet<Book> and DbSet<BorrowRecord> to represent the corresponding tables in the database. Additionally, it seeds the database with initial data for the Books table in the OnModelCreating method.
using Microsoft.EntityFrameworkCore; namespace LibraryManagement.Models { public class LibraryContext : DbContext { public LibraryContext(DbContextOptions<LibraryContext> options) : base(options) { } // Seed initial data protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>().HasData( new Book { BookId = 1, Title = "The Pragmatic Programmer", Author = "Andrew Hunt and David Thomas", ISBN = "978-0201616224", PublishedDate = new DateTime(2021, 10, 30), IsAvailable = true }, new Book { BookId = 2, Title = "Design Pattern using C#", Author = "Robert C. Martin", ISBN = "978-0132350884", PublishedDate = new DateTime(2023, 8, 1), IsAvailable = true }, new Book { BookId = 3, Title = "Mastering ASP.NET Core", Author = "Pranaya Kumar Rout", ISBN = "978-0451616235", PublishedDate = new DateTime(2022, 11, 22), IsAvailable = true }, new Book { BookId = 4, Title = "SQL Server with DBA", Author = "Rakesh Kumat", ISBN = "978-4562350123", PublishedDate = new DateTime(2020, 8, 15), IsAvailable = true } ); } public DbSet<Book> Books { get; set; } public DbSet<BorrowRecord> BorrowRecords { get; set; } } }
Code Explanation:
- LibraryContext inherits from DbContext and defines DbSet<Book> representing the Books table and DbSet<BorrowRecord> representing the BorrowRecords table.
- OnModelCreating seeds the database with initial data.
Adding Connection String in appsettings.json file:
We will store the connection string in the appsettings.json file. So, please modify the appsettings.json file as follows. It contains configuration settings for the application, including the database connection string under the “ConnectionStrings” section, which specifies how to connect to the SQL Server database.
{ "ConnectionStrings": { "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=LibraryManagementDB;Trusted_Connection=True;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
Register the Database Context in Program Class:
Next, we need to configure EF Core to use the SQL Server Database and provide the connection string from the appsettings.json file. So, please modify the Program class as follows. It configures services and middleware for the ASP.NET Core application. It registers the LibraryContext with the dependency injection container and sets up the default controller route to BooksController.
using LibraryManagement.Models; using Microsoft.EntityFrameworkCore; namespace LibraryManagement { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); // Register ApplicationDbContext with SQL Server provider builder.Services.AddDbContext<LibraryContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Books}/{action=Index}/{id?}"); app.Run(); } } }
Generating and Applying Migration:
Open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the LibraryManagementDB database and Books table based on our Book Model and LibraryContext class:
Once you execute the above commands and verify the database, you should see the LibraryManagementDB database with the required tables, as shown in the image below.
Now, if you verify the Books table, then you will see the initial seed data as shown in the below image:
Creating Books Controllers
Create a new Empty MVC Controller named BooksController.cs inside the Controllers folder and add the following code. The Books controller Manages CRUD (Create, Read, Update, Delete) operations for the Book model. It includes actions such as listing all books (Index), displaying details of a specific book (Details), creating a new book (Create), editing an existing book (Edit), and deleting a book (Delete). It ensures that sensitive properties are protected and data validation is enforced.
using LibraryManagement.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace LibraryManagement.Controllers { public class BooksController : Controller { private readonly LibraryContext _context; // Injecting the LibraryContext and Logger to interact with the database and log events. public BooksController(LibraryContext context) { _context = context; } // Retrieves and displays all books. // GET: Books public async Task<IActionResult> Index() { try { var books = await _context.Books .Include(b => b.BorrowRecords) .AsNoTracking() .ToListAsync(); return View(books); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the books."; return View("Error"); } } // GET: Books/Details/5 public async Task<IActionResult> Details(int? id) { if (id == null || id == 0) { TempData["ErrorMessage"] = "Book ID was not provided."; return View("NotFound"); } try { var book = await _context.Books .FirstOrDefaultAsync(m => m.BookId == id); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {id}."; return View("NotFound"); } return View(book); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the book details."; return View("Error"); } } // GET: Books/Create public IActionResult Create() { return View(); } // POST: Books/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create(Book book) { if (ModelState.IsValid) { try { // BookId and IsAvailable are not bound due to [BindNever] _context.Books.Add(book); await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully added the book: {book.Title}."; return RedirectToAction(nameof(Index)); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while adding the book."; return View(book); } } return View(book); } // GET: Books/Edit/5 public async Task<IActionResult> Edit(int? id) { if (id == null || id == 0) { TempData["ErrorMessage"] = "Book ID was not provided for editing."; return View("NotFound"); } try { var book = await _context.Books.AsNoTracking().FirstOrDefaultAsync(m => m.BookId == id); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {id} for editing."; return View("NotFound"); } return View(book); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the book for editing."; return View("Error"); } } // POST: Books/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int? id, Book book) { if (id == null || id == 0) { TempData["ErrorMessage"] = "Book ID was not provided for updating."; return View("NotFound"); } if (ModelState.IsValid) { try { var existingBook = await _context.Books.FindAsync(id); if (existingBook == null) { TempData["ErrorMessage"] = $"No book found with ID {id} for updating."; return View("NotFound"); } // Updating fields that can be edited existingBook.Title = book.Title; existingBook.Author = book.Author; existingBook.ISBN = book.ISBN; existingBook.PublishedDate = book.PublishedDate; await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully updated the book: {book.Title}."; return RedirectToAction(nameof(Index)); } catch (DbUpdateConcurrencyException ex) { if (!BookExists(book.BookId)) { TempData["ErrorMessage"] = $"No book found with ID {book.BookId} during concurrency check."; return View("NotFound"); } else { TempData["ErrorMessage"] = "A concurrency error occurred during the update."; return View("Error"); } } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while updating the book."; return View("Error"); } } return View(book); } // GET: Books/Delete/5 public async Task<IActionResult> Delete(int? id) { if (id == null || id == 0) { TempData["ErrorMessage"] = "Book ID was not provided for deletion."; return View("NotFound"); } try { var book = await _context.Books .AsNoTracking() .FirstOrDefaultAsync(m => m.BookId == id); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {id} for deletion."; return View("NotFound"); } return View(book); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the book for deletion."; return View("Error"); } } // POST: Books/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(int id) { try { var book = await _context.Books.FindAsync(id); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {id} for deletion."; return View("NotFound"); } _context.Books.Remove(book); await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully deleted the book: {book.Title}."; return RedirectToAction(nameof(Index)); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while deleting the book."; return View("Error"); } } private bool BookExists(int id) { return _context.Books.Any(e => e.BookId == id); } } }
Designing the Views
We will create professional-looking views using Bootstrap for styling.
Layout Configuration:
Ensure the _Layout.cshtml (found in Views/Shared) includes Bootstrap CSS and JS. So, please modify the _Layout.cshtml view as follows. This file serves as the master layout for all views in the application. It includes the HTML structure, references to Bootstrap CSS and JS for styling, a navigation bar with links to different sections (e.g., Books, Add Book), and placeholders (@RenderBody()) for rendering specific view content.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - LibraryManagement</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <a class="navbar-brand" href="/">LibraryManagement</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" href="/Books/Index">Books</a> </li> <li class="nav-item"> <a class="nav-link" href="/Books/Create">Add Book</a> </li> </ul> </div> </div> </nav> <div class="container mt-4"> @RenderBody() </div> <!-- jQuery --> <script src="~/lib/jquery/dist/jquery.min.js"></script> @RenderSection("Scripts", required: false) <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
Create the Index View:
Create a view file named Index.cshtml in the Views/Books folder, and then copy and paste the following code. This view displays a list of all books in a table format. It shows book details like title, author, ISBN, published date, and availability. It provides action buttons for viewing details, editing, deleting, borrowing, or returning books depending on their availability.
@model IEnumerable<LibraryManagement.Models.Book> @{ ViewData["Title"] = "Books List"; } @if (TempData["SuccessMessage"] != null) { <div class="alert alert-success alert-dismissible fade show" role="alert"> @TempData["SuccessMessage"] <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <h2>@ViewData["Title"]</h2> <table class="table table-striped table-hover"> <thead class="table-dark"> <tr> <th>Title</th> <th>Author</th> <th>ISBN</th> <th>Published Date</th> <th>Availability</th> <th>Actions</th> </tr> </thead> <tbody> @foreach (var book in Model) { <tr id="bookRow-@book.BookId"> <td>@book.Title</td> <td>@book.Author</td> <td>@book.ISBN</td> <td>@book.PublishedDate.ToString("yyyy-MM-dd")</td> <td> @if (book.IsAvailable) { <span class="badge bg-success">Available</span> } else { <span class="badge bg-danger">Borrowed</span> } </td> <td> <a asp-action="Details" asp-route-id="@book.BookId" class="btn btn-info btn-sm">Details</a> <a asp-action="Edit" asp-route-id="@book.BookId" class="btn btn-warning btn-sm">Edit</a> <a asp-action="Delete" asp-route-id="@book.BookId" class="btn btn-danger btn-sm">Delete</a> @if (book.IsAvailable) { <a asp-controller="Borrow" asp-action="Create" asp-route-bookId="@book.BookId" class="btn btn-primary btn-sm">Borrow</a> } else { var activeBorrowRecord = book.BorrowRecords?.FirstOrDefault(br => br.ReturnDate == null); if (activeBorrowRecord != null) { <a asp-controller="Borrow" asp-action="Return" asp-route-borrowRecordId="@activeBorrowRecord.BorrowRecordId" class="btn btn-success btn-sm">Return</a> } else { <span class="text-muted">No active borrow record</span> } } </td> </tr> } </tbody> </table> <a asp-action="Create" class="btn btn-primary">Add New Book</a>
Create the Details View:
Create a view file named Details.cshtml in the Views/Books folder, then copy and paste the following code. The Details View shows detailed information about a specific book, including title, author, ISBN, published date, and availability status. It offers options to edit the book or return to the book list.
@model LibraryManagement.Models.Book @{ ViewData["Title"] = "Book Details"; } <div class="container mt-5"> <div class="row"> <!-- Book Cover Image (Optional) --> <div class="col-md-3 text-center"> <img src="https://covers.openlibrary.org/b/ISBN/9780201616224-M.jpg" alt="@Model.Title" class="img-fluid rounded shadow"> </div> <!-- Book Information --> <div class="col-md-9"> <div class="card h-100"> <div class="card-header bg-primary text-white"> <h3 class="card-title">@Model.Title</h3> </div> <div class="card-body"> <dl class="row"> <dt class="col-sm-4">Author:</dt> <dd class="col-sm-8">@Model.Author</dd> <dt class="col-sm-4">ISBN:</dt> <dd class="col-sm-8">@Model.ISBN</dd> <dt class="col-sm-4">Published Date:</dt> <dd class="col-sm-8">@Model.PublishedDate.ToString("yyyy-MM-dd")</dd> <dt class="col-sm-4">Availability:</dt> <dd class="col-sm-8"> @if (Model.IsAvailable) { <span class="badge bg-success">Available</span> } else { <span class="badge bg-danger">Checked Out</span> } </dd> </dl> </div> <div class="card-footer"> <a asp-action="Edit" asp-route-id="@Model.BookId" class="btn btn-warning me-2"> <i class="bi bi-pencil-square"></i> Edit </a> <a asp-action="Index" class="btn btn-secondary"> <i class="bi bi-arrow-left-circle"></i> Back to List </a> </div> </div> </div> </div> </div> <!-- Optional: Include Bootstrap Icons for better button visuals --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
Creating the Create View:
Create a view file named Create.cshtml in the Views/Books folder, and then copy and paste the following code. This view provides a form for adding a new book to the library. The form includes fields for Title, Author, ISBN, and Published Date, but it excludes BookId and IsAvailable due to the [BindNever] attribute.
@model LibraryManagement.Models.Book @{ ViewData["Title"] = "Add New Book"; } <div class="container mt-5"> <!-- Form Card --> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card shadow-sm"> <!-- Card Header --> <div class="card-header bg-primary text-white"> <h4 class="mb-0"> <i class="bi bi-book-fill"></i> Add New Book </h4> </div> <!-- Card Body with Form --> <div class="card-body"> <form asp-action="Create" asp-controller="Books" method="post" class="needs-validation" novalidate> <!-- Validation Summary --> <div asp-validation-summary="All" class="alert alert-danger d-none"></div> <!-- Title Field --> <div class="mb-3"> <label asp-for="Title" class="form-label"></label> <input asp-for="Title" class="form-control" placeholder="Enter book title" autofocus /> <span asp-validation-for="Title" class="text-danger"></span> </div> <!-- Author Field --> <div class="mb-3"> <label asp-for="Author" class="form-label"></label> <input asp-for="Author" class="form-control" placeholder="Enter author's name" /> <span asp-validation-for="Author" class="text-danger"></span> </div> <!-- ISBN Field --> <div class="mb-3"> <label asp-for="ISBN" class="form-label"></label> <input asp-for="ISBN" class="form-control" placeholder="e.g., 978-1234567890" /> <span asp-validation-for="ISBN" class="text-danger"></span> </div> <!-- Published Date Field --> <div class="mb-4"> <label asp-for="PublishedDate" class="form-label"></label> <input asp-for="PublishedDate" class="form-control" type="date" /> <span asp-validation-for="PublishedDate" class="text-danger"></span> </div> <!-- Form Buttons --> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-primary me-2"> <i class="bi bi-plus-circle"></i> Add Book </button> <a asp-action="Index" class="btn btn-secondary"> <i class="bi bi-x-circle"></i> Cancel </a> </div> </form> </div> </div> </div> </div> </div> <!-- Include Bootstrap Icons --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Creating the Edit View:
Create a view file named Edit.cshtml in the Views/Books folder, and then copy and paste the following code. This offers a form for editing an existing book’s details. Similar to the create view, it allows modification of Title, Author, ISBN, and PublishedDate while keeping BookId and IsAvailable protected.
@model LibraryManagement.Models.Book @{ ViewData["Title"] = "Edit Book"; } <div class="container mt-5"> <!-- Form Card --> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card shadow-sm"> <!-- Card Header --> <div class="card-header bg-primary text-white"> <h4 class="mb-0"> <i class="bi bi-pencil-square"></i> Edit Book </h4> </div> <!-- Card Body with Form --> <div class="card-body"> <form asp-action="Edit" asp-controller="Books" method="post" class="needs-validation" novalidate> <!-- Validation Summary --> <div asp-validation-summary="All" class="alert alert-danger d-none"></div> <!-- Hidden BookId Field --> <input type="hidden" asp-for="BookId" /> <!-- Title Field --> <div class="mb-3"> <label asp-for="Title" class="form-label"></label> <input asp-for="Title" class="form-control" /> <span asp-validation-for="Title" class="text-danger"></span> </div> <!-- Author Field --> <div class="mb-3"> <label asp-for="Author" class="form-label"></label> <input asp-for="Author" class="form-control" /> <span asp-validation-for="Author" class="text-danger"></span> </div> <!-- ISBN Field --> <div class="mb-3"> <label asp-for="ISBN" class="form-label"></label> <input asp-for="ISBN" class="form-control" placeholder="e.g., 978-1234567890" /> <span asp-validation-for="ISBN" class="text-danger"></span> </div> <!-- Published Date Field --> <div class="mb-4"> <label asp-for="PublishedDate" class="form-label"></label> <input asp-for="PublishedDate" class="form-control" type="date" /> <span asp-validation-for="PublishedDate" class="text-danger"></span> </div> <!-- Form Buttons --> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-primary me-2"> <i class="bi bi-save"></i> Save Changes </button> <a asp-action="Index" class="btn btn-secondary"> <i class="bi bi-x-circle"></i> Cancel </a> </div> </form> </div> </div> </div> </div> </div> <!-- Include Bootstrap Icons --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Creating the Delete View:
Create a view file named Delete.cshtml in the Views/Books folder, and then copy and paste the following code. This view presents a confirmation page for deleting a book. It displays the book’s details and asks the user to confirm the deletion, emphasizing that the action cannot be undone.
@model LibraryManagement.Models.Book @{ ViewData["Title"] = "Delete Book"; } <div class="container mt-5"> <!-- Delete Confirmation Card --> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card border-danger"> <!-- Card Header --> <div class="card-header bg-danger text-white"> <h4 class="mb-0"> <i class="bi bi-exclamation-triangle-fill"></i> Confirm Delete </h4> </div> <!-- Card Body --> <div class="card-body"> <!-- Alert Message --> <div class="alert alert-danger" role="alert"> <h4 class="alert-heading">Are you sure you want to delete this book?</h4> <p>Once deleted, this action cannot be undone.</p> </div> <!-- Book Details --> <dl class="row"> <dt class="col-sm-4">Title:</dt> <dd class="col-sm-8">@Model.Title</dd> <dt class="col-sm-4">Author:</dt> <dd class="col-sm-8">@Model.Author</dd> <dt class="col-sm-4">ISBN:</dt> <dd class="col-sm-8">@Model.ISBN</dd> <dt class="col-sm-4">Published Date:</dt> <dd class="col-sm-8">@Model.PublishedDate.ToString("yyyy-MM-dd")</dd> <dt class="col-sm-4">Availability:</dt> <dd class="col-sm-8"> @if (Model.IsAvailable) { <span class="badge bg-success">Available</span> } else { <span class="badge bg-danger">Checked Out</span> } </dd> </dl> </div> <!-- Card Footer with Actions --> <div class="card-footer d-flex justify-content-end"> <form asp-action="Delete" asp-controller="Books" method="post" class="me-2"> <input type="hidden" asp-for="BookId" /> <button type="submit" class="btn btn-danger"> <i class="bi bi-trash-fill"></i> Delete </button> </form> <a asp-action="Index" asp-controller="Books" class="btn btn-secondary"> <i class="bi bi-x-circle-fill"></i> Cancel </a> </div> </div> </div> </div> </div> <!-- Include Bootstrap Icons --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
Create the NotFound.cshtml View:
So, create a view file named NotFound.cshtml within the Views/Shared folder and then copy and paste the following code. Provides a user-friendly custom error page for scenarios where a requested resource is not found. It displays an error message and includes a link to navigate back to the book list.
@{ ViewData["Title"] = "Page Not Found"; var errorMessage = ViewBag.ErrorMessage as string; } <div class="container text-center mt-5"> <h1 class="display-4 text-danger">Resource Not Found</h1> <p class="lead"> @if (!string.IsNullOrEmpty(errorMessage)) { @errorMessage } else { <text>The page you are looking for does not exist.</text> } </p> <a asp-action="Index" asp-controller="Books" class="btn btn-primary">Back to Book List</a> </div>
Why Protect the IsAvailable Property with [BindNever] Attribute?
The IsAvailable property in the Book model indicates whether the book is available for borrowing. This property prevents users from borrowing books that have already been checked out and ensures that only available books are listed for borrowing. We want to prevent malicious users from manipulating form data to set IsAvailable to false. We also need to ensure that the availability status is controlled solely by the application logic, not by user input. This is why we decorate this property with [BindNever] Attribute.
Defining Scenarios to Update IsAvailable
To manage the IsAvailable property effectively, we need to identify the key scenarios where its value should change:
Borrowing a Book:
- Action: A user borrows a book.
- Update: Set IsAvailable to false.
Returning a Book:
- Action: A user returns a book.
- Update: Set IsAvailable to true.
Administrative Overrides:
- Action: An administrator marks a book as unavailable (e.g., lost or under maintenance).
- Update: Set IsAvailable to false.
In our application, we will focus on Borrowing and Returning books. So, let us proceed and see how we can implement this.
Creating View Models for Managing Book Borrow and Return:
So, first, create a folder named ViewModels in the Project root directory, and inside this folder, we will create all our View Models.
BorrowViewModel
Create a class file named BorrowViewModel.cs within the ViewModels folder, and then copy and paste the following code. This serves as a view model used for borrowing books. It encapsulates the data required when a user borrows a book, including BookId, BookTitle, BorrowerName, BorrowerEmail, and Phone. It ensures that necessary information is collected from the user when borrowing a book.
using Microsoft.AspNetCore.Mvc.ModelBinding; using System.ComponentModel.DataAnnotations; namespace LibraryManagement.ViewModels { public class BorrowViewModel { [Required] public int BookId { get; set; } [BindNever] public string? BookTitle { get; set; } [Required(ErrorMessage = "Your name is required.")] [StringLength(100, ErrorMessage = "Name cannot exceed 100 characters.")] public string BorrowerName { get; set; } [Required(ErrorMessage = "Your email is required.")] [EmailAddress(ErrorMessage = "Invalid email address.")] public string BorrowerEmail { get; set; } [Required(ErrorMessage = "Your Phone Number is required.")] [Phone(ErrorMessage = "Invalid Phone Number")] public string Phone { get; set; } } }
ReturnViewModel
Create a class file named ReturnViewModel.cs within the ViewModels folder, and then copy and paste the following code. This class serves as a view model for returning borrowed books. It includes properties like BorrowRecordId, BookId, BookTitle, BorrowerName, and BorrowDate. It helps display the necessary information during the return process.
using Microsoft.AspNetCore.Mvc.ModelBinding; using System.ComponentModel.DataAnnotations; namespace LibraryManagement.ViewModels { public class ReturnViewModel { [Required] public int BorrowRecordId { get; set; } [BindNever] public string? BookTitle { get; set; } [BindNever] public string? BorrowerName { get; set; } [BindNever] public DateTime? BorrowDate { get; set; } } }
Creating the BorrowController
Create an empty MVC controller named BorrowController within the Controllers folder, then copy and paste the following code. This controller manages book borrowing and return. It handles actions such as displaying the borrow form (Create GET), processing the borrowing action (Create POST), displaying the return confirmation (Return GET), and processing the return action (Return POST). It updates the books’ IsAvailable status and manages BorrowRecord entries accordingly.
using LibraryManagement.Models; using Microsoft.AspNetCore.Mvc; using LibraryManagement.ViewModels; using Microsoft.EntityFrameworkCore; namespace LibraryManagement.Controllers { public class BorrowController : Controller { private readonly LibraryContext _context; public BorrowController(LibraryContext context) { _context = context; } // Displays the borrow form for a specific book. // GET: Borrow/Create/5 public async Task<IActionResult> Create(int? bookId) { if (bookId == null || bookId == 0) { TempData["ErrorMessage"] = "Book ID was not provided for borrowing."; return View("NotFound"); } try { var book = await _context.Books.FindAsync(bookId); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {bookId} to borrow."; return View("NotFound"); } if (!book.IsAvailable) { TempData["ErrorMessage"] = $"The book '{book.Title}' is currently not available for borrowing."; return View("NotAvailable"); } var borrowViewModel = new BorrowViewModel { BookId = book.BookId, BookTitle = book.Title }; return View(borrowViewModel); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the borrow form."; return View("Error"); } } // Processes the borrowing action, creates a BorrowRecord, updates the book's availability // POST: Borrow/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create(BorrowViewModel model) { if (!ModelState.IsValid) { return View(model); } try { var book = await _context.Books.FindAsync(model.BookId); if (book == null) { TempData["ErrorMessage"] = $"No book found with ID {model.BookId} to borrow."; return View("NotFound"); } if (!book.IsAvailable) { TempData["ErrorMessage"] = $"The book '{book.Title}' is already borrowed."; return View("NotAvailable"); } var borrowRecord = new BorrowRecord { BookId = book.BookId, BorrowerName = model.BorrowerName, BorrowerEmail = model.BorrowerEmail, Phone = model.Phone, BorrowDate = DateTime.UtcNow }; // Update the book's availability book.IsAvailable = false; _context.BorrowRecords.Add(borrowRecord); await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully borrowed the book: {book.Title}."; return RedirectToAction("Index", "Books"); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while processing the borrowing action."; return View("Error"); } } // Displays the return confirmation for a specific borrow record // GET: Borrow/Return/5 public async Task<IActionResult> Return(int? borrowRecordId) { if (borrowRecordId == null || borrowRecordId == 0) { TempData["ErrorMessage"] = "Borrow Record ID was not provided for returning."; return View("NotFound"); } try { var borrowRecord = await _context.BorrowRecords .Include(br => br.Book) .FirstOrDefaultAsync(br => br.BorrowRecordId == borrowRecordId); if (borrowRecord == null) { TempData["ErrorMessage"] = $"No borrow record found with ID {borrowRecordId} to return."; return View("NotFound"); } if (borrowRecord.ReturnDate != null) { TempData["ErrorMessage"] = $"The borrow record for '{borrowRecord.Book.Title}' has already been returned."; return View("AlreadyReturned"); } var returnViewModel = new ReturnViewModel { BorrowRecordId = borrowRecord.BorrowRecordId, BookTitle = borrowRecord.Book.Title, BorrowerName = borrowRecord.BorrowerName, BorrowDate = borrowRecord.BorrowDate }; return View(returnViewModel); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while loading the return confirmation."; return View("Error"); } } // Processes the return action, updates the BorrowRecord with the return date, updates the book's availability // POST: Borrow/Return/5 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Return(ReturnViewModel model) { if (!ModelState.IsValid) { return View(model); } try { var borrowRecord = await _context.BorrowRecords .Include(br => br.Book) .FirstOrDefaultAsync(br => br.BorrowRecordId == model.BorrowRecordId); if (borrowRecord == null) { TempData["ErrorMessage"] = $"No borrow record found with ID {model.BorrowRecordId} to return."; return View("NotFound"); } if (borrowRecord.ReturnDate != null) { TempData["ErrorMessage"] = $"The borrow record for '{borrowRecord.Book.Title}' has already been returned."; return View("AlreadyReturned"); } // Update the borrow record borrowRecord.ReturnDate = DateTime.UtcNow; // Update the book's availability borrowRecord.Book.IsAvailable = true; await _context.SaveChangesAsync(); TempData["SuccessMessage"] = $"Successfully returned the book: {borrowRecord.Book.Title}."; return RedirectToAction("Index", "Books"); } catch (Exception ex) { TempData["ErrorMessage"] = "An error occurred while processing the return action."; return View("Error"); } } } }
Designing the Views for Borrowing and Returning Books
Now, we will create views for borrowing and returning books and custom error views to handle different error scenarios.
Borrow View
Create a view file named Create.cshtml within the Views/Borrow folder, and then copy and paste the following code. The following view provides a form for borrowing a specific book. It includes fields for the borrower’s name, email, and phone number and displays the title of the book being borrowed.
@model LibraryManagement.ViewModels.BorrowViewModel @{ ViewData["Title"] = "Borrow Book"; } <div class="container mt-5"> <h2>Borrow Book</h2> <div class="card"> <div class="card-header"> <strong>@Model.BookTitle</strong> </div> <div class="card-body"> <form asp-action="Create" method="post"> <input type="hidden" asp-for="BookId" /> @* <input type="hidden" asp-for="BookTitle" /> *@ <div class="mb-3"> <label asp-for="BorrowerName" class="form-label"></label> <input asp-for="BorrowerName" class="form-control" /> <span asp-validation-for="BorrowerName" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="BorrowerEmail" class="form-label"></label> <input asp-for="BorrowerEmail" class="form-control" /> <span asp-validation-for="BorrowerEmail" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="Phone" class="form-label"></label> <input asp-for="Phone" class="form-control" /> <span asp-validation-for="Phone" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Confirm Borrow</button> <a asp-action="Index" asp-controller="Books" class="btn btn-secondary">Cancel</a> </form> </div> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Return View
Create a view file named Return.cshtml within the Views/Borrow folder, and then copy and paste the following code. It presents a confirmation page for returning a borrowed book. It displays details about the borrow record and allows the user to confirm the return action.
@model LibraryManagement.ViewModels.ReturnViewModel @{ ViewData["Title"] = "Return Book"; } <div class="container mt-5"> <h2>Return Book</h2> <div class="card"> <div class="card-header"> <strong>@Model.BookTitle</strong> </div> <div class="card-body"> <p><strong>Borrower Name:</strong> @Model.BorrowerName</p> <p><strong>Borrow Date:</strong> @Model.BorrowDate?.ToString("yyyy-MM-dd HH:mm:ss")</p> <form asp-action="Return" method="post"> <input type="hidden" asp-for="BorrowRecordId" /> <button type="submit" class="btn btn-success">Confirm Return</button> <a asp-action="Index" asp-controller="Books" class="btn btn-secondary">Cancel</a> </form> </div> </div> </div>
Not Available View
Create a view file named NotAvailable.cshtml within the Views/Shared folder, then copy and paste the following code. This view displays an error message when a user attempts to borrow a book that is not available. It informs the user that the book is currently unavailable and provides a link back to the book list.
@{ ViewData["Title"] = "Book Not Available"; var errorMessage = ViewBag.ErrorMessage as string; } <div class="container text-center mt-5"> <h1 class="display-4 text-warning">Book Not Available</h1> <p class="lead"> @if (!string.IsNullOrEmpty(errorMessage)) { @errorMessage } else { <text>The book you are trying to borrow is currently not available.</text> } </p> <a asp-action="Index" asp-controller="Books" class="btn btn-primary">Back to Book List</a> </div>
Already Returned View
Create a view file named AlreadyReturned.cshtml within the Views/Shared folder, then copy and paste the following code. This view shows an error message if a user tries to return a book that has already been returned. It notifies the user of the issue and offers navigation back to the main list.
@{ ViewData["Title"] = "Book Already Returned"; var errorMessage = ViewBag.ErrorMessage as string; } <div class="container text-center mt-5"> <h1 class="display-4 text-info">Book Already Returned</h1> <p class="lead"> @if (!string.IsNullOrEmpty(errorMessage)) { @errorMessage } else { <text>The borrow record for this book has already been returned.</text> } </p> <a asp-action="Index" asp-controller="Books" class="btn btn-primary">Back to Book List</a> </div>
That’s it. We have completed our Library Management System implementation with ASP.NET Core MVC and Entity Framework Core. Run the application and test the functionalities; it should work as expected. I hope this will give you a very good idea of how to develop a Library Management System application using ASP.NET Core MVC and EF Core.
In the next article, I will discuss how to develop a Gmail-like registration Process using ASP.NET Core MVC and Entity Framework Core. I hope you enjoy this in-depth article on implementing a Library Management System with ASP.NET Core MVC and Entity Framework Core. Please give your valuable feedback about this article and tell me how we can improve this project.
Hi,
I am looking for Razor pages application instead of mvc based application in .net core.
In my opinion people from strong asp.net web forms background Razor pages are more helpful compared to mvc.
Hoping to see more tutorials on Razor pages tutorials / applications
Thanks