Book Store Project

Bookstore Project Guide

Step-by-Step Guide to Building the Bookstore Project (Murach's ASP.NET Core MVC)

Here's a detailed, step-by-step guide on how to build the Bookstore project, drawing directly from the concepts and examples presented in "Murach's ASP.NET Core MVC." This guide will walk you through setting up your environment, implementing the database, building the core MVC components, adding shopping cart functionality, creating an administration area with search, and finally, securing your application with authentication and authorization.

Introduction to the Bookstore Website

The Bookstore website is a comprehensive ASP.NET Core MVC application designed to showcase many of the essential and advanced skills covered in the book. It features both a customer-facing interface for browsing and purchasing books, and an administration area for managing books, authors, and genres.

User Interface Overview (Chapter 13, Figures 13-1)

  • Customer Pages: Include a Home page with staff picks, a Book Catalog page with paging, sorting, and filtering options, an Author Catalog page with similar features, and a Shopping Cart page. Navigation is handled by a Bootstrap navbar with icons and a cart badge.
  • Admin Pages: Accessible via a separate area, these pages allow administrators to add, edit, and delete books, authors, and genres. A search functionality is also provided for books.

Folder Structure (Chapter 13, Figure 13-2)

The project follows a "fat model, skinny controller" philosophy, with a well-organized Models folder containing subfolders for DataLayer, ExtensionMethods, Grid, ViewModels, and DTOs. Controllers and Views are structured according to MVC conventions, with a dedicated Areas/Admin folder for administration functionalities.

Prerequisites

Before you begin, ensure you have the following software and foundational knowledge:

  • Software:
    • Visual Studio (Community Edition for Windows/Mac) or Visual Studio Code (Chapter 1, Figures 1-9, 1-10; Appendix A, Figure A-1; Appendix B, Figure B-1).
    • .NET Core 3.1 or later (Chapter 1, Figure 1-1).
    • SQL Server Express LocalDB (Windows) or SQLite (macOS) for database management (Chapter 1, Figure 1-1; Appendix A, Figure A-3; Appendix B, Figure B-3, B-4).
  • Foundational Knowledge (from earlier chapters):
    • Basic C# and HTML/CSS.
    • How to create a single-page MVC web app (Chapter 2).
    • How to make a web app responsive with Bootstrap (Chapter 3).
    • How to develop a data-driven MVC web app (Chapter 4).
    • How to manually test and debug (Chapter 5).
    • Working with controllers and routing (Chapter 6).
    • Working with Razor views (Chapter 7).
    • Transferring data from controllers (Chapter 8).
    • Working with session state and cookies (Chapter 9).
    • Working with model binding (Chapter 10).
    • Validating data (Chapter 11).

Step-by-Step Construction of the Bookstore Project

Step 1: Setup and Initial Project Creation

  1. Start a New ASP.NET Core MVC Web App:

    • Open Visual Studio (or VS Code).
    • Select File > New > Project (or File > Open Folder in VS Code).
    • Choose the ASP.NET Core Web Application (or mvc template in VS Code CLI) and select the Web Application (Model-View-Controller) template (Chapter 2, Figures 2-1, 2-2; Chapter 17, Figure 17-6).
    • Give your project a name (e.g., Bookstore) and a solution name.
    // Example: dotnet CLI command to create a new MVC project
    dotnet new mvc -n Bookstore -o BookstoreProject
    
  2. Set Up MVC Folders:

    • If you used the MVC template, delete unnecessary files from Controllers, Models, and Views folders, but keep the folders themselves (Chapter 2, Figure 2-3).
    • If you used the Empty template, manually add Controllers, Models, Views, and their necessary subfolders (Home, Shared) (Chapter 2, Figure 2-3).
    // Example: Manual folder structure (if starting from Empty template)
    // ProjectRoot/
    // ├── Controllers/
    // ├── Models/
    // └── Views/
    //     ├── Home/
    //     └── Shared/
    
  3. Configure the Startup.cs File:

    • Edit Startup.cs to correctly configure the middleware for a basic MVC app. Ensure AddControllersWithViews(), UseSession(), UseRouting(), and UseEndpoints() are correctly set up (Chapter 2, Figure 2-6; Chapter 9, Figure 9-2; Chapter 16, Figure 16-7).
    // Startup.cs - ConfigureServices method
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache(); // Required for session state
        services.AddSession();     // Enables session state
        services.AddControllersWithViews(); // Adds MVC services
    }
    
    // Startup.cs - Configure method
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
    
        app.UseHttpsRedirection(); // Redirects HTTP requests to HTTPS
        app.UseStaticFiles();      // Enables serving static files (CSS, JS, images)
    
        app.UseRouting();          // Marks where routing decisions are made
        app.UseAuthentication();   // Enables authentication middleware (for Identity)
        app.UseAuthorization();    // Enables authorization middleware (for Identity)
        app.UseSession();          // Enables session state middleware
    
        app.UseEndpoints(endpoints => // Configures endpoints for routes
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
    

Step 2: Database and Entity Framework Core (EF Core) Setup

  1. Add EF Core to Your Project:

    • Install the Microsoft.EntityFrameworkCore.SqlServer (for Windows) or Microsoft.EntityFrameworkCore.Sqlite (for macOS) and Microsoft.EntityFrameworkCore.Tools NuGet packages (Chapter 4, Figure 4-3).
    // Example: dotnet CLI commands to install NuGet packages
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    // For SQLite on macOS:
    // dotnet add package Microsoft.EntityFrameworkCore.Sqlite
    
  2. Code Entity Classes:

    • Create Author, Book, BookAuthor, and Genre entity classes in your Models folder. Define their properties and relationships (e.g., Book has a GenreId and Genre navigation property; BookAuthor has composite primary key and navigation properties for Book and Author). Include DataAnnotations attributes for validation and configuration (Chapter 12, Figures 12-1, 12-11).
    // Models/Author.cs
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Mvc; // For Remote attribute
    
    namespace Bookstore.Models
    {
        public class Author
        {
            public int AuthorId { get; set; }
    
            [Required(ErrorMessage = "Please enter a first name.")]
            [StringLength(200)]
            public string FirstName { get; set; }
    
            [Required(ErrorMessage = "Please enter a last name.")]
            [StringLength(200)]
            // Example of remote validation, assuming ValidationController
            [Remote("CheckAuthor", "Validation", "", AdditionalFields = "FirstName, Operation")]
            public string LastName { get; set; }
    
            // Read-only property for full name
            public string FullName => $"{FirstName} {LastName}";
    
            // Navigation property for many-to-many relationship with Book
            public ICollection<BookAuthor> BookAuthors { get; set; }
        }
    }
    
    // Models/Book.cs
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace Bookstore.Models
    {
        public partial class Book
        {
            public int BookId { get; set; }
    
            [Required(ErrorMessage = "Please enter a title.")]
            [StringLength(200)]
            public string Title { get; set; }
    
            [Range(0.0, 1000000.0, ErrorMessage = "Price must be more than 0.")]
            public double Price { get; set; }
    
            [Required(ErrorMessage = "Please select a genre.")]
            public string GenreId { get; set; } // Foreign key property
    
            public Genre Genre { get; set; } // Navigation property
    
            // Navigation property for many-to-many relationship with Author
            public ICollection<BookAuthor> BookAuthors { get; set; }
        }
    }
    
    // Models/BookAuthor.cs (Linking entity for many-to-many)
    namespace Bookstore.Models
    {
        public class BookAuthor
        {
            // Composite primary key and foreign keys
            public int BookId { get; set; }
            public int AuthorId { get; set; }
    
            // Navigation properties
            public Book Book { get; set; }
            public Author Author { get; set; }
        }
    }
    
    // Models/Genre.cs
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Mvc; // For Remote attribute
    
    namespace Bookstore.Models
    {
        public class Genre
        {
            [StringLength(10)]
            [Required(ErrorMessage = "Please enter a genre id.")]
            // Example of remote validation
            [Remote("CheckGenre", "Validation", "")]
            public string GenreId { get; set; }
    
            [StringLength(25)]
            [Required(ErrorMessage = "Please enter a genre name.")]
            public string Name { get; set; }
    
            // Navigation property for one-to-many relationship with Book
            public ICollection<Book> Books { get; set; }
        }
    }
    
  3. Code the DbContext Class:

    • Create a BookstoreContext class that inherits IdentityDbContext<User> (for Identity later) and DbContextOptions<BookstoreContext>. Include DbSet properties for all your entities. Override OnModelCreating to configure relationships (especially many-to-many for BookAuthor) and disable cascading deletes for Genre as needed (Chapter 12, Figures 12-1, 12-12; Chapter 16, Figure 16-5).
    // Models/BookstoreContext.cs
    using Microsoft.EntityFrameworkCore;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore; // For IdentityDbContext
    
    namespace Bookstore.Models
    {
        public class BookstoreContext : IdentityDbContext<User> // Inherit IdentityDbContext
        {
            public BookstoreContext(DbContextOptions<BookstoreContext> options)
                : base(options) { }
    
            // DbSet properties for all your entities
            public DbSet<Author> Authors { get; set; }
            public DbSet<Book> Books { get; set; }
            public DbSet<BookAuthor> BookAuthors { get; set; }
            public DbSet<Genre> Genres { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // IMPORTANT: Call the base method for Identity tables
                base.OnModelCreating(modelBuilder);
    
                // BookAuthor: set composite primary key
                modelBuilder.Entity<BookAuthor>()
                    .HasKey(ba => new { ba.BookId, ba.AuthorId });
    
                // BookAuthor: set foreign keys and relationships
                modelBuilder.Entity<BookAuthor>().HasOne(ba => ba.Book)
                    .WithMany(b => b.BookAuthors)
                    .HasForeignKey(ba => ba.BookId);
    
                modelBuilder.Entity<BookAuthor>().HasOne(ba => ba.Author)
                    .WithMany(a => a.BookAuthors)
                    .HasForeignKey(ba => ba.AuthorId);
    
                // Book: remove cascading delete with Genre (to prevent accidental deletion of books)
                modelBuilder.Entity<Book>().HasOne(b => b.Genre)
                    .WithMany(g => g.Books)
                    .OnDelete(DeleteBehavior.Restrict); // Restrict delete behavior
    
                // Seed initial data (assuming SeedGenres, SeedBooks, etc. are defined as IEntityTypeConfiguration)
                modelBuilder.ApplyConfiguration(new SeedGenres());
                modelBuilder.ApplyConfiguration(new SeedBooks());
                modelBuilder.ApplyConfiguration(new SeedAuthors());
                modelBuilder.ApplyConfiguration(new SeedBookAuthors());
            }
        }
    }
    
  4. Manage Configuration Files (Optional but Recommended):

    • For cleaner code, create separate configuration classes (e.g., SeedGenres, SeedBooks, SeedAuthors, SeedBookAuthors) that implement IEntityTypeConfiguration<T> to define seed data and entity configurations. Apply these configurations in BookstoreContext.OnModelCreating (Chapter 12, Figures 12-3, 12-13).
    // Models/Configuration/SeedGenres.cs
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Metadata.Builders;
    
    namespace Bookstore.Models
    {
        internal class SeedGenres : IEntityTypeConfiguration<Genre>
        {
            public void Configure(EntityTypeBuilder<Genre> entity)
            {
                entity.HasData(
                    new Genre { GenreId = "novel", Name = "Novel" },
                    new Genre { GenreId = "memoir", Name = "Memoir" },
                    new Genre { GenreId = "mystery", Name = "Mystery" },
                    new Genre { GenreId = "scifi", Name = "Science Fiction" },
                    new Genre { GenreId = "history", Name = "History" }
                );
            }
        }
    }
    // ... similar classes for SeedBooks, SeedAuthors, SeedBookAuthors
    
  5. Add Connection String and Enable Dependency Injection:

    • Add your database connection string to appsettings.json (e.g., BookstoreContext).
    • In Startup.cs, configure AddDbContext to use your BookstoreContext and the connection string, enabling dependency injection for your database context (Chapter 4, Figure 4-6; Chapter 12, Figure 12-15).
    // appsettings.json
    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*",
      "ConnectionStrings": {
        "BookstoreContext": "Server=(localdb)\\mssqllocaldb;Database=Bookstore;Trusted_Connection=True;MultipleActiveResultSets=true"
        // For SQLite: "BookstoreContext": "Filename=Bookstore.sqlite"
      }
    }
    
    // Startup.cs - ConfigureServices method (partial)
    public void ConfigureServices(IServiceCollection services)
    {
        // ... other services
        services.AddDbContext<BookstoreContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("BookstoreContext")));
            // For SQLite: options.UseSqlite(Configuration.GetConnectionString("BookstoreContext")));
        // ... other services
    }
    
  6. Use Migrations to Create the Database:

    • Open the Package Manager Console (Visual Studio) or Terminal (VS Code).
    • Run Add-Migration Initial (or dotnet ef migrations add Initial in CLI) to create the initial migration file.
    • Run Update-Database (or dotnet ef database update in CLI) to create the database and seed initial data (Chapter 4, Figure 4-7; Chapter 12, Figures 12-4, 12-5; Chapter 17, Figure 17-4, 17-5).
    // Example: Package Manager Console commands
    PM> Add-Migration Initial
    PM> Update-Database
    
    // Example: dotnet CLI commands
    dotnet ef migrations add Initial
    dotnet ef database update
    

Step 3: Data Layer Implementation (Repository and Unit of Work Patterns)

  1. Encapsulate EF Core Code:

    • Create a DataLayer folder within your Models folder.
    • Implement extension methods for IQueryable<T> (e.g., PageBy) to encapsulate common query operations (Chapter 12, Figure 12-22; Chapter 13, Figure 13-3).
    // Models/DataLayer/QueryExtensions.cs
    using System.Linq;
    
    namespace Bookstore.Models
    {
        public static class QueryExtensions
        {
            // Extension method for paging
            public static IQueryable<T> PageBy<T>(this IQueryable<T> query,
                int pageNumber, int pageSize)
            {
                return query
                    .Skip((pageNumber - 1) * pageSize)
                    .Take(pageSize);
            }
    
            // Other useful string extensions (from Chapter 13, Figure 13-3)
            public static string Slug(this string s) { /* ... */ return s; }
            public static bool EqualsNoCase(this string s, string tocompare) { /* ... */ return true; }
            public static int ToInt(this string s) { /* ... */ return 0; }
            public static string Capitalize(this string s) { /* ... */ return s; }
        }
    }
    
  2. Implement Generic QueryOptions Class:

    • Create a generic QueryOptions<T> class to hold parameters for sorting, paging, and filtering, including OrderByDirection and WhereClauses for multiple filters (Chapter 12, Figure 12-23; Chapter 13, Figure 13-4).
    // Models/DataLayer/QueryOptions.cs
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    
    namespace Bookstore.Models
    {
        public class QueryOptions<T>
        {
            // Public properties for sorting, filtering, and paging
            public Expression<Func<T, Object>> OrderBy { get; set; }
            public string OrderByDirection { get; set; } = "asc"; // default
    
            public int PageNumber { get; set; }
            public int PageSize { get; set; }
    
            private string[] includes;
            public string Includes
            {
                set => includes = value.Replace(" ", "").Split(',');
            }
            public string[] GetIncludes() => includes ?? new string[0];
    
            public WhereClauses<T> WhereClauses { get; set; }
            public Expression<Func<T, bool>> Where // Write-only property to add where clauses
            {
                set
                {
                    if (WhereClauses == null)
                    {
                        WhereClauses = new WhereClauses<T>();
                    }
                    WhereClauses.Add(value);
                }
            }
    
            // Read-only properties for checking if options are set
            public bool HasWhere => WhereClauses != null;
            public bool HasOrderBy => OrderBy != null;
            public bool HasPaging => PageNumber > 0 && PageSize > 0;
        }
    
        // Helper class for multiple where clauses
        public class WhereClauses<T> : List<Expression<Func<T, bool>>> { }
    }
    
  3. Implement Generic Repository Class:

    • Create a generic Repository<T> class that implements an IRepository<T> interface. This class will handle basic CRUD operations and build queries using QueryOptions (Chapter 12, Figure 12-24; Chapter 13, Figure 13-5).
    // Models/DataLayer/IRepository.cs
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Bookstore.Models
    {
        public interface IRepository<T> where T : class
        {
            // Read operations
            IEnumerable<T> List(QueryOptions<T> options);
            T Get(int id);
            T Get(string id);
            T Get(QueryOptions<T> options);
            int Count { get; } // Get count of filtered items
    
            // Write operations
            void Insert(T entity);
            void Update(T entity);
            void Delete(T entity);
            void Save(); // Saves changes to the database
        }
    }
    
    // Models/DataLayer/Repository.cs
    using Microsoft.EntityFrameworkCore;
    using System.Collections.Generic;
    using System.Linq; // For LINQ extension methods
    
    namespace Bookstore.Models
    {
        public class Repository<T> : IRepository<T> where T : class
        {
            protected BookstoreContext context { get; set; }
            private DbSet<T> dbset { get; set; }
    
            public Repository(BookstoreContext ctx)
            {
                context = ctx;
                dbset = context.Set<T>();
            }
    
            private int? count; // Stores count of filtered items
            public int Count => count ?? dbset.Count(); // Returns count, or queries if not set
    
            public virtual IEnumerable<T> List(QueryOptions<T> options)
            {
                IQueryable<T> query = BuildQuery(options);
                return query.ToList();
            }
    
            public virtual T Get(int id) => dbset.Find(id);
            public virtual T Get(string id) => dbset.Find(id);
            public virtual T Get(QueryOptions<T> options)
            {
                IQueryable<T> query = BuildQuery(options);
                return query.FirstOrDefault();
            }
    
            public virtual void Insert(T entity) => dbset.Add(entity);
            public virtual void Update(T entity) => dbset.Update(entity);
            public virtual void Delete(T entity) => dbset.Remove(entity);
            public virtual void Save() => context.SaveChanges();
    
            private IQueryable<T> BuildQuery(QueryOptions<T> options)
            {
                IQueryable<T> query = dbset;
    
                // Apply includes
                foreach (string include in options.GetIncludes())
                {
                    query = query.Include(include);
                }
    
                // Apply where clauses
                if (options.HasWhere)
                {
                    foreach (var clause in options.WhereClauses)
                    {
                        query = query.Where(clause);
                    }
                    count = query.Count(); // Get filtered count after applying where clauses
                }
    
                // Apply order by
                if (options.HasOrderBy)
                {
                    if (options.OrderByDirection == "asc")
                        query = query.OrderBy(options.OrderBy);
                    else
                        query = query.OrderByDescending(options.OrderBy);
                }
    
                // Apply paging
                if (options.HasPaging)
                    query = query.PageBy(options.PageNumber, options.PageSize);
    
                return query;
            }
        }
    }
    
  4. Implement Unit of Work Pattern:

    • Create an IBookstoreUnitOfWork interface and a BookstoreUnitOfWork class that implements it. This class will contain properties for all your repositories (e.g., Books, Authors, Genres, BookAuthors) and a single Save() method to commit changes across all repositories (Chapter 12, Figure 12-25; Chapter 13, Figure 13-14).
    // Models/DataLayer/IBookstoreUnitOfWork.cs
    namespace Bookstore.Models
    {
        public interface IBookstoreUnitOfWork
        {
            IRepository<Book> Books { get; }
            IRepository<Author> Authors { get; }
            IRepository<BookAuthor> BookAuthors { get; }
            IRepository<Genre> Genres { get; }
    
            void Save();
            void DeleteCurrentBookAuthors(Book book);
            void AddNewBookAuthors(Book book, int[] authorids);
        }
    }
    
    // Models/DataLayer/BookstoreUnitOfWork.cs
    using System.Linq;
    using System.Collections.Generic;
    
    namespace Bookstore.Models
    {
        public class BookstoreUnitOfWork : IBookstoreUnitOfWork
        {
            private BookstoreContext context { get; set; }
    
            public BookstoreUnitOfWork(BookstoreContext ctx) => context = ctx;
    
            // Private repository instances (lazy loaded)
            private Repository<Book> bookData;
            private Repository<Author> authorData;
            private Repository<BookAuthor> bookAuthorData;
            private Repository<Genre> genreData;
    
            // Public properties for repositories
            public IRepository<Book> Books
            {
                get
                {
                    if (bookData == null)
                        bookData = new Repository<Book>(context);
                    return bookData;
                }
            }
            public IRepository<Author> Authors
            {
                get
                {
                    if (authorData == null)
                        authorData = new Repository<Author>(context);
                    return authorData;
                }
            }
            public IRepository<BookAuthor> BookAuthors
            {
                get
                {
                    if (bookAuthorData == null)
                        bookAuthorData = new Repository<BookAuthor>(context);
                    return bookAuthorData;
                }
            }
            public IRepository<Genre> Genres
            {
                get
                {
                    if (genreData == null)
                        genreData = new Repository<Genre>(context);
                    return genreData;
                }
            }
    
            public void Save() => context.SaveChanges();
    
            // Helper methods for managing BookAuthors (many-to-many relationship)
            public void DeleteCurrentBookAuthors(Book book)
            {
                var currentAuthors = BookAuthors.List(new QueryOptions<BookAuthor>
                {
                    Where = ba => ba.BookId == book.BookId
                });
                foreach (BookAuthor ba in currentAuthors)
                {
                    BookAuthors.Delete(ba);
                }
            }
    
            public void AddNewBookAuthors(Book book, int[] authorids)
            {
                foreach (int id in authorids)
                {
                    BookAuthor ba = new BookAuthor { BookId = book.BookId, AuthorId = id };
                    BookAuthors.Insert(ba);
                }
            }
        }
    }
    

Step 4: Core MVC Components (Controllers and Views)

  1. Home Controller and Home/Index View:

    • Implement the HomeController with an Index action that selects a random book (staff pick) and passes it to the Home/Index view (Chapter 13, Figure 13-1).
    // Controllers/HomeController.cs
    using Microsoft.AspNetCore.Mvc;
    using System;
    using System.Linq; // For Guid.NewGuid() in OrderBy
    
    namespace Bookstore.Controllers
    {
        public class HomeController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; } // Inject Unit of Work
    
            public HomeController(IBookstoreUnitOfWork unit) => data = unit;
    
            public IActionResult Index()
            {
                // Get a random book for "Staff Selection"
                var randomBook = data.Books.Get(new QueryOptions<Book>
                {
                    Include = "Genre,BookAuthors.Author", // Include related entities
                    OrderBy = b => Guid.NewGuid() // Order by random GUID
                });
    
                return View(randomBook); // Pass the random book as the model
            }
    
            // Other actions like About, Contact, etc.
        }
    }
    
    // Views/Home/Index.cshtml (Simplified example)
    @model Bookstore.Models.Book // Model is a single Book object
    @{
        ViewData["Title"] = "Home | Staff Selection";
    }
    
    <header class="jumbotron text-center">
        <img src="~/images/logo.png" class="img-fluid center-block" alt="Bookstore Logo" />
        <h1 class="mt-3">Staff Selection</h1>
    </header>
    
    <main class="container">
        @if (Model != null)
        {
            <h2>@Model.Title</h2>
            <p>By:
                @foreach (var ba in Model.BookAuthors)
                {
                    <span>@ba.Author.FullName</span>
                    @if (!ba.Equals(Model.BookAuthors.Last()))
                    {
                        <text>, </text>
                    }
                }
            </p>
            <p>Genre: @Model.Genre.Name</p>
            <p>Price: @Model.Price.ToString("C2")</p>
            <a asp-action="Details" asp-controller="Book" asp-route-id="@Model.BookId" asp-route-slug="@Model.Title.Slug()" class="btn btn-primary">View Details</a>
        }
        else
        {
            <p>No staff selection available at the moment.</p>
        }
    </main>
    
  2. Author Catalog (Paging and Sorting):

    • Create AuthorController with a List action that accepts a GridDTO (for paging/sorting parameters).
    • Use GridBuilder to manage route data and QueryOptions to build the database query.
    • Pass an AuthorListViewModel (containing authors, current route, and total pages) to the Author/List view.
    • Design the Author/List view to display authors in a table with sortable columns and paging links (Chapter 13, Figures 13-6, 13-7, 13-8, 13-9, 13-10).
    // Models/ViewModels/AuthorListViewModel.cs
    using System.Collections.Generic;
    
    namespace Bookstore.Models
    {
        public class AuthorListViewModel
        {
            public IEnumerable<Author> Authors { get; set; }
            public RouteDictionary CurrentRoute { get; set; } // For current paging/sorting state
            public int TotalPages { get; set; }
        }
    }
    
    // Controllers/AuthorController.cs
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Http; // For HttpContext.Session
    using System.Linq; // For OrderBy
    
    namespace Bookstore.Controllers
    {
        public class AuthorController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; }
    
            public AuthorController(IBookstoreUnitOfWork unit) => data = unit;
    
            public IActionResult Index() => RedirectToAction("List"); // Redirect to List action
    
            public ViewResult List(GridDTO vals) // Accepts GridDTO for paging/sorting
            {
                // Get GridBuilder object, load route segment values, store in session
                string defaultSortField = nameof(Author.FirstName);
                var builder = new GridBuilder(HttpContext.Session, vals, defaultSortField);
    
                // Create options for querying authors
                var options = new QueryOptions<Author>
                {
                    Includes = "BookAuthors.Book", // Include related books
                    PageNumber = builder.CurrentRoute.PageNumber,
                    PageSize = builder.CurrentRoute.PageSize,
                    OrderByDirection = builder.CurrentRoute.SortDirection
                };
    
                // OrderBy depends on value of SortField route
                if (builder.CurrentRoute.SortField.EqualsNoCase(defaultSortField))
                    options.OrderBy = a => a.FirstName;
                else
                    options.OrderBy = a => a.LastName;
    
                // Create and populate AuthorListViewModel
                var viewModel = new AuthorListViewModel
                {
                    Authors = data.Authors.List(options),
                    CurrentRoute = builder.CurrentRoute,
                    TotalPages = builder.GetTotalPages(data.Authors.Count) // Total authors count
                };
    
                return View(viewModel);
            }
    
            public IActionResult Details(int id)
            {
                // Logic to get author details and pass to view
                var author = data.Authors.Get(new QueryOptions<Author>
                {
                    Where = a => a.AuthorId == id,
                    Includes = "BookAuthors.Book"
                });
                return View(author);
            }
        }
    }
    
    // Views/Author/List.cshtml (Simplified example)
    @model Bookstore.Models.AuthorListViewModel
    @{
        ViewData["Title"] = "Author Catalog";
        // Clone routes for sorting links to avoid modifying current route
        RouteDictionary currentRoute = Model.CurrentRoute;
        RouteDictionary routesForSorting = Model.CurrentRoute.Clone();
    }
    
    <h1>Author Catalog</h1>
    
    <table class="table table-bordered table-striped table-sm">
        <thead class="thead-dark">
            <tr>
                <th>
                    @{ routesForSorting.SetSortAndDirection(nameof(Author.FirstName), currentRoute); }
                    <a asp-action="List" asp-all-route-data="@routesForSorting" class="text-white">First Name</a>
                </th>
                <th>
                    @{ routesForSorting.SetSortAndDirection(nameof(Author.LastName), currentRoute); }
                    <a asp-action="List" asp-all-route-data="@routesForSorting" class="text-white">Last Name</a>
                </th>
                <th>Books(s)</th>
            </tr>
        </thead>
        <tbody>
            @foreach (Author author in Model.Authors)
            {
                <tr>
                    <td>
                        <a asp-action="Details" asp-route-id="@author.AuthorId" asp-route-slug="@author.FullName.Slug()">@author.FirstName</a>
                    </td>
                    <td>
                        <a asp-action="Details" asp-route-id="@author.AuthorId" asp-route-slug="@author.FullName.Slug()">@author.LastName</a>
                    </td>
                    <td>
                        @foreach (var ba in author.BookAuthors)
                        {
                            <p><a asp-action="Details" asp-controller="Book" asp-route-id="@ba.Book.BookId" asp-route-slug="@ba.Book.Title.Slug()">@ba.Book.Title</a></p>
                        }
                    </td>
                </tr>
            }
        </tbody>
    </table>
    
    <div class="d-flex justify-content-center mt-3">
        @{
            // Clone routes for paging links
            RouteDictionary routesForPaging = Model.CurrentRoute.Clone();
            for (int i = 1; i <= Model.TotalPages; i++)
            {
                routesForPaging.PageNumber = i;
                <a asp-action="List" asp-all-route-data="@routesForPaging"
                   class="btn btn-primary @(i == currentRoute.PageNumber ? "active" : "")">@i</a>
            }
        }
    </div>
    
  3. Book Catalog (Paging, Sorting, and Filtering):

    • Create BookController with List and Filter actions.
    • The List action accepts a BooksGridDTO (which extends GridDTO to include filter parameters).
    • Use BooksGridBuilder and BookQueryOptions to handle paging, sorting, and multiple filtering criteria (by author, genre, price range).
    • Pass a BookListViewModel (containing books, filter options, current route, and total pages) to the Book/List view.
    • Design the Book/List view to include filter dropdowns, sortable columns, and paging links (Chapter 13, Figures 13-11, 13-12, 13-13, 13-14, 13-15, 13-16).
    // Models/ViewModels/BookListViewModel.cs
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Bookstore.Models
    {
        public class BookListViewModel
        {
            public IEnumerable<Book> Books { get; set; }
            public RouteDictionary CurrentRoute { get; set; }
            public int TotalPages { get; set; }
    
            // For filter drop-down data
            public IEnumerable<Author> Authors { get; set; }
            public IEnumerable<Genre> Genres { get; set; }
            public Dictionary<string, string> Prices =>
                new Dictionary<string, string>
                {
                    { "under7", "Under $7" },
                    { "7to14", "$7 to $14" },
                    { "over14", "Over $14" }
                };
        }
    }
    
    // Controllers/BookController.cs (Simplified)
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Http;
    using System.Linq;
    
    namespace Bookstore.Controllers
    {
        public class BookController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; }
    
            public BookController(IBookstoreUnitOfWork unit) => data = unit;
    
            public ViewResult List(BooksGridDTO values) // Accepts BooksGridDTO for all options
            {
                // Initialize BooksGridBuilder to manage route data in session
                var builder = new BooksGridBuilder(HttpContext.Session, values,
                                                   defaultSortField: nameof(Book.Title));
    
                // Create query options for books
                var options = new BookQueryOptions
                {
                    Includes = "BookAuthors.Author, Genre",
                    OrderByDirection = builder.CurrentRoute.SortDirection,
                    PageNumber = builder.CurrentRoute.PageNumber,
                    PageSize = builder.CurrentRoute.PageSize
                };
    
                // Apply sorting and filtering based on builder's state
                options.SortFilter(builder);
    
                // Create and populate BookListViewModel
                var viewModel = new BookListViewModel
                {
                    Books = data.Books.List(options),
                    Authors = data.Authors.List(new QueryOptions<Author> { OrderBy = a => a.FirstName }),
                    Genres = data.Genres.List(new QueryOptions<Genre> { OrderBy = g => g.Name }),
                    CurrentRoute = builder.CurrentRoute,
                    TotalPages = builder.GetTotalPages(data.Books.Count)
                };
    
                return View(viewModel);
            }
    
            [HttpPost]
            public RedirectToActionResult Filter(string[] filter, bool clear = false)
            {
                var builder = new BooksGridBuilder(HttpContext.Session); // Load current state
    
                if (clear)
                {
                    builder.ClearFilterSegments();
                }
                else
                {
                    // Get author object to include slug if needed in the filter segment
                    var author = data.Authors.Get(filter[0].ToInt());
                    builder.LoadFilterSegments(filter, author);
                }
    
                builder.SaveRouteSegments(); // Save updated route segments to session
    
                // Redirect to List action with updated route parameters
                return RedirectToAction("List", builder.CurrentRoute);
            }
    
            // Details action for individual book details
            public IActionResult Details(int id)
            {
                var book = data.Books.Get(new QueryOptions<Book>
                {
                    Where = b => b.BookId == id,
                    Includes = "Genre,BookAuthors.Author"
                });
                return View(book);
            }
        }
    }
    
    // Views/Book/List.cshtml (Simplified example)
    @model Bookstore.Models.BookListViewModel
    @{
        ViewData["Title"] = "Book Catalog";
        RouteDictionary currentRoute = Model.CurrentRoute;
        RouteDictionary routesForSorting = Model.CurrentRoute.Clone();
    }
    
    <h1>Book Catalog</h1>
    
    <form asp-action="Filter" method="post" class="form-inline mb-3">
        <label class="mr-2">Author:</label>
        <vc:author-drop-down selected-value="@currentRoute.AuthorFilter"></vc:author-drop-down>
    
        <label class="ml-3 mr-2">Genre:</label>
        <vc:genre-drop-down selected-value="@currentRoute.GenreFilter"></vc:genre-drop-down>
    
        <label class="ml-3 mr-2">Price:</label>
        <vc:price-drop-down selected-value="@currentRoute.PriceFilter"></vc:price-drop-down>
    
        <button type="submit" class="btn btn-primary ml-3 mr-2">Filter</button>
        <button type="submit" class="btn btn-secondary" name="clear" value="true">Clear</button>
    </form>
    
    <form asp-action="Add" asp-controller="Cart" method="post">
        <table class="table table-bordered table-striped table-sm">
            <thead class="thead-dark">
                <tr>
                    <th>
                        <my-sorting-link sort-field="Title"
                        current="@currentRoute">Title</my-sorting-link></th>
                    <th>Author(s)</th>
                    <th>
                        <my-sorting-link sort-field="Genre"
                        current="@currentRoute">Genre</my-sorting-link></th>
                    <th>
                        <my-sorting-link sort-field="Price"
                        current="@currentRoute">Price</my-sorting-link></th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach (Book book in Model.Books)
                {
                    <tr>
                        <td><partial name="_BookLinkPartial" model="@book" /></td>
                        <td>
                            @foreach (var ba in book.BookAuthors)
                            {
                                <p><partial name="_AuthorLinkPartial" model="@ba.Author" /></p>
                            }
                        </td>
                        <td>@book.Genre?.Name</td>
                        <td>@book.Price.ToString("C2")</td>
                        <td>
                            <button type="submit" name="id" value="@book.BookId" class="btn btn-success btn-sm">Add To Cart</button>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </form>
    
    <partial name="_PagingLinksPartial" model="@Model" />
    
  4. Common Razor View Skills:

    • Ensure your views use Razor syntax effectively for code blocks, inline expressions, and conditional statements (Chapter 7, Figures 7-1, 7-2, 7-3).
    • Set up a default layout (_Layout.cshtml) and _ViewStart.cshtml for consistent page structure and to enable tag helpers via _ViewImports.cshtml (Chapter 7, Figures 7-4, 7-6).
    • Utilize built-in tag helpers (asp-action, asp-controller, asp-for, asp-items, asp-validation-for, asp-validation-summary) for generating URLs, binding data, and displaying validation messages (Chapter 7, Figures 7-7, 7-14, 7-15; Chapter 11, Figure 11-6).
    • Format numbers in views (Chapter 7, Figure 7-11).
    • Pass models to views and display model properties (Chapter 7, Figures 7-12, 7-13).
    // Views/_ViewImports.cshtml
    @using Bookstore
    @using Bookstore.Models
    @using Bookstore.Models.ViewModels
    @using Bookstore.Models.DataLayer
    @using Bookstore.Models.DataLayer.SeedData
    @using Bookstore.Models.ExtensionMethods
    @using Bookstore.Models.Grid
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers // Built-in tag helpers
    @addTagHelper *, Bookstore // Custom tag helpers from your project
    
    // Views/_ViewStart.cshtml
    @{
        Layout = "_Layout"; // Sets the default layout for all views
    }
    
    // Views/Shared/_Layout.cshtml (Simplified)
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Bookstore | @ViewData["Title"]</title>
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" xintegrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
        <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    </head>
    <body>
        <div class="container">
            <nav class="navbar navbar-expand-md navbar-dark bg-dark">
                <partial name="_NavbarMenuButtonPartial" />
                <div class="collapse navbar-collapse" id="menu">
                    <ul class="navbar-nav mr-auto">
                        <li class="nav-item">
                            <a class="nav-link" asp-action="Index" asp-controller="Home" asp-area="">
                                <span class="fas fa-home"></span>&nbsp;Home
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" asp-action="List" asp-controller="Book" asp-area="">
                                <span class="fas fa-book-open"></span>&nbsp;Books
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" asp-action="List" asp-controller="Author" asp-area="">
                                <span class="fas fa-user-tie"></span>&nbsp;Authors
                            </a>
                        </li>
                    </ul>
                    <ul class="navbar-nav ml-auto">
                        <li class="nav-item">
                            <a class="nav-link" asp-action="Index" asp-controller="Cart" asp-area="">
                                <span class="fas fa-shopping-cart"></span>&nbsp;Cart
                                <vc:cart-badge></vc:cart-badge>
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" asp-action="Register" asp-controller="Account" asp-area="">Register</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" asp-action="Login" asp-controller="Account" asp-area="" class="btn btn-outline-light">Log In</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" asp-action="Index" asp-controller="Book" asp-area="Admin" my-mark-area-active>
                                <span class="fas fa-cog"></span>&nbsp;Admin
                            </a>
                        </li>
                    </ul>
                </div>
            </nav>
            <header class="jumbotron text-center">
                <a asp-action="Index" asp-controller="Home">
                    <img src="~/images/logo.png" class="img-fluid center-block" alt="Bookstore Logo" />
                </a>
            </header>
            <main>
                <my-temp-message></my-temp-message>
                @RenderBody()
            </main>
        </div>
        <script src="~/lib/jquery/dist/jquery.min.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
        @RenderSection("Scripts", required: false)
    </body>
    </html>
    

Step 5: Shopping Cart Functionality

  1. Implement Session and Cookie Extension Methods:

    • Create static extension methods for ISession (SetObject<T>, GetObject<T>) to store complex objects as JSON in session state.
    • Create static extension methods for IRequestCookieCollection and IResponseCookies to store and retrieve complex objects as JSON in persistent cookies (Chapter 13, Figure 13-3, 13-17; Chapter 9, Figure 9-4, 9-5, 9-6).
    // Models/ExtensionMethods/SessionExtensions.cs
    using Microsoft.AspNetCore.Http;
    using Newtonsoft.Json; // Requires Newtonsoft.Json NuGet package
    
    namespace Bookstore.Models.ExtensionMethods
    {
        public static class SessionExtensions
        {
            public static void SetObject<T>(this ISession session, string key, T value)
            {
                session.SetString(key, JsonConvert.SerializeObject(value));
            }
    
            public static T GetObject<T>(this ISession session, string key)
            {
                var value = session.GetString(key);
                return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
            }
        }
    }
    
    // Models/ExtensionMethods/CookieExtensions.cs
    using Microsoft.AspNetCore.Http;
    using Newtonsoft.Json;
    using System;
    using System.Linq; // For Sum()
    
    namespace Bookstore.Models.ExtensionMethods
    {
        public static class CookieExtensions
        {
            public static string GetString(this IRequestCookieCollection cookies, string key) => cookies[key];
    
            public static int? GetInt32(this IRequestCookieCollection cookies, string key) =>
                int.TryParse(cookies[key], out int i) ? i : (int?)null;
    
            public static T GetObject<T>(this IRequestCookieCollection cookies, string key)
            {
                var value = cookies[key];
                return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
            }
    
            public static void SetString(this IResponseCookies cookies, string key,
                string value, int days = 30)
            {
                cookies.Delete(key); // Delete old value first
                if (days == 0) // Session cookie
                {
                    cookies.Append(key, value);
                }
                else // Persistent cookie
                {
                    CookieOptions options = new CookieOptions { Expires = DateTime.Now.AddDays(days) };
                    cookies.Append(key, value, options);
                }
            }
    
            public static void SetInt32(this IResponseCookies cookies, string key,
                int value, int days = 30) =>
                cookies.SetString(key, value.ToString(), days);
    
            public static void SetObject<T>(this IResponseCookies cookies, string key,
                T value, int days = 30) =>
                cookies.SetString(key, JsonConvert.SerializeObject(value), days);
        }
    }
    
  2. Define Cart Model Classes:

    • Create CartItem class (stores BookDTO and Quantity, calculates Subtotal, uses JsonIgnore for calculated properties).
    • Create BookDTO class (a Data Transfer Object for Book to avoid circular references and store minimal data).
    • Create CartItemDTO class (minimal data for persistent cookies).
    • Create CartViewModel (for passing cart data to the view) (Chapter 13, Figures 13-18, 13-19).
    // Models/CartItem.cs
    using Newtonsoft.Json; // For JsonIgnore
    
    namespace Bookstore.Models
    {
        public class CartItem
        {
            public BookDTO Book { get; set; }
            public int Quantity { get; set; }
    
            [JsonIgnore] // Don't serialize this calculated property
            public double Subtotal => Book.Price * Quantity;
        }
    }
    
    // Models/BookDTO.cs (Data Transfer Object for Book)
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Bookstore.Models
    {
        public class BookDTO
        {
            public int BookId { get; set; }
            public string Title { get; set; }
            public double Price { get; set; }
            public Dictionary<int, string> Authors { get; set; } // AuthorId and FullName
    
            // Method to load data from a full Book entity
            public void Load(Book book)
            {
                BookId = book.BookId;
                Title = book.Title;
                Price = book.Price;
                Authors = new Dictionary<int, string>();
                foreach (BookAuthor ba in book.BookAuthors)
                {
                    Authors.Add(ba.Author.AuthorId, ba.Author.FullName);
                }
            }
        }
    }
    
    // Models/CartItemDTO.cs (Minimal data for persistent cookie)
    namespace Bookstore.Models
    {
        public class CartItemDTO
        {
            public int BookId { get; set; }
            public int Quantity { get; set; }
        }
    }
    
    // Models/ViewModels/CartViewModel.cs
    using System.Collections.Generic;
    
    namespace Bookstore.Models.ViewModels
    {
        public class CartViewModel
        {
            public IEnumerable<CartItem> List { get; set; }
            public RouteDictionary BookGridRoute { get; set; } // For "Back to Shopping" link
            public double Subtotal { get; set; }
        }
    }
    
  3. Implement the Cart Class:

    • Create a Cart class responsible for managing cart items, loading them from session or cookies, adding/editing/removing items, and saving changes to both session state and persistent cookies (Chapter 13, Figure 13-20).
    // Models/Cart.cs
    using Microsoft.AspNetCore.Http;
    using Bookstore.Models.ExtensionMethods; // For SessionExtensions and CookieExtensions
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Bookstore.Models
    {
        public class Cart
        {
            private const string CartKey = "mycart";
            private const string CountKey = "mycount";
    
            private List<CartItem> items { get; set; }
            private List<CartItemDTO> storedItems { get; set; } // For items from persistent cookie
    
            private ISession session { get; set; }
            private IRequestCookieCollection requestCookies { get; set; }
            private IResponseCookies responseCookies { get; set; }
    
            public Cart(IHttpContextAccessor ctx) // Injected HttpContextAccessor
            {
                session = ctx.HttpContext.Session;
                requestCookies = ctx.HttpContext.Request.Cookies;
                responseCookies = ctx.HttpContext.Response.Cookies;
                items = new List<CartItem>(); // Initialize to prevent NullReferenceException in tests
            }
    
            // Load cart items from session or persistent cookie
            public void Load(IRepository<Book> data)
            {
                items = session.GetObject<List<CartItem>>(CartKey); // Try to get from session
    
                if (items == null) // If not in session, try to get from cookie
                {
                    items = new List<CartItem>();
                    storedItems = requestCookies.GetObject<List<CartItemDTO>>(CartKey);
                }
    
                // If items from cookie are more than session, restore session from cookie
                if (storedItems?.Count > items?.Count)
                {
                    foreach (CartItemDTO storedItem in storedItems)
                    {
                        var book = data.Get(new QueryOptions<Book>
                        {
                            Include = "BookAuthors.Author, Genre",
                            Where = b => b.BookId == storedItem.BookId
                        });
                        if (book != null) // Only add if book still exists in DB
                        {
                            var dto = new BookDTO();
                            dto.Load(book);
                            CartItem item = new CartItem
                            {
                                Book = dto,
                                Quantity = storedItem.Quantity
                            };
                            items.Add(item);
                        }
                    }
                    Save(); // Save restored items to session and cookie
                }
            }
    
            // Read-only properties for cart data
            public double Subtotal => items.Sum(c => c.Subtotal);
            public int? Count => session.GetInt32(CountKey) ?? requestCookies.GetInt32(CountKey); // Get count from session or cookie
            public IEnumerable<CartItem> List => items;
    
            public CartItem GetById(int id) => items.FirstOrDefault(ci => ci.Book.BookId == id);
    
            public void Add(CartItem item)
            {
                var itemInCart = GetById(item.Book.BookId);
                if (itemInCart == null) // If new item, add it
                {
                    items.Add(item);
                }
                else // Otherwise, increase quantity by 1
                {
                    itemInCart.Quantity += 1;
                }
            }
    
            public void Edit(CartItem item)
            {
                var itemInCart = GetById(item.Book.BookId);
                if (itemInCart != null)
                {
                    itemInCart.Quantity = item.Quantity; // Update quantity
                }
            }
    
            public void Remove(CartItem item) => items.Remove(item);
    
            public void Clear() => items.Clear();
    
            // Save cart items to session and persistent cookie
            public void Save()
            {
                if (items.Count == 0)
                {
                    session.Remove(CartKey);
                    session.Remove(CountKey);
                    responseCookies.Delete(CartKey);
                    responseCookies.Delete(CountKey);
                }
                else
                {
                    session.SetObject<List<CartItem>>(CartKey, items);
                    session.SetInt32(CountKey, items.Count);
                    responseCookies.SetObject<List<CartItemDTO>>(
                        CartKey, items.ToDTO()); // Convert CartItem list to DTO list for cookie
                    responseCookies.SetInt32(CountKey, items.Count);
                }
            }
        }
    }
    
  4. Implement CartController and Cart/Index View:

    • Create CartController with actions for Index (display cart), Add (add book to cart), Remove (remove item), Clear (clear cart), and Edit (edit item quantity).
    • The Add action should use the Post-Redirect-Get (PRG) pattern and TempData for messages.
    • The Index action should load the cart data and pass it to the CartViewModel.
    • Design the Cart/Index view to display cart items, subtotal, and buttons for checkout, clearing the cart, and continuing shopping (Chapter 13, Figures 13-21, 13-22; Chapter 8, Figure 8-13, 8-14).
    // Controllers/CartController.cs
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Http; // For IHttpContextAccessor
    using Bookstore.Models.ExtensionMethods; // For SessionExtensions
    using Bookstore.Models.ViewModels;
    using System.Linq;
    
    namespace Bookstore.Controllers
    {
        public class CartController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; } // Injected Unit of Work
            private Cart cart { get; set; } // Model-level Cart object
    
            // Constructor with dependency injection
            public CartController(IBookstoreUnitOfWork unit, IHttpContextAccessor accessor)
            {
                data = unit;
                cart = new Cart(accessor); // Create Cart object using accessor
                cart.Load(data); // Load cart data
            }
    
            public ViewResult Index()
            {
                var builder = new BooksGridBuilder(HttpContext.Session); // For "Back to Shopping" link
                var viewModel = new CartViewModel
                {
                    List = cart.List,
                    Subtotal = cart.Subtotal,
                    BookGridRoute = builder.CurrentRoute // Pass current book catalog route
                };
                return View(viewModel);
            }
    
            [HttpPost]
            public RedirectToActionResult Add(int id) // BookId from button value
            {
                var book = data.Books.Get(new QueryOptions<Book>
                {
                    Include = "BookAuthors.Author, Genre",
                    Where = b => b.BookId == id
                });
    
                if (book == null)
                {
                    TempData["message"] = "Unable to add book to cart.";
                }
                else
                {
                    var dto = new BookDTO();
                    dto.Load(book);
                    CartItem item = new CartItem { Book = dto, Quantity = 1 }; // Default quantity
                    cart.Add(item);
                    cart.Save(); // Save changes to session and cookie
                    TempData["message"] = $"{book.Title} added to cart.";
                }
    
                var builder = new BooksGridBuilder(HttpContext.Session);
                return RedirectToAction("List", "Book", builder.CurrentRoute); // PRG pattern
            }
    
            [HttpPost]
            public RedirectToActionResult Remove(int id) // BookId from button value
            {
                CartItem item = cart.GetById(id);
                if (item != null)
                {
                    cart.Remove(item);
                    cart.Save();
                    TempData["message"] = $"{item.Book.Title} removed from cart.";
                }
                return RedirectToAction("Index"); // PRG pattern
            }
    
            [HttpPost]
            public RedirectToActionResult Clear()
            {
                cart.Clear();
                cart.Save();
                TempData["message"] = "Your cart has been cleared.";
                return RedirectToAction("Index"); // PRG pattern
            }
    
            // Edit actions (GET and POST) are similar to other edit forms
            [HttpGet]
            public IActionResult Edit(int id)
            {
                var item = cart.GetById(id);
                if (item == null)
                {
                    TempData["message"] = "Cart item not found.";
                    return RedirectToAction("Index");
                }
                return View(item);
            }
    
            [HttpPost]
            public IActionResult Edit(CartItem item)
            {
                if (ModelState.IsValid)
                {
                    cart.Edit(item);
                    cart.Save();
                    TempData["message"] = $"{item.Book.Title} quantity updated.";
                    return RedirectToAction("Index");
                }
                return View(item);
            }
        }
    }
    
    // Views/Cart/Index.cshtml (Simplified)
    @model Bookstore.Models.ViewModels.CartViewModel
    @{
        ViewData["Title"] = "Your Cart";
    }
    
    <h1>Your Cart</h1>
    
    <form asp-action="Clear" method="post" class="mb-3">
        <ul class="list-group">
            <li class="list-group-item">
                <div class="row">
                    <div class="col">Subtotal: @Model.Subtotal.ToString("C2")</div>
                    <div class="col">
                        <div class="float-right">
                            <a asp-action="Checkout" class="btn btn-primary mr-2">Checkout</a>
                            <button type="submit" class="btn btn-danger mr-2">Clear Cart</button>
                            <a asp-action="List" asp-controller="Book" asp-all-route-data="@Model.BookGridRoute" class="btn btn-secondary">Back to Shopping</a>
                        </div>
                    </div>
                </div>
            </li>
        </ul>
    </form>
    
    <form asp-action="Remove" method="post">
        <table class="table table-bordered table-striped">
            <thead class="thead-dark">
                <tr>
                    <th>Title</th>
                    <th>Author(s)</th>
                    <th>Price</th>
                    <th>Quantity</th>
                    <th>Subtotal</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach (CartItem item in Model.List)
                {
                    <tr>
                        <td><partial name="_BookLinkPartial" model="@item.Book" /></td>
                        <td>
                            @foreach (var author in item.Book.Authors)
                            {
                                <p><partial name="_AuthorLinkPartial" model="@new Author { AuthorId = author.Key, FirstName = author.Value.Split(' ')[0], LastName = author.Value.Split(' ')[1] }" /></p>
                            }
                        </td>
                        <td>@item.Book.Price.ToString("C2")</td>
                        <td>@item.Quantity</td>
                        <td>@item.Subtotal.ToString("C2")</td>
                        <td>
                            <div class="float-right">
                                <a asp-action="Edit" asp-controller="Cart" asp-route-id="@item.Book.BookId" asp-route-slug="@item.Book.Title.Slug()" class="btn btn-info btn-sm mr-2">Edit</a>
                                <button type="submit" name="id" value="@item.Book.BookId" class="btn btn-danger btn-sm">Remove</button>
                            </div>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </form>
    

Step 6: Admin Area and Search Functionality

  1. Set Up Admin Area:

    • Create an Areas/Admin folder structure mirroring the main MVC structure (Controllers, Models, Views).
    • Configure a specific route for the Admin area in Startup.cs using MapAreaControllerRoute (Chapter 6, Figure 6-11).
    // Startup.cs - Configure method (partial)
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ... other middleware
    
        app.UseEndpoints(endpoints =>
        {
            // Admin area route (most specific)
            endpoints.MapAreaControllerRoute(
                name: "admin",
                areaName: "Admin",
                pattern: "Admin/{controller=Book}/{action=Index}/{id?}/{slug?}"); // Default to Book in Admin area
    
            // Custom route for Book/Author catalog paging/sorting/filtering
            endpoints.MapControllerRoute(
                name: "filtered_books_authors",
                pattern: "{controller}/{action}/page/{pagenumber}/size/{pagesize}/sort/{sortfield}/{sortdirection}/filter/{author}/{genre}/{price}");
    
            // Default route
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}/{slug?}");
        });
    }
    
  2. Implement Admin Book Search:

    • Create SearchData class to store search terms and types in TempData (using Peek() for persistence across multiple actions) (Chapter 13, Figure 13-24).
    • Create SearchViewModel for passing search results to the view.
    • Implement Search() action methods (GET and POST) in Admin/BookController. The POST action validates input, stores search data in TempData, and redirects to the GET action. The GET action retrieves search data from TempData, builds a query based on search type (title, author, genre), executes it, and passes results to a SearchResults view (Chapter 13, Figures 13-23, 13-24, 13-25).
    // Models/SearchData.cs
    using Microsoft.AspNetCore.Mvc.ViewFeatures; // For ITempDataDictionary
    using Bookstore.Models.ExtensionMethods; // For EqualsNoCase, ToInt
    
    namespace Bookstore.Models
    {
        public class SearchData
        {
            private const string SearchKey = "search";
            private const string TypeKey = "searchtype";
    
            private ITempDataDictionary tempData { get; set; }
    
            public SearchData(ITempDataDictionary temp) => tempData = temp;
    
            // Use Peek() rather than a straight read so value will persist
            public string SearchTerm
            {
                get => tempData.Peek(SearchKey)?.ToString();
                set => tempData[SearchKey] = value;
            }
    
            public string Type
            {
                get => tempData.Peek(TypeKey)?.ToString();
                set => tempData[TypeKey] = value;
            }
    
            public bool HasSearchTerm => !string.IsNullOrEmpty(SearchTerm);
            public bool IsBook => Type.EqualsNoCase("book");
            public bool IsAuthor => Type.EqualsNoCase("author");
            public bool IsGenre => Type.EqualsNoCase("genre");
    
            public void Clear()
            {
                tempData.Remove(SearchKey);
                tempData.Remove(TypeKey);
            }
        }
    }
    
    // Models/ViewModels/SearchViewModel.cs
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace Bookstore.Models.ViewModels
    {
        public class SearchViewModel
        {
            public IEnumerable<Book> Books { get; set; }
    
            [Required(ErrorMessage = "Please enter a search term.")]
            public string SearchTerm { get; set; }
            public string Type { get; set; } // "book", "author", "genre"
            public string Header { get; set; } // Header text for search results
        }
    }
    
    // Areas/Admin/Controllers/BookController.cs (Simplified Search actions)
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.ViewFeatures; // For ITempDataDictionary
    using System.Linq; // For Contains, Any
    using Bookstore.Models.ExtensionMethods; // For Slug, ToInt, EqualsNoCase
    
    namespace Bookstore.Areas.Admin.Controllers
    {
        [Area("Admin")]
        // [Authorize(Roles = "Admin")] // Uncomment to restrict access
        public class BookController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; }
    
            public BookController(IBookstoreUnitOfWork unit) => data = unit;
    
            // POST action for search form submission
            [HttpPost]
            public RedirectToActionResult Search(SearchViewModel vm)
            {
                if (ModelState.IsValid)
                {
                    var search = new SearchData(TempData)
                    {
                        SearchTerm = vm.SearchTerm,
                        Type = vm.Type
                    };
                    return RedirectToAction("Search"); // Redirect to GET Search action
                }
                else
                {
                    // If validation fails, return to the Index view (Manage Books tab)
                    return RedirectToAction("Index");
                }
            }
    
            // GET action to display search results
            [HttpGet]
            public ViewResult Search()
            {
                var search = new SearchData(TempData); // Load search data from TempData
    
                if (search.HasSearchTerm)
                {
                    var viewModel = new SearchViewModel
                    {
                        SearchTerm = search.SearchTerm
                    };
    
                    var options = new QueryOptions<Book>
                    {
                        Includes = "Genre, BookAuthors.Author" // Include related data
                    };
    
                    // Build query based on search type
                    if (search.IsBook)
                    {
                        options.Where = b => b.Title.Contains(viewModel.SearchTerm);
                        viewModel.Header = $"Search results for book title '{viewModel.SearchTerm}'";
                    }
                    else if (search.IsAuthor)
                    {
                        // Handle single word or multiple words in author search term
                        int index = viewModel.SearchTerm.LastIndexOf(' ');
                        if (index == -1) // Single word
                        {
                            options.Where = b => b.BookAuthors.Any(
                                ba => ba.Author.FirstName.Contains(viewModel.SearchTerm) ||
                                      ba.Author.LastName.Contains(viewModel.SearchTerm));
                        }
                        else // Multiple words (assume First Last)
                        {
                            string first = viewModel.SearchTerm.Substring(0, index);
                            string last = viewModel.SearchTerm.Substring(index + 1);
                            options.Where = b => b.BookAuthors.Any(
                                ba => ba.Author.FirstName.Contains(first) &&
                                      ba.Author.LastName.Contains(last));
                        }
                        viewModel.Header = $"Search results for author '{viewModel.SearchTerm}'";
                    }
                    else if (search.IsGenre)
                    {
                        options.Where = b => b.GenreId.Contains(viewModel.SearchTerm);
                        viewModel.Header = $"Search results for genre ID '{viewModel.SearchTerm}'";
                    }
    
                    viewModel.Books = data.Books.List(options); // Execute query
                    return View("SearchResults", viewModel);
                }
                else
                {
                    // If no search term, redirect back to the main admin index
                    return View("Index");
                }
            }
    
            // Other CRUD actions for books (Add, Edit, Delete)
            // ...
        }
    }
    
    // Views/Admin/Book/SearchResults.cshtml (Simplified)
    @model Bookstore.Models.ViewModels.SearchViewModel
    @{
        ViewData["Title"] = "Search Results";
    }
    
    <h1>@Model.Header</h1>
    
    @if (Model.Books.Any())
    {
        <table class="table table-bordered table-striped">
            <thead class="thead-dark">
                <tr>
                    <th>Title</th>
                    <th>Author(s)</th>
                    <th>Genre</th>
                    <th>Price</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach (var book in Model.Books)
                {
                    <tr>
                        <td><partial name="_BookLinkPartial" model="@book" /></td>
                        <td>
                            @foreach (var ba in book.BookAuthors)
                            {
                                <p><partial name="_AuthorLinkPartial" model="@ba.Author" /></p>
                            }
                        </td>
                        <td>@book.Genre?.Name</td>
                        <td>@book.Price.ToString("C2")</td>
                        <td>
                            <a asp-action="Edit" asp-route-id="@book.BookId" class="btn btn-info btn-sm mr-2">Edit</a>
                            <a asp-action="Delete" asp-route-id="@book.BookId" class="btn btn-danger btn-sm">Delete</a>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    }
    else
    {
        <p>No books found matching your search criteria.</p>
    }
    
    <a asp-action="Index" class="btn btn-secondary">Back to Manage Books</a>
    
  3. Implement Genre/Author Delete with Related Books Check:

    • Modify Admin/GenreController's Delete() action (GET) to check for related books. If books exist, redirect to the book search results page, pre-filtered by that genre, using SearchData (Chapter 13, Figure 13-26). A similar logic applies to authors.
    // Areas/Admin/Controllers/GenreController.cs (Simplified Delete action)
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.ViewFeatures; // For ITempDataDictionary
    using System.Linq;
    
    namespace Bookstore.Areas.Admin.Controllers
    {
        [Area("Admin")]
        // [Authorize(Roles = "Admin")] // Uncomment to restrict access
        public class GenreController : Controller
        {
            private IBookstoreUnitOfWork data { get; set; }
    
            public GenreController(IBookstoreUnitOfWork unit) => data = unit;
    
            [HttpGet]
            public IActionResult Delete(string id)
            {
                var genre = data.Genres.Get(new QueryOptions<Genre>
                {
                    Include = "Books", // Include related books to check for dependencies
                    Where = g => g.GenreId == id
                });
    
                if (genre == null)
                {
                    TempData["message"] = "Genre not found.";
                    return RedirectToAction("Index");
                }
    
                if (genre.Books.Count > 0) // Check if genre has associated books
                {
                    TempData["message"] = $"Can't delete genre '{genre.Name}' because it's associated with these books.";
                    return GoToBookSearchResults(id); // Redirect to book search results
                }
                else
                {
                    return View("Genre", genre); // Display confirmation for deletion
                }
            }
    
            [HttpPost]
            public RedirectToActionResult Delete(Genre genre)
            {
                data.Genres.Delete(genre);
                data.Save();
                TempData["message"] = $"Genre '{genre.Name}' deleted.";
                return RedirectToAction("Index");
            }
    
            // Private helper method to redirect to book search results
            private RedirectToActionResult GoToBookSearchResults(string id)
            {
                // Display search results of all books in this genre
                var search = new SearchData(TempData)
                {
                    SearchTerm = id,
                    Type = "genre" // Set search type to genre
                };
                return RedirectToAction("Search", "Book"); // Redirect to Admin/Book/Search
            }
    
            // Other CRUD actions for genres (Index, Add, Edit)
            // ...
        }
    }
    

Step 7: Authentication and Authorization

  1. Introduce ASP.NET Core Identity:

    • Understand the concepts of authentication, authorization, and the benefits of ASP.NET Identity (Chapter 16, Figures 16-1, 16-2, 16-3).
    • Authentication: Verifies user identity (e.g., username/password).
    • Authorization: Checks if the authenticated user has permission to access a resource.
    • ASP.NET Identity: A membership system for managing users, passwords, roles, etc.
  2. Add Identity to DB Context and Tables:

    • Modify User entity to inherit IdentityUser and add custom fields (e.g., FirstName, LastName).
    • Modify BookstoreContext to inherit IdentityDbContext<User>.
    • Add a migration for Identity tables (AspNetUsers, AspNetRoles, etc.) and update the database (Chapter 16, Figures 16-5, 16-6).
    // Models/User.cs (Updated for Identity)
    using Microsoft.AspNetCore.Identity;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations.Schema; // For NotMapped
    
    namespace Bookstore.Models
    {
        public class User : IdentityUser // Inherit from IdentityUser
        {
            // Add custom fields if needed
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            [NotMapped] // This property is not mapped to the database
            public IList<string> RoleNames { get; set; } // For displaying user roles
        }
    }
    
    // Models/BookstoreContext.cs (Updated for Identity)
    // ... (using statements)
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore; // For IdentityDbContext
    
    namespace Bookstore.Models
    {
        public class BookstoreContext : IdentityDbContext<User> // Inherit IdentityDbContext with your User class
        {
            // ... (constructor and DbSet properties)
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder); // IMPORTANT: Call the base method for Identity tables
                // ... (other entity configurations and seed data)
            }
        }
    }
    
  3. Configure Middleware for Identity:

    • In Startup.cs, add AddIdentity service configuration (including password options) and UseAuthentication()/UseAuthorization() middleware in the correct order (Chapter 16, Figure 16-7).
    // Startup.cs - ConfigureServices method (partial)
    public void ConfigureServices(IServiceCollection services)
    {
        // ... (other services like AddDbContext, AddSession, AddMemoryCache)
    
        // Configure Identity services
        services.AddIdentity<User, IdentityRole>(options =>
        {
            // Password settings (example: relaxed for development)
            options.Password.RequiredLength = 6;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireDigit = false;
            options.Password.RequireUppercase = true;
            options.Password.RequireLowercase = true;
        })
        .AddEntityFrameworkStores<BookstoreContext>() // Use your DbContext for Identity
        .AddDefaultTokenProviders(); // For password reset tokens, etc.
    
        services.AddControllersWithViews()
                .AddNewtonsoftJson(); // If using Newtonsoft.Json for session/cookies
    }
    
    // Startup.cs - Configure method (partial)
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ... (developer exception page, static files, etc.)
    
        app.UseRouting();
    
        app.UseAuthentication(); // Must be before UseAuthorization
        app.UseAuthorization();  // Must be after UseAuthentication
    
        app.UseSession(); // If using session state
    
        app.UseEndpoints(endpoints =>
        {
            // ... (your routing configurations, including Admin area route)
        });
    
        // Seed admin user and role (see next step for implementation)
        // BookstoreContext.CreateAdminUser(app.ApplicationServices).Wait();
    }
    
  4. Implement User Registration and Login/Logout:

    • Create RegisterViewModel and LoginViewModel with validation attributes.
    • Implement AccountController with Register() (GET/POST), LogIn() (GET/POST), and LogOut() (POST) action methods. Use UserManager and SignInManager for user management and authentication.
    • Update the layout to display login/logout buttons and user info based on authentication status (Chapter 16, Figures 16-8, 16-10, 16-11, 16-12, 16-13, 16-14, 16-15).
    // Models/ViewModels/RegisterViewModel.cs
    using System.ComponentModel.DataAnnotations;
    
    namespace Bookstore.Models.ViewModels
    {
        public class RegisterViewModel
        {
            [Required(ErrorMessage = "Please enter a username.")]
            [StringLength(255)]
            public string Username { get; set; }
    
            // Custom fields for registration (added in Chapter 16, Figure 16-24)
            [Required(ErrorMessage = "Please enter a first name.")]
            [StringLength(255)]
            public string FirstName { get; set; }
    
            [Required(ErrorMessage = "Please enter a last name.")]
            [StringLength(255)]
            public string LastName { get; set; }
    
            [Required(ErrorMessage = "Please enter an email address.")]
            [DataType(DataType.EmailAddress)]
            public string Email { get; set; }
    
            [Required(ErrorMessage = "Please enter a password.")]
            [DataType(DataType.Password)]
            [Compare("ConfirmPassword")] // Compares with ConfirmPassword property
            public string Password { get; set; }
    
            [Required(ErrorMessage = "Please confirm your password.")]
            [DataType(DataType.Password)]
            [Display(Name = "Confirm Password")] // Display name for validation messages
            public string ConfirmPassword { get; set; }
        }
    }
    
    // Models/ViewModels/LoginViewModel.cs
    using System.ComponentModel.DataAnnotations;
    
    namespace Bookstore.Models.ViewModels
    {
        public class LoginViewModel
        {
            [Required(ErrorMessage = "Please enter a username.")]
            [StringLength(255)]
            public string Username { get; set; }
    
            [Required(ErrorMessage = "Please enter a password.")]
            [StringLength(255)]
            public string Password { get; set; }
    
            public string ReturnUrl { get; set; } // For redirecting after login
            public bool RememberMe { get; set; } // For persistent cookie
        }
    }
    
    // Controllers/AccountController.cs (Simplified)
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Identity; // For UserManager, SignInManager
    using Bookstore.Models; // Your User class
    using Bookstore.Models.ViewModels;
    
    namespace Bookstore.Controllers
    {
        public class AccountController : Controller
        {
            private UserManager<User> userManager;
            private SignInManager<User> signInManager;
            private RoleManager<IdentityRole> roleManager; // For role management
    
            // Constructor with dependency injection for Identity managers
            public AccountController(UserManager<User> userMngr, SignInManager<User> signInMngr, RoleManager<IdentityRole> roleMngr)
            {
                userManager = userMngr;
                signInManager = signInMngr;
                roleManager = roleMngr;
            }
    
            // GET: /Account/Register
            [HttpGet]
            public IActionResult Register()
            {
                return View();
            }
    
            // POST: /Account/Register
            [HttpPost]
            public async Task<IActionResult> Register(RegisterViewModel model)
            {
                if (ModelState.IsValid)
                {
                    // Create a new User object
                    var user = new User {
                        UserName = model.Username,
                        FirstName = model.FirstName, // Custom field
                        LastName = model.LastName,   // Custom field
                        Email = model.Email          // IdentityUser field
                    };
    
                    // Create the user in the Identity system
                    var result = await userManager.CreateAsync(user, model.Password);
    
                    if (result.Succeeded)
                    {
                        // Sign in the newly registered user
                        await signInManager.SignInAsync(user, isPersistent: false);
                        return RedirectToAction("Index", "Home"); // Redirect to home on success
                    }
                    else
                    {
                        // Add Identity errors to ModelState
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError("", error.Description);
                        }
                    }
                }
                return View(model); // Return to view with validation errors
            }
    
            // GET: /Account/Login
            [HttpGet]
            public IActionResult LogIn(string returnUrl = "")
            {
                var model = new LoginViewModel { ReturnUrl = returnUrl };
                return View(model);
            }
    
            // POST: /Account/Login
            [HttpPost]
            public async Task<IActionResult> LogIn(LoginViewModel model)
            {
                if (ModelState.IsValid)
                {
                    // Attempt to sign in the user
                    var result = await signInManager.PasswordSignInAsync(
                        model.Username, model.Password,
                        isPersistent: model.RememberMe, // Use persistent cookie if "Remember Me" is checked
                        lockoutOnFailure: false); // Don't lock out on failure (for simplicity)
    
                    if (result.Succeeded)
                    {
                        // Redirect to ReturnUrl if it's a local URL, otherwise to Home
                        if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
                        {
                            return Redirect(model.ReturnUrl);
                        }
                        else
                        {
                            return RedirectToAction("Index", "Home");
                        }
                    }
                }
                ModelState.AddModelError("", "Invalid username/password."); // Generic error for invalid credentials
                return View(model); // Return to view with error
            }
    
            // POST: /Account/Logout
            [HttpPost]
            public async Task<IActionResult> LogOut()
            {
                await signInManager.SignOutAsync(); // Sign out the current user
                return RedirectToAction("Index", "Home"); // Redirect to home after logout
            }
    
            // GET: /Account/AccessDenied
            [HttpGet]
            public ViewResult AccessDenied()
            {
                return View(); // Displays a simple access denied page
            }
        }
    }
    
  5. Implement User and Role Management:

    • Create UserViewModel (for displaying users and roles).
    • Implement Admin/UserController with Index() (display users/roles), Add() (add user), Delete() (delete user), AddToAdmin() (add user to Admin role), RemoveFromAdmin() (remove user from Admin role), CreateAdminRole() (create Admin role), and DeleteRole() (delete role) actions.
    • Use UserManager and RoleManager to manage users and roles.
    • Restrict access to AdminControllers using [Authorize(Roles = "Admin")] (Chapter 16, Figures 16-16, 16-17, 16-18, 16-19, 16-20, 16-21).
    • Seed an initial Admin user and role in BookstoreContext (Chapter 16, Figure 16-22).
    // Models/ViewModels/UserViewModel.cs
    using System.Collections.Generic;
    using Microsoft.AspNetCore.Identity; // For IdentityRole
    
    namespace Bookstore.Models.ViewModels
    {
        public class UserViewModel
        {
            public IEnumerable<User> Users { get; set; } // Collection of users
            public IEnumerable<IdentityRole> Roles { get; set; } // Collection of Identity roles
        }
    }
    
    // Areas/Admin/Controllers/UserController.cs (Simplified)
    using System.Threading.Tasks;
    using System.Collections.Generic;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Authorization; // For Authorize attribute
    using Microsoft.AspNetCore.Identity; // For UserManager, RoleManager
    using Bookstore.Models; // Your User class
    using Bookstore.Models.ViewModels;
    
    namespace Bookstore.Areas.Admin.Controllers
    {
        [Area("Admin")]
        [Authorize(Roles = "Admin")] // Restrict access to users in "Admin" role
        public class UserController : Controller
        {
            private UserManager<User> userManager;
            private RoleManager<IdentityRole> roleManager;
    
            public UserController(UserManager<User> userMngr, RoleManager<IdentityRole> roleMngr)
            {
                userManager = userMngr;
                roleManager = roleMngr;
            }
    
            // GET: /Admin/User/Index - Displays list of users and roles
            public async Task<IActionResult> Index()
            {
                List<User> users = new List<User>();
                foreach (User user in userManager.Users)
                {
                    // Get roles for each user
                    user.RoleNames = await userManager.GetRolesAsync(user);
                    users.Add(user);
                }
    
                UserViewModel model = new UserViewModel
                {
                    Users = users,
                    Roles = roleManager.Roles // All available roles
                };
                return View(model);
            }
    
            // POST: /Admin/User/Delete - Deletes a user
            [HttpPost]
            public async Task<IActionResult> Delete(string id)
            {
                User user = await userManager.FindByIdAsync(id);
                if (user != null)
                {
                    IdentityResult result = await userManager.DeleteAsync(user);
                    if (!result.Succeeded)
                    {
                        // Handle errors if deletion fails
                        string errorMessage = "";
                        foreach (IdentityError error in result.Errors)
                        {
                            errorMessage += error.Description + " | ";
                        }
                        TempData["message"] = errorMessage;
                    }
                }
                return RedirectToAction("Index");
            }
    
            // POST: /Admin/User/AddToAdmin - Adds user to "Admin" role
            [HttpPost]
            public async Task<IActionResult> AddToAdmin(string id)
            {
                IdentityRole adminRole = await roleManager.FindByNameAsync("Admin");
                if (adminRole == null)
                {
                    TempData["message"] = "Admin role does not exist. Click 'Create Admin Role' button to create it.";
                }
                else
                {
                    User user = await userManager.FindByIdAsync(id);
                    await userManager.AddToRoleAsync(user, adminRole.Name);
                }
                return RedirectToAction("Index");
            }
    
            // POST: /Admin/User/RemoveFromAdmin - Removes user from "Admin" role
            [HttpPost]
            public async Task<IActionResult> RemoveFromAdmin(string id)
            {
                User user = await userManager.FindByIdAsync(id);
                await userManager.RemoveFromRoleAsync(user, "Admin");
                return RedirectToAction("Index");
            }
    
            // POST: /Admin/User/CreateAdminRole - Creates "Admin" role
            [HttpPost]
            public async Task<IActionResult> CreateAdminRole()
            {
                await roleManager.CreateAsync(new IdentityRole("Admin"));
                return RedirectToAction("Index");
            }
    
            // POST: /Admin/User/DeleteRole - Deletes a role
            [HttpPost]
            public async Task<IActionResult> DeleteRole(string id)
            {
                IdentityRole role = await roleManager.FindByIdAsync(id);
                await roleManager.DeleteAsync(role);
                return RedirectToAction("Index");
            }
    
            // Other actions like Add, Edit, ChangePassword (similar to AccountController)
            // ...
        }
    }
    
    // Views/Admin/User/Index.cshtml (Simplified)
    @model Bookstore.Models.ViewModels.UserViewModel
    @{
        ViewData["Title"] = "Manage Users";
    }
    
    <h1>Manage Users</h1>
    
    <h5 class="mt-2"><a asp-action="Add">Add a User</a></h5>
    
    <table class="table table-bordered table-striped table-sm">
        <thead class="thead-dark">
            <tr><th>Username</th><th>Roles</th><th></th><th></th><th></th></tr>
        </thead>
        <tbody>
            @if (Model.Users.Count() == 0)
            {
                <tr><td colspan="5">There are no user accounts.</td></tr>
            }
            else
            {
                @foreach (User user in Model.Users)
                {
                    <tr>
                        <td>@user.UserName</td>
                        <td>
                            @foreach (string roleName in user.RoleNames)
                            {
                                <div>@roleName</div>
                            }
                        </td>
                        <td>
                            <form method="post" asp-action="Delete" asp-route-id="@user.Id">
                                <button type="submit" class="btn btn-danger btn-sm">Delete User</button>
                            </form>
                        </td>
                        <td>
                            @if (!user.RoleNames.Contains("Admin")) // Only show if not already Admin
                            {
                                <form method="post" asp-action="AddToAdmin" asp-route-id="@user.Id">
                                    <button type="submit" class="btn btn-primary btn-sm">Add To Admin</button>
                                </form>
                            }
                        </td>
                        <td>
                            @if (user.RoleNames.Contains("Admin")) // Only show if user is Admin
                            {
                                <form method="post" asp-action="RemoveFromAdmin" asp-route-id="@user.Id">
                                    <button type="submit" class="btn btn-warning btn-sm">Remove From Admin</button>
                                </form>
                            }
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    
        <h1 class="mb-2 mt-4">Manage Roles</h1>
        @if (Model.Roles.Count() == 0)
        {
            <form method="post" asp-action="CreateAdminRole">
                <button type="submit" class="btn btn-primary">Create Admin Role</button>
            </form>
        }
        else
        {
            <table class="table table-bordered table-striped table-sm">
                <thead class="thead-dark">
                    <tr><th>Role</th><th></th></tr>
                </thead>
                <tbody>
                    @foreach (var role in Model.Roles)
                    {
                        <tr>
                            <td>@role.Name</td>
                            <td>
                                <form method="post" asp-action="DeleteRole" asp-route-id="@role.Id">
                                    <button type="submit" class="btn btn-danger btn-sm">Delete Role</button>
                                </form>
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    
    // Models/BookstoreContext.cs - CreateAdminUser method (for seeding)
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using System.Threading.Tasks; // For async/await
    
    namespace Bookstore.Models
    {
        public partial class BookstoreContext : IdentityDbContext<User>
        {
            // ... (constructor, DbSets, OnModelCreating)
    
            // Static method to seed admin user and role
            public static async Task CreateAdminUser(IServiceProvider serviceProvider)
            {
                UserManager<User> userManager =
                    serviceProvider.GetRequiredService<UserManager<User>>();
                RoleManager<IdentityRole> roleManager =
                    serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    
                string username = "admin";
                string password = "Sesame"; // Use a strong password in production!
                string roleName = "Admin";
    
                // If role doesn't exist, create it
                if (await roleManager.FindByNameAsync(roleName) == null)
                {
                    await roleManager.CreateAsync(new IdentityRole(roleName));
                }
    
                // If username doesn't exist, create user and add to role
                if (await userManager.FindByNameAsync(username) == null)
                {
                    User user = new User { UserName = username, Email = "admin@example.com" }; // Add email
                    var result = await userManager.CreateAsync(user, password);
                    if (result.Succeeded)
                    {
                        await userManager.AddToRoleAsync(user, roleName);
                    }
                }
            }
        }
    }
    
    // Program.cs (Updated to allow ValidateScopes = false for seeding)
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.DependencyInjection; // For CreateDefaultBuilder
    
    namespace Bookstore
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>()
                            // This is needed to allow CreateAdminUser to run during startup
                            // In production, you might want to remove this or use a different seeding strategy.
                            .UseDefaultServiceProvider(options => options.ValidateScopes = false);
                    });
        }
    }
    

Step 8: Advanced UI/Code Organization (Tag Helpers, Partial Views, View Components)

  1. Custom Tag Helpers:

    • Create custom tag helpers (e.g., ButtonTagHelper, SubmitButtonTagHelper, ActiveNavbarTagHelper, TempMessageTagHelper, PagingLinkTagHelper, SortingLinkTagHelper) to encapsulate common HTML patterns and logic.
    • Learn how to define properties, control scope (HtmlTargetElement), and generate HTML dynamically (TagBuilder, TagHelperOutput) (Chapter 15, Figures 15-3, 15-4, 15-5, 15-6, 15-7, 15-8, 15-9, 15-10, 15-11, 15-12).
    • Use dependency injection within tag helpers (Chapter 15, Figure 15-10).
    // TagHelpers/ButtonTagHelper.cs
    using Microsoft.AspNetCore.Razor.TagHelpers;
    
    namespace Bookstore.TagHelpers
    {
        // Targets any element with type="submit" or <a> with my-button attribute
        [HtmlTargetElement(Attributes = "type=submit")]
        [HtmlTargetElement("a", Attributes = "my-button")]
        public class ButtonTagHelper : TagHelper
        {
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                // Appends Bootstrap button classes to existing classes
                output.Attributes.AppendCssClass("btn btn-primary");
            }
        }
    }
    
    // TagHelpers/ActiveNavbarTagHelper.cs
    using Microsoft.AspNetCore.Mvc.Rendering; // For ViewContext
    using Microsoft.AspNetCore.Mvc.ViewFeatures; // For ViewContextAttribute, HtmlAttributeNotBoundAttribute
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Bookstore.Models.ExtensionMethods; // For EqualsNoCase
    
    namespace Bookstore.TagHelpers
    {
        // Targets <a> elements with class="nav-link" within an <li> parent
        [HtmlTargetElement("a", Attributes = "[class=nav-link]", ParentTag = "li")]
        public class ActiveNavbarTagHelper : TagHelper
        {
            [ViewContext] // Injects ViewContext
            [HtmlAttributeNotBound] // Prevents mapping to HTML attribute
            public ViewContext ViewCtx { get; set; }
    
            [HtmlAttributeName("my-mark-area-active")] // Custom attribute for area-only matching
            public bool IsAreaOnly { get; set; }
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                string currentArea = ViewCtx.RouteData.Values["area"]?.ToString() ?? "";
                string currentController = ViewCtx.RouteData.Values["controller"].ToString();
    
                string aspArea = context.AllAttributes["asp-area"]?.Value?.ToString() ?? "";
                string aspController = context.AllAttributes["asp-controller"].Value.ToString();
    
                // Mark active if both area and controller match
                if (currentArea.EqualsNoCase(aspArea) && currentController.EqualsNoCase(aspController))
                {
                    output.Attributes.AppendCssClass("active");
                }
                // Mark active if only area matches and IsAreaOnly is true
                else if (IsAreaOnly && currentArea.EqualsNoCase(aspArea))
                {
                    output.Attributes.AppendCssClass("active");
                }
            }
        }
    }
    
    // TagHelpers/TempMessageTagHelper.cs
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Bookstore.Models.ExtensionMethods; // For BuildTag, SetContent
    
    namespace Bookstore.TagHelpers
    {
        [HtmlTargetElement("my-temp-message")] // Custom element
        public class TempMessageTagHelper : TagHelper
        {
            [ViewContext]
            [HtmlAttributeNotBound]
            public ViewContext ViewCtx { get; set; }
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                var tempData = ViewCtx.TempData; // Access TempData from ViewContext
    
                if (tempData.ContainsKey("message")) // Check if message exists
                {
                    // Build an <h4> element with Bootstrap classes
                    output.BuildTag("h4", "bg-info text-center text-white p-2");
                    output.Content.SetContent(tempData["message"].ToString()); // Set content to message
                }
                else
                {
                    output.SuppressOutput(); // Suppress output if no message
                }
            }
        }
    }
    
    // TagHelpers/PagingLinkTagHelper.cs
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Microsoft.AspNetCore.Routing; // For LinkGenerator
    using Bookstore.Models; // For RouteDictionary
    using Bookstore.Models.ExtensionMethods; // For BuildLink, SetContent
    
    namespace Bookstore.TagHelpers
    {
        [HtmlTargetElement("my-paging-link")] // Custom element for paging links
        public class PagingLinkTagHelper : TagHelper
        {
            private LinkGenerator linkBuilder; // Injected LinkGenerator
    
            public PagingLinkTagHelper(LinkGenerator lg) => linkBuilder = lg;
    
            [ViewContext]
            [HtmlAttributeNotBound]
            public ViewContext ViewCtx { get; set; }
    
            public int Number { get; set; } // Page number for this link
            public RouteDictionary Current { get; set; } // Current route parameters
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                // Clone current route and update page number for this link
                var routes = Current.Clone();
                routes.PageNumber = Number;
    
                // Get controller and action from current route data
                string controller = ViewCtx.RouteData.Values["controller"].ToString();
                string action = ViewCtx.RouteData.Values["action"].ToString();
    
                // Generate URL using LinkGenerator
                string url = linkBuilder.GetPathByAction(action, controller, routes);
    
                // Build CSS classes for the link
                string linkClasses = "btn btn-outline-primary";
                if (Number == Current.PageNumber) // Mark active if it's the current page
                    linkClasses += " active";
    
                // Build the <a> tag
                output.BuildLink(url, linkClasses);
                output.Content.SetContent(Number.ToString()); // Set link text to page number
            }
        }
    }
    
    // TagHelpers/SortingLinkTagHelper.cs (Similar to PagingLinkTagHelper)
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    using Microsoft.AspNetCore.Routing;
    using Bookstore.Models;
    using Bookstore.Models.ExtensionMethods;
    
    namespace Bookstore.TagHelpers
    {
        [HtmlTargetElement("my-sorting-link")] // Custom element for sorting links
        public class SortingLinkTagHelper : TagHelper
        {
            private LinkGenerator linkBuilder;
    
            public SortingLinkTagHelper(LinkGenerator lg) => linkBuilder = lg;
    
            [ViewContext]
            [HtmlAttributeNotBound]
            public ViewContext ViewCtx { get; set; }
    
            public string SortField { get; set; } // Field to sort by for this link
            public RouteDictionary Current { get; set; } // Current route parameters
    
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                // Clone current route and set sort field/direction for this link
                var routes = Current.Clone();
                routes.SetSortAndDirection(SortField, Current); // Handles toggling asc/desc
    
                string controller = ViewCtx.RouteData.Values["controller"].ToString();
                string action = ViewCtx.RouteData.Values["action"].ToString();
    
                string url = linkBuilder.GetPathByAction(action, controller, routes);
    
                string linkClasses = "text-white"; // Default text color for header links
                // Add active class if this is the currently sorted field
                if (SortField.EqualsNoCase(Current.SortField))
                    linkClasses += " active";
    
                output.BuildLink(url, linkClasses);
                // Content is usually set by the inner HTML of the tag helper (e.g., "Title")
            }
        }
    }
    
    // TagHelpers/TagHelperExtensions.cs (Helper methods for Tag Helpers)
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Razor.TagHelpers;
    
    namespace Bookstore.TagHelpers
    {
        public static class TagHelperExtensions
        {
            // Appends CSS classes to an existing class attribute
            public static void AppendCssClass(this TagHelperAttributeList list, string newCssClasses)
            {
                string oldCssClasses = list["class"]?.Value?.ToString();
                string cssClasses = (string.IsNullOrEmpty(oldCssClasses)) ?
                                    newCssClasses : $"{oldCssClasses} {newCssClasses}";
                list.SetAttribute("class", cssClasses);
            }
    
            // Builds a new HTML tag with specified name and classes
            public static void BuildTag(this TagHelperOutput output, string tagName, string classNames)
            {
                output.TagName = tagName;
                output.TagMode = TagMode.StartTagAndEndTag;
                output.Attributes.AppendCssClass(classNames);
            }
    
            // Builds an <a> tag with specified URL and classes
            public static void BuildLink(this TagHelperOutput output, string url, string className)
            {
                output.BuildTag("a", className);
                output.Attributes.SetAttribute("href", url);
            }
        }
    }
    
  2. Partial Views:

    • Create partial views (e.g., _NavbarMenuButtonPartial, _BookLinkPartial, _AuthorLinkPartial, _PagingLinksPartial) for reusable HTML snippets.
    • Use the <partial> tag helper to render them in main views or layouts, passing models or ViewData as needed (Chapter 15, Figures 15-13, 15-14).
    // Views/Shared/_NavbarMenuButtonPartial.cshtml
    <button class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#menu" aria-controls="menu" aria-expanded="false"
            aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    
    // Views/Shared/_BookLinkPartial.cshtml
    @model Bookstore.Models.Book
    <a asp-action="Details" asp-controller="Book"
       asp-route-id="@Model.BookId"
       asp-route-slug="@Model.Title.Slug()">
        @Model.Title
    </a>
    
    // Views/Shared/_AuthorLinkPartial.cshtml
    @model Bookstore.Models.Author
    <a asp-action="Details" asp-controller="Author"
       asp-route-id="@Model.AuthorId"
       asp-route-slug="@Model.FullName.Slug()">
        @Model.FullName
    </a>
    
    // Views/Shared/_PagingLinksPartial.cshtml
    @model Bookstore.Models.ViewModels.GridViewModel<dynamic> // Use dynamic or specific type
    @{
        RouteDictionary routes = Model.CurrentRoute.Clone();
    }
    <div class="d-flex justify-content-center mt-3">
        @for (int i = 1; i <= Model.TotalPages; i++)
        {
            <my-paging-link number="@i" current="@routes" />
        }
    </div>
    
  3. View Components:

    • Create view components (e.g., CartBadge, AuthorDropDown, GenreDropDown, PriceDropDown) for complex, reusable UI logic that might require data access.
    • Implement Invoke() methods in view component classes to prepare data and return a partial view.
    • Use dependency injection in view components to get required services (e.g., ICart, IRepository<T>).
    • Render view components using <vc:component-name> syntax (Chapter 15, Figures 15-15, 15-16, 15-17).
    • Simplify controllers and view models by offloading data preparation for UI elements to view components (Chapter 15, Figure 15-17).
    // Components/CartBadge.cs
    using Microsoft.AspNetCore.Mvc;
    using Bookstore.Models; // For Cart
    
    namespace Bookstore.Components
    {
        // View component for displaying cart item count in a badge
        public class CartBadge : ViewComponent
        {
            private ICart cart { get; set; } // Injected Cart service
    
            public CartBadge(ICart c) => cart = c;
    
            public IViewComponentResult Invoke()
            {
                // Pass the cart item count to the Default.cshtml partial view
                return View(cart.Count);
            }
        }
    }
    
    // Views/Shared/Components/CartBadge/Default.cshtml
    @model int? // Model is the integer count of cart items
    <span class="badge badge-light">@Model</span>
    
    // Components/AuthorDropDown.cs
    using Microsoft.AspNetCore.Mvc;
    using System.Linq; // For ToDictionary
    using Bookstore.Models; // For Author, IRepository
    using Bookstore.Models.ViewModels; // For DropDownViewModel
    
    namespace Bookstore.Components
    {
        // View component for author filter dropdown
        public class AuthorDropDown : ViewComponent
        {
            private IRepository<Author> data { get; set; } // Injected Author repository
    
            public AuthorDropDown(IRepository<Author> rep) => data = rep;
    
            public IViewComponentResult Invoke(string selectedValue) // selectedValue from tag helper attribute
            {
                var authors = data.List(new QueryOptions<Author> { OrderBy = a => a.FirstName });
    
                var viewModel = new DropDownViewModel
                {
                    SelectedValue = selectedValue,
                    DefaultValue = "all", // Default filter value
                    Items = authors.ToDictionary(a => a.AuthorId.ToString(), a => a.FullName)
                };
                // Use a shared partial view for all dropdowns
                return View("~/Views/Shared/Components/Common/DropDown.cshtml", viewModel);
            }
        }
    }
    
    // Models/ViewModels/DropDownViewModel.cs (Shared for all dropdowns)
    using System.Collections.Generic;
    
    namespace Bookstore.Models.ViewModels
    {
        public class DropDownViewModel
        {
            public Dictionary<string, string> Items { get; set; } // Key-value pairs for dropdown options
            public string SelectedValue { get; set; } // Currently selected value
            public string DefaultValue { get; set; } // Value for "All" option
        }
    }
    
    // Views/Shared/Components/Common/DropDown.cshtml (Shared partial view for dropdowns)
    @model Bookstore.Models.ViewModels.DropDownViewModel
    <select name="filter" class="form-control m-2"
            asp-items="@(new SelectList(Model.Items, "Key", "Value", Model.SelectedValue))">
        <option value="@Model.DefaultValue">All</option>
    </select>
    
  4. Integrate Advanced UI Elements into Bookstore App:

    • Update _Layout.cshtml and Book/List.cshtml to utilize the newly created custom tag helpers, partial views, and view components for a cleaner, more modular codebase (Chapter 15, Figures 15-18, 15-19, 15-20, 15-21).
    • (See Views/Shared/_Layout.cshtml and Views/Book/List.cshtml in previous steps for integration examples).

Conclusion

Building the Bookstore project step-by-step as outlined in "Murach's ASP.NET Core MVC" provides a comprehensive understanding of ASP.NET Core MVC development. By following these steps, you'll gain practical experience with fundamental concepts like MVC patterns, EF Core, data persistence, and validation, as well as advanced topics such as dependency injection, unit testing, authentication, and UI componentization.

Remember that each step builds upon the previous one, and referring back to the specific figures and code examples in the book is crucial for successful implementation.

Comments

Popular posts from this blog

Master Asp.Net Core Middleware concepts.

ASP.net Interview P1