avatarM. Ramadhan

Summary

This article discusses implementing checkbox lists in a CRUD operation involving an M:N (many-to-many) relationship in a Blazor Server project.

Abstract

The article focuses on creating a checkbox list for authors in a CRUD operation involving an M:N (many-to-many) relationship in a Blazor Server project. It provides a detailed walkthrough of the process, starting with creating the BookDB database and its tables, followed by creating the Blazor Server project and its files. The article then explains the steps to create the checkbox list, including adding a new book, editing an existing book, and handling the checkbox list. The article also provides the SQL script for creating the database and its tables, as well as the code for the Blazor Server project files.

Opinions

  • The author emphasizes the importance of understanding the M:N relationship and how it affects the CRUD operation.
  • The author suggests modifying the previous project code to make the page component displays tidier and the method names more consistent.
  • The author provides a detailed walkthrough of the process, making it easy for readers to follow along and implement the checkbox list in their own projects.
  • The author uses clear and concise language, making the article easy to understand even for those who may not be familiar with Blazor Server projects.
  • The author provides the SQL script and code for the Blazor Server project files, making it easy for readers to replicate the process in their own projects.
  • The author highlights the importance of handling the checkbox list properly to ensure that the CRUD operation is successful.
  • The author provides a visual representation of the checkbox list, making it easier for readers to understand how it should look and function.

Blazor Server Project #3: How to Implement a Checkbox List

On CRUD operation involves an M:N (many-to-many) relationship using Dapper with SQL Server database

This article is the third in a series covering the Blazor Server Project: (1) How to create a CRUD operation using Dapper (2) Building a dropdown list involves a 1:N relationship (3) How to implement a checkbox list involving an M:N relationships (4) Understanding URL routing and navigation (5) Creating and using page layout (6) How to create a reusable modal dialog component (7) Practical guide to making a master-detail page (8) Master-detail page using dynamic query (9) How to avoid SQL injection attacks (10) Hiding/showing HTML elements (11) Migrate to ASP.NET Core 6.0 (12) Installing ASP.NET Core Identity (13) Integrating Identity tables into the existing project database (14) Authentication and authorization (15) Role-based authorization (16) How to implement Google Authentication (17) Facebook Authentication and Authorization (18) How to configure Twitter Authentication (19) How to Customize Password Policy (20) Account Lockout Policy

These articles are for anyone who wants to learn how to build Blazor Server applications in a practical approach through some sample projects. It will be straightforward if you have some experience with C#, HTML, CSS, and SQL.

This article focuses on implementing checkbox lists on CRUD operation that involves an M:N (many-to-many) Author-write-Book relationship, as shown in the following ER diagram.

The Author-write-Book relationship generates three tables, namely Book, BookAuthor, and Author.

I modified almost all of the previous project code, Blazor Server Project #1 and Blazor Server Project #2, so that the page component displays are much tidier, and the method names are more consistent.

Overview

List of Books

Below is a screenshot of the book list involving the M:N relationship (Author-write-Book) and 1:N relationship (Publisher-publish-book). For the 1:N relationship, please read the previous article here.

  • The Author author in the third column shows M:N relationship. An author may write many books; for example, David M. Kroenke wrote two books. Instead, one book may be written by many authors; for example, Database Processing was written by two authors.
  • The list is sorted descending by Purchase Date. By clicking on the Purchase Date column heading, the order changes from descending to ascending. The same applies to other columns.
  • Navigation button: go to the previous page go to the next page 1, 2, 3, ... page number
  • To search books base on title criterium, type a title in the search box. The title does not have to complete.
  • To add new book data, click Add new book
  • To change existing data, click Edit
  • To delete, click Delete

Adding Book Data

Below is an illustration page for adding book data. There is no data input for the author. Authors’ data can be input after the book is saved.

  • Click Save, the checkbox list of authors appears.
  • Enter the author order and click the checkbox as shown in the figure above.
  • Three authors wrote the book. The third author isn’t on the list yet, so click the Add author button.
  • Click Save, the new author appears on the list.
  • As shown in the figure above, enter the third author order and click the checkbox.

Editing Book Data

Here is an example page for editing a book’s data. You can edit any field including the ISBN primary key. There is a checkbox list for editing the author of the book.

Creating the BookDB Database

First of all, we need to write and execute SQL scripts for creating the BookDB database, tables, data, stored procedures, and functions.

  • Open SQL Server Management Studio.
  • Copy the scripts below, paste it into the query editor.
CREATE DATABASE BookDB
GO
USE BookDB
GO
CREATE TABLE Publisher (
   [Id]       [Int] IDENTITY(0,1),
   [Name]     [VarChar](20),
   [City]     [VarChar](20),
   [Country]  [VarChar](20),
   CONSTRAINT [pkPublisher] PRIMARY KEY ([Id])
)
GO
CREATE TABLE Author(
   [Id]    [Int] IDENTITY(1,1),
   [FName] [VarChar](20),
   [LName] [VarChar](20),
   [Phone] [VarChar](16),
   CONSTRAINT [pkAuthor] PRIMARY KEY ([Id])
)
GO
CREATE TABLE Book(
   [ISBN]      [BigInt],
   [Title]     [VarChar](80),
   [PubYear]   [SmallInt],
   [PurchDate] [Date],
   [PubId]     [Int] DEFAULT 0,
   CONSTRAINT  [pkBook] PRIMARY KEY ([ISBN]),
   CONSTRAINT  [fkBookPub] FOREIGN KEY ([PubId])
   REFERENCES  [dbo].[Publisher]([Id])
   ON UPDATE CASCADE ON DELETE SET DEFAULT
)
GO
CREATE TABLE BookAuthor(
   [ISBN]      [BigInt],
   [AuthorId]  [Int],
   [AuthorOrd] [TinyInt],
   CONSTRAINT  [pkBookAuthor] PRIMARY KEY ([ISBN],[AuthorId]),
   CONSTRAINT  [fkBookAuthor_Book] FOREIGN KEY([ISBN])
   REFERENCES  [dbo].[Book] ([ISBN]) 
   ON UPDATE CASCADE ON DELETE CASCADE,
   CONSTRAINT  [fkBookAuthor_Author] FOREIGN KEY ([AuthorId])
   REFERENCES  [dbo].[Author] ([Id])
   ON UPDATE CASCADE ON DELETE CASCADE
)
GO
INSERT INTO Publisher ([Name], [City], [Country]) 
VALUES ('Unknown', NULL, NULL),
       ('Unsri Press', 'Palembang', 'Indonesia'),
       ('Gramedia', 'Jakarta', 'Indonesia'),
       ('Apress', 'New York', 'USA'),
       ('UIGM Press', 'Palembang', 'Indonesia'),
       ('Packt', 'Birmingham', 'UK'),
       ('Rafah Press', 'Palembang', 'Indonesia'),
       ('O’Reilly','Sebastopol','USA'),
       ('Informatika', 'Bandung', 'Indonesia'),
       ('Pearson', 'London', 'UK'),
       ('Cengage', 'Boston', 'USA'),
       ('Noerfikri', 'Palembang', 'Indonesia'),
       ('Iris Press','Bandung','Indonesia')
GO
INSERT INTO Book (ISBN, PubYear, PurchDate, Title, PubId)
VALUES (9789791339957, 2013, '2019-02-12', 'Pranata Sosial', 6),
       (9780134802749, 2018, '2020-10-14',
        'Database Processing', 9),
       (9786024474348, 2019, '2019-10-01',
        'Desain Basis Data Akademik Perguruan Tinggi', 11),
       (9781305576766, 2015, '2018-12-31',
        'NoSQL Web Development with Apache Cassandra', 10),
       (9780135191767, 2020, '2020-06-30',
        'Using MIS',9),
       (9781789619768, 2020, '2020-08-31',
        'Modern Web Development with ASP.NET Core 3', 5),
       (9789793053585, 2009, '2015-03-25',
        'Mengenal Pola Huruf Arab',12),
       (9781492056812, 2020, '2020-09-27', 'Programming C# 8.0',7),
       (9781783989201, 2015, '2017-07-23',
        'Learning Apache Cassandra', 5),
       (9781484259276, 2020, '2020-10-17',
        'Microsoft Blazor: Building Web App in .NET', 3),
       (9786020338682, 2017, '2018-08-10', 'Disruption', 2)
GO
INSERT INTO Author ([FName], [LName], [Phone])
VALUES ('Rhenald', 'Kasali', 'Unknown'),
       ('David M.', 'Kroenke ', 'Unknown'),
       ('M.', 'Ramadhan', '081122334455'),
       ('Randall J.', 'Boyle', 'Unknown'),
       ('Deepak', 'Vohra', 'Unknown'),
       ('Hamidah', 'Akil', '08123456789'),
       ('David','Auer','Unknown'),
       ('Ricardo', 'Peres', 'Unknown'),
       ('Ian', 'Griffiths', 'Unknown'),
       ('Mat', 'Brown', 'Unknown'),
       ('Peter', 'Himschoot', 'Unknown')
GO
INSERT INTO BookAuthor ([ISBN],[AuthorId],[AuthorOrd])
VALUES (9780134802749, 2, 1),(9780134802749, 7, 2),
       (9789791339957, 6, 1),(9781789619768, 8, 1),
       (9781305576766, 5, 1),(9781492056812, 9, 1),
       (9780135191767, 2, 1),(9780135191767, 4, 2),
       (9786020338682, 1, 1),(9789793053585, 3, 1),
       (9786024474348, 3, 1),(9781783989201, 10, 1),
       (9781484259276, 11, 1)
GO
CREATE PROCEDURE dbo.spAddPublisher
                 @Name    VarChar(20), 
                 @City    VarChar(20), 
                 @Country VarChar(20)
       AS
BEGIN    
   DECLARE @Id Int;
   INSERT  INTO Publisher([Name], [City], [Country]) 
   VALUES  (@Name, @City, @Country);
   SET     @Id = SCOPE_IDENTITY();
END
GO
CREATE PROCEDURE dbo.spUpdatePublisher
                 @Id      Int, 
                 @Name    VarChar(20), 
                 @City    VarChar(20), 
                 @Country VarChar(20)
       AS
UPDATE Publisher 
SET [Name]    = @Name, 
    [City]    = @City, 
    [Country] = @Country 
WHERE [Id] = @Id
GO
CREATE PROCEDURE dbo.spAddBook
                 @ISBN      Bigint, 
                 @Title     VarChar(80), 
                 @PubYear   SmallInt,
                 @PurchDate Date,
                 @PubId     Int 
       AS
INSERT INTO dbo.Book (ISBN, Title, PubYear, PurchDate, PubId)
VALUES (@ISBN, @Title, @PubYear, @PurchDate, @PubId)
GO
CREATE PROCEDURE dbo.spUpdateBook
                 @ISBN      Bigint, 
                 @Title     VarChar(80), 
                 @PubYear   SmallInt,
                 @PurchDate Date,
                 @PubId     Int,
                 @Pk        Bigint
       AS
UPDATE Book 
SET [ISBN]      = @ISBN,
    [Title]     = @Title, 
    [PubYear]   = @PubYear,
    [PurchDate] = @PurchDate,
    [PubId]     = @PubId
WHERE [ISBN] = @Pk
GO
CREATE PROCEDURE dbo.spAddAuthor
                 @FName VarChar(20), 
                 @LName VarChar(20), 
                 @Phone VarChar(20)
       AS
BEGIN    
   DECLARE @Id as Int;
   INSERT  INTO Author([FName], [LName], [Phone]) 
   VALUES  (@FName, @LName, @Phone);
   SET     @Id = SCOPE_IDENTITY();  
END
GO
CREATE PROCEDURE dbo.spUpdateAuthor
                 @Id    Int, 
                 @FName VarChar(20), 
                 @LName VarChar(20), 
                 @Phone VarChar(20)
       AS
UPDATE Author 
SET [FName] = @FName, 
    [LName] = @LName, 
    [Phone] = @Phone 
WHERE  [Id] = @Id
GO
CREATE PROCEDURE dbo.spAddBookAuthor
                 @ISBN      BigInt, 
                 @AuthorId  Int, 
                 @AuthorOrd TinyInt
       AS
INSERT INTO [dbo].[BookAuthor]([ISBN],[AuthorId],[AuthorOrd])
VALUES (@ISBN, @AuthorId, @AuthorOrd)
GO
CREATE PROCEDURE dbo.spUpdateBookAuthor
                 @ISBN      BigInt, 
                 @AuthorId  Int, 
                 @AuthorOrd TinyInt
       AS
UPDATE BookAuthor 
SET [AuthorOrd] = @AuthorOrd
WHERE [ISBN] = @ISBN and AuthorId = @AuthorId
GO
CREATE FUNCTION dbo.AuthorOfBook (@ISBN BigInt) RETURNS Table AS
       RETURN 
SELECT ISBN, AuthorId AuId, AuthorOrd
FROM BookAuthor WHERE ISBN=@ISBN
GO
CREATE FUNCTION dbo.BookAuthorName (@ISBN BigInt) Returns Table AS
       RETURN
SELECT ISBN, Id AuthorId, AuthorOrd, FName + ' ' + LName AuthorName
FROM Author LEFT OUTER JOIN dbo.AuthorOfBook (@ISBN) On Id = AuId
  • Select menu: Query| Execute, or press F5 key to execute the script above.
  • Pay attention to the last two functions above. The BookAuthorName function calls the AuthorOfBook function. The AddBook.razor component displays a checkbox list of book authors by calling Task> in BookAuthorService.cs that use the BookAuthorName function. Thus users can select one or more book authors from the list. The same is true for EditBook.razor.

Creating the Blazor Server Project

1. Open Visual Studio 2019, create a Blazor Server project named BookApp. 2. Delete the five files that are not needed. 3. Delete lines of codes in the related deleted files. 4. Create database connection strings. 5. Install Dapper and Microsoft.EntityFrameworkCore.SqlServer package.

For the details, please read the previous article here.

The following is the overall project structure.

Files Supporting CRUD Operation

Besides the SQL script above, CRUD operation need files of entities, interfaces, implementation of interfaces, razor components, and code modification of some existing files.

Entity Files

  • There are six classes in the Entities folder: Author.cs, Book.cs, BookAuthor.cs, Publisher.cs, BookAuthorName.cs, and BookAuPub.cs,
  • The four entity classes — Author.cs, Book.cs, BookAuthor.cs, and Publisher.cs — map the database tables with the same name.
  • The BookAuthorName.cs entity class maps the joining two database tables; Author and BookAuthor.
  • The BookAuPub.cs entity class maps the joining four database tables; Book, Publisher, BookAuthor, and Author.

Below are the code lists.

Author.cs

using System.ComponentModel.DataAnnotations;
namespace BookApp.Entities
{
   public class Author
   {
      [Key]
      public int Id { get; set; }
      public string FName { get; set; }
      public string LName { get; set; }
      public string Phone { get; set; }
   }
}

Book.cs

using System;
using System.ComponentModel.DataAnnotations;
namespace BookApp.Entities
{
   public class Book
   {
      [Key]
      public long? ISBN { get; set; }
      public string Title { get; set; }
      public short? PubYear { get; set; }
      public DateTime PurchDate { get; set; }
      public int PubId { get; set; }
   }
}

BookAuthor.cs

using System.ComponentModel.DataAnnotations;
namespace BookApp.Entities
{
   public class BookAuthor
   {
      [Key]
      public long ISBN { get; set; }
      public int AuthorId { get; set; }
      public byte? AuthorOrd { get; set; }
   }
}

Publisher.cs

using System.ComponentModel.DataAnnotations;
namespace BookApp.Entities
{
   public class Publisher
   {
      [Key]
      public int Id { get; set; }
      public string Name { get; set; }
      public string City { get; set; }
      public string Country { get; set; }
   }
}

BookAuthorName.cs

namespace BookApp.Entities
{
   public class BookAuthorName : BookAuthor
   {
      public string AuthorName { get; set; }
   }
}

BookAuPub.cs

namespace BookApp.Entities
{
   public class BookAuPub : Book
   {
      public string AuthorName { get; set; }
      public string PubName { get; set; }
   }
}

Interface Files

The interface files declare methods. There are five interface files: IAuthorService.cs, IBookService.cs, IBookAuthorService.cs, IDapperService.cs, and IPublisherService.cs. The files exist in the Interfaces folder.

IAuthorService.cs

using BookApp.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BookApp.Interfaces
{
   public interface IAuthorService
   {
      Task<int> Create(Author author);
      Task<Author> ReadByPk(int Id);
      Task<int> Update(Author author);
      Task<int> Delete(int Id);
      Task<int> Count(string search);
      Task<List<Author>> ListAll(int skip,
                                 int take,
                                 string orderBy,
                                 string direction,
                                 string search);
   }
}

IBookService.cs

using BookApp.Entities;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BookApp.Interfaces
{
   public interface IBookService
   {
      Task<long> Create(Book book);
      Task<Book> ReadByPk(long isbn);
      Task<int> Update(Book book, long pk);
      Task<int> Delete(long id);
      Task<int> Count(string search);
      Task<List<BookAuPub>> ListAll(int skip,
                                    int take,
                                    string orderBy,
                                    string direction,
                                    string search);
   }
}

IBookAuthorService.cs

using BookApp.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BookApp.Interfaces
{
   public interface IBookAuthorService
   {
      Task<int> Create(BookAuthorName bookAuthorNamer);
      Task<int> Delete(long isbn, int authorId);
      Task<List<BookAuthorName>> FetchAll(long isbn);
   }
}

IDapperService.cs

using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
namespace BookApp.Interfaces
{
   public interface IDapperService : IDisposable
   {
      DbConnection GetConnection();
      T Get<T>(string sp, DynamicParameters parms, 
          CommandType commandType = CommandType.StoredProcedure);
      List<T> GetAll<T>(string sp, DynamicParameters parms,
          CommandType commandType = CommandType.StoredProcedure);
      int Execute(string sp, DynamicParameters parms,
          CommandType commandType = CommandType.StoredProcedure);
      T Insert<T>(string sp, DynamicParameters parms,
          CommandType commandType = CommandType.StoredProcedure);
      T Update<T>(string sp, DynamicParameters parms,
          CommandType commandType = CommandType.StoredProcedure);
   }
}

IPublisherService.cs

using BookApp.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BookApp.Interfaces
{
   public interface IPublisherService
   {
      Task<int> Create(Publisher publisher);
      Task<Publisher> ReadByPk(int Id);
      Task<int> Update(Publisher publisher);
      Task<int> Delete(int Id);
      Task<int> Count(string search);
      Task<List<Publisher>> FetchAll();
      Task<List<Publisher>> ListAll(int skip,
                                    int take,
                                    string orderBy,
                                    string direction,
                                    string search);
   }
}

Method Files

  • Method files implement the methods declared in the interface files.
  • There are five method files: AuthorService.cs, BookService.cs, BookAuthorService.cs, DapperService.cs, and PublisherService.cs.
  • There is also AppContext.cs file, which contains information and configuration for accessing the database.
  • All six files exist in the Data folder.

AuthorService.cs

  • AuthorService.cs implements the methods declared in the IAuthorService.cs file.
using BookApp.Interfaces;
using BookApp.Entities;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace BookApp.Data
{
   public class AuthorService : IAuthorService
   {
      private readonly IDapperService _dapperService;
      public AuthorService(IDapperService dapperService)
      {
         this._dapperService = dapperService;
      }
      public Task<int> Create(Author author)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("FName", author.FName, DbType.String);
         dbPara.Add("LName", author.LName, DbType.String);
         dbPara.Add("Phone", author.Phone, DbType.String);
         var authorId = Task.FromResult(_dapperService.Insert<int>
             ("[dbo].[spAddAuthor]", dbPara, 
             commandType: CommandType.StoredProcedure));
         return authorId;
      }
      public Task<Author> ReadByPk(int id)
      {
         var author = Task.FromResult(_dapperService.Get<Author>
             ($"select * from [Author] where Id = {id}", null,
             commandType: CommandType.Text));
         return author;
      }
      public Task<int> Update(Author author)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("Id", author.Id);
         dbPara.Add("FName", author.FName, DbType.String);
         dbPara.Add("LName", author.LName, DbType.String);
         dbPara.Add("Phone", author.Phone, DbType.String);
         var updateAuthor = Task.FromResult(_dapperService.
             Update<int>("[dbo].[spUpdateAuthor]", dbPara, 
             commandType: CommandType.StoredProcedure));
         return updateAuthor;
      }
      public Task<int> Delete(int id)
      {
         var deleteAuthor = Task.FromResult(_dapperService.Execute
             ($"Delete [Author] where Id = {id}", null,
             commandType: CommandType.Text));
         return deleteAuthor;
      }
      public Task<int> Count(string search)
      {
         var totAuthor = Task.FromResult(_dapperService.Get<int>
             ($"select COUNT(*) from [Author] WHERE LName like " +
             $"'%{search}%'", null, commandType: CommandType.Text));
         return totAuthor;
      }
      public Task<List<Author>> ListAll (int skip, int take,
             string orderBy, string direction = "DESC", 
             string search = "")
      {
         var authors = Task.FromResult(_dapperService.GetAll<Author>
             ($"SELECT * FROM [Author] WHERE LName like " +
             $"'%{search}%' ORDER BY {orderBy} {direction} " +
             $"OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY; ",
             null, commandType: CommandType.Text));
         return authors;
      }
   }
}

BookService.cs

  • BookService.cs implements the methods declared in the IBookService.cs file.
using BookApp.Interfaces;
using BookApp.Entities;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace BookApp.Data
{
   public class BookService : IBookService
   {
      private readonly IDapperService _dapperService;
      public BookService(IDapperService dapperService)
      {
         this._dapperService = dapperService;
      }
      public Task<long> Create(Book book)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("ISBN", book.ISBN, DbType.Int64);
         dbPara.Add("Title", book.Title, DbType.String);
         dbPara.Add("PubYear", book.PubYear, DbType.Int16);
         dbPara.Add("PurchDate", book.PurchDate, DbType.Date);
         dbPara.Add("PubId", book.PubId, DbType.Int32);
         var bookId = Task.FromResult(_dapperService.
             Insert<long>("[dbo].[spAddBook]", dbPara, 
             commandType: CommandType.StoredProcedure));
         return bookId;
      }
      public Task<Book> ReadByPk(long isbn)
      {
         var book = Task.FromResult(_dapperService.Get<Book>
             ($"select * from [Book] where ISBN = {isbn}",
             null, commandType: CommandType.Text));
         return book;
      }
      public Task<int> Update(Book book, long pk)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("ISBN", book.ISBN, DbType.Int64);
         dbPara.Add("Title", book.Title, DbType.String);
         dbPara.Add("PubYear", book.PubYear, DbType.Int16);
         dbPara.Add("PurchDate", book.PurchDate, DbType.Date);
         dbPara.Add("PubId", book.PubId, DbType.Int32);
         dbPara.Add("Pk", pk, DbType.Int64);
         var updateBook = Task.FromResult(_dapperService.
             Update<int>("[dbo].[spUpdateBook]", dbPara, 
             commandType: CommandType.StoredProcedure));
         return updateBook;
      }
      public Task<int> Delete(long id)
      {
         var deleteBook = Task.FromResult(_dapperService.Execute
             ($"Delete [Book] where ISBN = {id}", null, 
             commandType: CommandType.Text));
         return deleteBook;
      }
      public Task<int> Count(string search)
      {
         var totBook = Task.FromResult(_dapperService.Get<int>
            ($"select COUNT(*) from [Book] " +
            $"WHERE Title like '%{search}%'", null, 
            commandType: CommandType.Text));
         return totBook;
      }
      public Task<List<BookAuPub>> ListAll (int skip,
             int take, string orderBy, string direction = "DESC",
             string search = "")
      {
         var books = Task.FromResult(_dapperService.
         GetAll<BookAuPub>($"SELECT B.*, FName + ' ' + LName " +
         $"AuthorName, P.Name PubName FROM Book B LEFT OUTER JOIN" +
         $" Publisher P ON B.PubId=P.Id LEFT OUTER JOIN " +
         $"BookAuthor BA ON B.ISBN = BA.ISBN LEFT OUTER JOIN " +
         $"Author A ON BA.AuthorId = A.Id " +
         $"WHERE Title like '%{search}%' ORDER BY {orderBy} " +
         $"{direction} OFFSET {skip} ROWS FETCH NEXT {take} " +
         $"ROWS ONLY;", null, commandType: CommandType.Text));
         return books;
      }
   }
}

BookAuthorService.cs

  • BookAuthorService.cs implements the methods declared in the IBookAuthorService.cs file.
using BookApp.Interfaces;
using BookApp.Entities;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace BookApp.Data
{
   public class BookAuthorService : IBookAuthorService
   {
      private readonly IDapperService _dapperService;
      public BookAuthorService(IDapperService dapperService)
      {
         this._dapperService = dapperService;
      }
      public Task<int> Create(BookAuthorName bookAuthorName)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("ISBN", bookAuthorName.ISBN, DbType.Int64);
         dbPara.Add("AuthorId", bookAuthorName.AuthorId,
                    DbType.Int32);
         dbPara.Add("AuthorOrd", bookAuthorName.AuthorOrd,
                    DbType.Byte);
         var bookAuthorId = Task.FromResult(_dapperService.
             Insert<int>("[dbo].[spAddBookAuthor]", dbPara,
             commandType: CommandType.StoredProcedure));
         return bookAuthorId;
      }
      public Task<int> Delete(long isbn, int authorId)
      {
         var deleteBookAuthor = Task.FromResult(_dapperService.
             Execute($"Delete [BookAuthor] where ISBN = {isbn} " +
             $"and AuthorId = {authorId}", null,
             commandType: CommandType.Text));
         return deleteBookAuthor;
      }
      public Task<List<BookAuthorName>> FetchAll(long isbn)
      {
         var bookAuthorNames = Task.FromResult(_dapperService.GetAll
             <BookAuthorName>($"select * from BookAuthorName " +
             $"({isbn}) order by AuthorName; ", null,
             commandType: CommandType.Text));
         return bookAuthorNames;
      }
   }
}

DapperService.cs

  • DapperService.cs implements the methods declared in the IDapperService.cs file.
using BookApp.Interfaces;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
namespace BookApp.Data
{
   public class DapperService : IDapperService
   {
      private readonly IConfiguration _config;
      public DapperService(IConfiguration config)
      {
         _config = config;
      }
      public DbConnection GetConnection()
      {
         return new SqlConnection(_config.GetConnectionString
                ("DefaultConnection"));
      }
      public T Get<T>(string sp, DynamicParameters parms,
             CommandType commandType = CommandType.StoredProcedure)
      {
         using IDbConnection db = new SqlConnection
               (_config.GetConnectionString("DefaultConnection"));
         return db.Query<T>(sp, parms, commandType: commandType).
                FirstOrDefault();
      }
      public List<T> GetAll<T>(string sp, DynamicParameters parms,
             CommandType commandType = CommandType.StoredProcedure)
      {
         using IDbConnection db = new SqlConnection
               (_config.GetConnectionString("DefaultConnection"));
         return db.Query<T>(sp, parms, commandType: commandType).
                ToList();
      }
      public int Execute(string sp, DynamicParameters parms,
             CommandType commandType = CommandType.StoredProcedure)
      {
         using IDbConnection db = new SqlConnection
               (_config.GetConnectionString("DefaultConnection"));
         return db.Execute(sp, parms, commandType: commandType);
      }
      public T Insert<T>(string sp, DynamicParameters parms,
             CommandType commandType = CommandType.StoredProcedure)
      {
         T result;
         using IDbConnection db = new SqlConnection
               (_config.GetConnectionString("DefaultConnection"));
         try
         {
            if (db.State == ConnectionState.Closed)
               db.Open();
            using var tran = db.BeginTransaction();
            try
            {
               result = db.Query<T>(sp, parms,
                        commandType: commandType, transaction: tran)
                        .FirstOrDefault();
               tran.Commit();
            }
            catch (Exception ex)
            {
               tran.Rollback();
               throw ex;
            }
         }
         catch (Exception ex)
         {
            throw ex;
         }
         finally
         {
            if (db.State == ConnectionState.Open)
               db.Close();
         }
         return result;
      }
      public T Update<T>(string sp, DynamicParameters parms,
             CommandType commandType = CommandType.StoredProcedure)
      {
         T result;
         using IDbConnection db = new SqlConnection
               (_config.GetConnectionString("DefaultConnection"));
         try
         {
            if (db.State == ConnectionState.Closed)
               db.Open();
            using var tran = db.BeginTransaction();
            try
            {
               result = db.Query<T>(sp, parms,
                        commandType: commandType, transaction: tran)
                        .FirstOrDefault();
               tran.Commit();
            }
            catch (Exception ex)
            {
               tran.Rollback();
               throw ex;
            }
         }
         catch (Exception ex)
         {
            throw ex;
         }
         finally
         {
            if (db.State == ConnectionState.Open)
               db.Close();
         }
         return result;
      }
      public void Dispose()
      {
      }
   }
}

PublisherService.cs

  • PublisherService.cs implements the methods declared in the IPublisherService.cs file.
using BookApp.Interfaces;
using BookApp.Entities;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace BookApp.Data
{
   public class PublisherService : IPublisherService
   {
      private readonly IDapperService _dapperService;
      public PublisherService(IDapperService dapperService)
      {
         this._dapperService = dapperService;
      }
      public Task<int> Create(Publisher publisher)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("Name", publisher.Name, DbType.String);
         dbPara.Add("City", publisher.City, DbType.String);
         dbPara.Add("Country", publisher.Country, DbType.String);
         var publisherId = Task.FromResult(_dapperService.
             Insert<int>("[dbo].[spAddPublisher]", dbPara,
             commandType: CommandType.StoredProcedure));
         return publisherId;
      }
      public Task<Publisher> ReadByPk(int id)
      {
         var publisher = Task.FromResult(_dapperService.Get
         <Publisher>($"select * from [Publisher] where Id = {id}",
         null, commandType: CommandType.Text));
         return publisher;
      }
      public Task<int> Update(Publisher publisher)
      {
         var dbPara = new DynamicParameters();
         dbPara.Add("Id", publisher.Id);
         dbPara.Add("Name", publisher.Name, DbType.String);
         dbPara.Add("City", publisher.City, DbType.String);
         dbPara.Add("Country", publisher.Country, DbType.String);
         var updatePublisher = Task.FromResult(_dapperService.
             Update<int>("[dbo].[spUpdatePublisher]", dbPara, 
             commandType: CommandType.StoredProcedure));
         return updatePublisher;
      }
      public Task<int> Delete(int id)
      {
         var deletePublisher = Task.FromResult(_dapperService.
             Execute($"Delete [Publisher] where Id = {id}", null,
             commandType: CommandType.Text));
         return deletePublisher;
      }
      public Task<int> Count(string search)
      {
         var totPublisher = Task.FromResult(_dapperService.Get<int>
             ($"select COUNT(*) from [Publisher] WHERE Name like " +
             $"'%{search}%'", null, commandType: CommandType.Text));
         return totPublisher;
      }
      public Task<List<Publisher>> FetchAll()
      {
         var publishers = Task.FromResult
             (_dapperService.GetAll<Publisher>
             ($"SELECT * FROM [Publisher] ORDER BY Name; ",
              null, commandType: CommandType.Text));
         return publishers;
      }
      public Task<List<Publisher>> ListAll (int skip, int take,
             string orderBy, string direction = "DESC",
             string search = "")
      {
         var publishers = Task.FromResult(_dapperService.GetAll
             <Publisher>($"SELECT * FROM [Publisher] WHERE Name " +
             $"like '%{search}%' ORDER BY {orderBy} {direction} " +
             $"OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY; ",
             null, commandType: CommandType.Text));
         return publishers;
      }
   }
}

Component Files *. razor

There are nine razor files added to the Page folder: AddAuthor.razor, AddBook.razor, AddPublisher.razor,EditAuthor.razor,EditBook.razor, EditPublisher.razor, ListAuthor.razor, ListBook.razor, and ListPublisher.razor. All nine razor files contain I/O code.

AddAuthor.razor

  • It displays a page for adding a new author.
  • It may be called from: (1) menu (NavMenu.razor), or (2) booklist page (ListBook.razor)
@page "/addAuthor/{isbnDummy:long}"
@inject IAuthorService authorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h3>
   Add Author
</h3>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td>
               <label for="FName" class="control-label">
                  First Name
               </label>
            </td>
            <td>
               <input for="FName" class="form-control"
                      @bind="@author.FName" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="LName" class="control-label">
                  Last Name
               </label>
            </td>
            <td>
               <input for="LName" class="form-control"
                      @bind="@author.LName" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="Phone" class="control-label">
                  Phone
               </label>
            </td>
            <td>
               <input for="Phone" class="form-control"
                      @bind="@author.Phone" />
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => CreateAuthor()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr>
      </tbody>
   </table>
</form>
@code {
   [Parameter]
   public long isbnDummy { get; set; }
   Author author = new Author();
   protected async Task CreateAuthor()
   {
      await authorService.Create(author);
      Cancel();
   }
   void Cancel()
   {
      if (isbnDummy > 0)
         navigationManager.NavigateTo("/editBook/" + isbnDummy);
      else
         navigationManager.NavigateTo("/listAuthor");
   }
}

AddBook.razor

  • It’s for adding a book data.
  • Pay attention to the bold text below. The authors’ checkbox list appears if and only if the book data has been added.
  • The list consists of three columns. The first is the author's sequence of the book, the second is checkboxes, and the third is the author's name.
  • Change on a checkbox triggers the CheckChanged method on the C# Code block. Check a checkbox to include an author of the book or uncheck to exclude it.
@page "/addBook"
@inject IBookService bookService
@inject IPublisherService publisherService
@inject IBookAuthorService bookAuthorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h3>
   Add Book
</h3>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td><label for="ISBN" class="control-label">ISBN</label>
            </td>
            <td><input for="ISBN" class="form-control" 
                @bind="@book.ISBN" /></td>
         </tr>
         <tr>
            <td><label for="Title" class="control-label">
                Title</label></td>
            <td><input for="Title" class="form-control" 
                @bind="@book.Title" /></td>
         </tr>
         @if (@hasAdded)
         { 
            <tr>
               <td><label for="Authors" class="control-label">
                   Authors</label></td>
               <td><label style="width: 95px;">
                          <u><i><b>Sequence</b></i></u></label>
                  <label><u><i><b>Full Name</b></i></u></label><br/>
                  @foreach (var bookAuthorName in bookAuthorNames)
                  {
                     <input type="number" 
                            @bind="@bookAuthorName.AuthorOrd" 
                            @bind:event="oninput"
                            style="width: 75px;" min="0" max="5" />
                     @if (@bookAuthorName.ISBN > 0)
                     {
                        <input name="AreChecked" type="checkbox" 
                            value="@bookAuthorName.AuthorId" checked
                            @onchange = "eventArgs => { CheckChanged
                            (bookAuthorName, eventArgs.Value);}" />
                     }
                     else
                     {
                     <input name="AreChecked" type="checkbox" 
                            value="@bookAuthorName.AuthorId"
                            @onchange="eventArgs => { CheckChanged
                            (bookAuthorName, eventArgs.Value);}" />
                     }
                     <label for="AuthorName" style="width: 150px;">
                            @bookAuthorName.AuthorName</label><br />
                  }
               </td>
               <td>
                  &#8194;
                  <a class="btn btn-primary"
                          href='/addAuthor/@book.ISBN'>
                     &#8194;Add author&#8194;
                  </a><br />
               </td>
            </tr>
         }
         <tr>
            <td><label for="PubYear" class="control-label">
                       Publication Year</label></td>
            <td><input for="PubYear" class="form-control" 
                       @bind="@book.PubYear" /></td>
         </tr>
         <tr>
            <td><label for="PurchDate" class="control-label">
                       Purchase Date</label></td>
            <td><input type="date" class="form-control" 
                       @bind="@book.PurchDate" /></td>
         </tr>
         <tr>
            <td><label for="Publisher" class="control-label">
                       Publisher</label></td>
            <td>
               <select for="Publisher" class="form-control" 
                       @bind="@book.PubId">
                  <option value=0 disabled selected hidden>
                  [Select Publisher]</option>
                  @foreach (var publisher in publishers)
                  {
                     <option value="@publisher.Id">
                             @publisher.Name</option>
                  }
               </select>
            </td>
            <td>
               &#8194;
               <a class="btn btn-primary"
                  href='/addPublisher/@book.ISBN'>Add publisher</a>
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => CreateBook()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr>
      </tbody>
   </table>
</form>
@code {
   Book book = new Book();  
   BookAuthorName bookAuthorName = new BookAuthorName();
   List<Publisher> publishers = new List<Publisher>();
   List<BookAuthorName>bookAuthorNames = new List<BookAuthorName>();
   List<Book> books = new List<Book>();
   bool hasAdded = false;
   protected override async Task OnInitializedAsync()
   {
      //book.PubYear = (short)DateTime.Now.Year;
      book.PurchDate = DateTime.Now;      
      publishers = await publisherService.FetchAll();
   }
   protected async Task CreateBook()
   {
      if (hasAdded)
      {
         navigationManager.NavigateTo("/listBook");
      }
      else
      {
         await bookService.Create(book);
         bookAuthorNames = await bookAuthorService.FetchAll(0);
         hasAdded = !hasAdded;
      }
   }
   protected async Task CheckChanged(BookAuthorName bookAuthorName,
                                     object checkValue)
   {
      long isbn = 0;
      if (book.ISBN > isbn)
      {
         isbn = (long)book.ISBN;
         bookAuthorNames = await bookAuthorService.FetchAll(isbn);
         if ((bool)checkValue)
         {
            // insert
            bookAuthorName.ISBN = isbn;
            await bookAuthorService.Create(bookAuthorName);
         }
         else
         {
            //delete
            bookAuthorName.AuthorOrd = 0;
            await bookAuthorService.Delete
                  (isbn, bookAuthorName.AuthorId);
         }
         bookAuthorNames = await bookAuthorService.FetchAll(isbn);
      }
   }
   void Cancel()
   {
      navigationManager.NavigateTo("/listBook");
   }
}

AddPublisher.razor

  • It shows a page for adding an author.
  • It may be called from: (1) menu (NavMenu.razor), or (2) booklist page (ListBook.razor)
@page "/addPublisher/{isbnDummy:long}"
@inject IPublisherService publisherService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h3>
   Add Publisher
</h3>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td>
               <label for="Name" class="control-label">Name</label>
            </td>
            <td>
               <input for="Name" class="form-control"
                      @bind="@publisher.Name" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="City" class="control-label">City</label>
            </td>
            <td>
               <input for="City" class="form-control"
                      @bind="@publisher.City" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="Country" class="control-label">
                  Country
               </label>
            </td>
            <td>
               <input for="Country" class="form-control"
                      @bind="@publisher.Country" />
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => CreatePublisher()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr> 
      </tbody>
   </table>
</form>
@code {
   [Parameter]
   public long isbnDummy { get; set; }
   Publisher publisher = new Publisher();
   protected async Task CreatePublisher()
   {
      await publisherService.Create(publisher);
      Cancel();
   }
   void Cancel()
   {
      if (isbnDummy > 0)
         navigationManager.NavigateTo("/editBook/" + isbnDummy);
      else
         navigationManager.NavigateTo("/listPublisher");
   }
}

EditAuthor.razor

  • It displays a page for editing author data.
@page "/editAuthor/{id:int}"
@inject IAuthorService authorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h2>
   Edit Author
</h2>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td>
               <label for="FName" class="control-label">
                  First Name
               </label>
            </td>
            <td>
               <input for="FName" class="form-control"
                      @bind="@author.FName" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="LName" class="control-label">
                  Last Name
               </label>
            </td>
            <td>
               <input for="LName" class="form-control"
                      @bind="@author.LName" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="Phone" class="control-label">
                  Phone
               </label>
            </td>
            <td>
               <input for="Phone" class="form-control"
                      @bind="@author.Phone" />
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => UpdateAuthor()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr>
      </tbody>
   </table>
</form>
@code  {
   [Parameter]
   public int id { get; set; }
   Author author = new Author();
   protected override async Task OnInitializedAsync()
   {
      author = await authorService.ReadByPk(id);
   }
   protected async Task UpdateAuthor()
   {
      await authorService.Update(author);
      navigationManager.NavigateTo("/listAuthor");
   }
   void Cancel()
   {
      navigationManager.NavigateTo("/listAuthor");
   }
}

EditBook.razor

  • Focus your mind on the bold text below. The codes are for displaying the checkbox list of book authors.
  • The list consists of three columns. The first is the author's sequence of the book, the second is checkboxes, and the third is the author's name.
  • Change on a checkbox triggers the CheckChanged method on the c# @code block. Check a checkbox to include an author of the book or uncheck to exclude it.
@page "/editBook/{isbn:long}"
@inject IBookService bookService
@inject IPublisherService publisherService
@inject IBookAuthorService bookAuthorService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h3>
   Edit Book
</h3>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td>
               <label for="ISBN" class="control-label">
                  ISBN
               </label>
            </td>
            <td><input for="ISBN" class="form-control" 
                 @bind="@book.ISBN"  /></td>
         </tr>
         <tr>
            <td><label for="Title" class="control-label">
                       Title</label></td>
            <td><input for="Title" class="form-control" 
                       @bind="@book.Title" /></td>
         </tr>
         <tr>
            <td><label for="Authors" class="control-label">
                       Authors</label></td>
            <td>
               <label style="width: 95px;">
                      <u><i><b>Sequence</b></i></u></label>
               <label><u><i><b>Full Name</b></i></u></label><br />
               @foreach (var bookAuthorName in bookAuthorNames)
               {                 
                  <input type="number"
                         @bind="@bookAuthorName.AuthorOrd"
                         @bind:event="oninput"
                         style="width: 75px;" min="0" max="5" />
                  @if (@bookAuthorName.ISBN > 0)
                  {
                     <input name="AreChecked" type="checkbox"
                        value="@bookAuthorName.AuthorId" checked
                        @onchange="eventArgs => { CheckChanged
                        (bookAuthorName, eventArgs.Value);}" />
                  }
                  else
                  {
                     <input name="AreChecked" type="checkbox"
                        value="@bookAuthorName.AuthorId"
                        @onchange="eventArgs => { CheckChanged
                        (bookAuthorName, eventArgs.Value);}" />
                  }
                  <label for="AuthorName" style="width: 150px;">
                         @bookAuthorName.AuthorName</label><br />
               }
            </td>
            <td>
               &#8201;
               <a class="btn btn-primary"
                  href='/addAuthor/@book.ISBN'>
                  &#8194;Add author&#8194;</a><br />
            </td>
         </tr>
         <tr>
            <td><label for="PubYear" class="control-label">
                       Publication Year</label></td>
            <td><input for="PubYear" class="form-control" 
                       @bind="@book.PubYear" /></td>
         </tr>
         <tr>
            <td><label for="PurchDate" class="control-label">
                       Purchase Date</label></td>
            <td><input type="date" class="form-control" 
                       @bind="@book.PurchDate" /></td>
         </tr>
         <tr>
            <td><label for="Publisher" class="control-label">
                       Publisher</label></td>
            <td>
               <select for="Publisher" class="form-control" 
                       @bind="@book.PubId">
                  @foreach (var publisher in publishers)
                  {
                     <option value="@publisher.Id">
                             @publisher.Name</option>
                  }
               </select>
            </td>
            <td>
               &#8201;
               <a class="btn btn-primary"
                  href='/addPublisher/@book.ISBN'>Add publisher</a>
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => UpdateBook()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr>
      </tbody>
   </table>
</form>
@code {
   [Parameter] public long isbn { get; set; }
   Book book = new Book();
   BookAuthorName bookAuthorName = new BookAuthorName();
   List<Publisher> publishers = new List<Publisher>();
   List<BookAuthorName>bookAuthorNames = new List<BookAuthorName>();
   protected override async Task OnInitializedAsync()
   {
      book = await bookService.ReadByPk(isbn);
      bookAuthorNames = await bookAuthorService.FetchAll(isbn);
      publishers = await publisherService.FetchAll();
   }
   protected async Task CheckChanged(BookAuthorName bookAuthorName,
                                     object checkValue)
   {
      if ((bool)checkValue)
      {
         // insert
         bookAuthorName.ISBN = isbn;
         await bookAuthorService.Create(bookAuthorName);
      }
      else
      {
         // delete
         bookAuthorName.AuthorOrd = null;
         await bookAuthorService.Delete
               (isbn, bookAuthorName.AuthorId);
      }
      bookAuthorNames = await bookAuthorService.FetchAll(isbn);
   }
   protected async Task UpdateBook()
   {
      await bookService.Update(book, isbn);
      navigationManager.NavigateTo("/listBook");
   }
   void Cancel()
   {
      navigationManager.NavigateTo("/listBook");
   }
}

EditPublisher.razor

  • The component is for editing the data of publishers.
@page "/editPublisher/{id:int}"
@inject IPublisherService publisherService
@inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
<h2>
   Edit Publisher
</h2>
<form>
   <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tbody>
         <tr>
            <td>
               <label for="Name" class="control-label">
                  Name
               </label>
            </td>
            <td>
               <input for="Name" class="form-control"
                      @bind="@publisher.Name" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="City" class="control-label">
                  City
               </label>
            </td>
            <td>
               <input for="City" class="form-control"
                      @bind="@publisher.City" />
            </td>
         </tr>
         <tr>
            <td>
               <label for="Country" class="control-label">
                  Country
               </label>
            </td>
            <td>
               <input for="Country" class="form-control"
                      @bind="@publisher.Country" />
            </td>
         </tr>
         <tr>
            <td></td>
            <td>
               <br />
               <button type="button" class="btn btn-primary"
                       @onclick="() => UpdatePublisher()">
                  &#8195;Save&#8195;
               </button>&#8194;
               <button type="button" class="btn btn-warning"
                       @onclick="() => Cancel()">
                  &#8194;&#8201;Cancel&#8201;&#8194;
               </button>
            </td>
         </tr>
      </tbody>
   </table>
</form>
@code  {
   [Parameter]
   public int id { get; set; }
   Publisher publisher = new Publisher();
   protected override async Task OnInitializedAsync()
   {
      publisher = await publisherService.ReadByPk(id);
   }
   protected async Task UpdatePublisher()
   {
      await publisherService.Update(publisher);
      navigationManager.NavigateTo("/listPublisher");
   }
   void Cancel()
   {
      navigationManager.NavigateTo("/listPublisher");
   }
}

ListAuthor.razor

  • It displays a list of authors
@page "/listAuthor"
@inject IAuthorService authorService
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
   .sort-th {
      cursor: pointer;
   }
   .fa {
      float: right;
   }
   .btn-custom {
      color: black;
      float: left;
      padding: 8px 16px;
      text-decoration: none;
      transition: background-color .3s;
      border: 2px solid #000;
      margin: 0px 5px 0px 5px;
   }
</style>
<a class="btn btn-primary" href='/addAuthor/0'>Add new author</a>
@if (authors == null)
{
   <p><em>Loading...</em></p>
}
else
{
   <div class="row col-md-3 pull-right">
      <input type="text" id="txtSearch" 
             placeholder="Search Names..." class="form-control" 
             @bind="SearchTerm" @bind:event="oninput" />
   </div>
   <table class="table table-bordered table-hover">
      <thead>
         <tr>
            <th class="sort-th" @onclick="@(() => SortTable("Id"))">
                Id<span class="fa @(SetSortIcon("Id"))"></span>
            </th>
            <th class="sort-th" 
                @onclick="@(() => SortTable("FName"))">First Name
               <span class="fa @(SetSortIcon("FName"))"></span>
            </th>
            <th class="sort-th" 
                @onclick="@(() => SortTable("LName"))">Last Name
               <span class="fa @(SetSortIcon("LName"))"></span>
            </th>
            <th class="sort-th" 
                @onclick="@(() => SortTable("Phone"))">Phone
               <span class="fa @(SetSortIcon("Phone"))"></span>
            </th>
            <th>Action</th>
         </tr>
      </thead>
      <tbody>
         @if (authors == null || authors.Count == 0)
         {
            <tr>
               <td colspan="3">
                  No Records to display
               </td>
            </tr>
         }
         else
         {
            foreach (var author in authors)
            {
               <tr>
                  <td><hr style="padding:0px; margin:0px;">
                     @author.Id
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @author.FName
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @author.LName
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @author.Phone
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     <a class="btn btn-primary"
                        href='/editAuthor/@author.Id'>
                        &#8194;Edit&#8194;</a>&#8194;
                     <a class="btn btn-warning" @onclick="() =>
                        DeleteAuthor(author.Id)">Delete</a>
                  </td>
               </tr> 
            }
         }
      </tbody>
   </table>
   <div class="pagination">
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("previous"))>◀</button>
   @for (int i = startPage; i <= endPage; i++)
      {
         var currentPage = i;
         <button class="btn btn-custom pagebutton
                 @(currentPage==curPage?"btn-info":"")" 
                 @onclick =@(async () =>
                 await refreshRecords(currentPage))>@currentPage
         </button>
      }
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("next"))>▶</button>
   </div>
}
@code {
   private string searchTerm;
   private string SearchTerm
   {
      get { return searchTerm; }
      set { searchTerm = value; FilterRecords(); }
   }
   List<Author> authors;
   #region Pagination
   int totalPages;
   int totalRecords;
   int curPage;
   int pagerSize;
   int pageSize;
   int startPage;
   int endPage;
   string sortColumnName = "Id";
   string sortDir = "ASC";
   #endregion
   protected override async Task OnInitializedAsync()
   {
      //display total page buttons
      pagerSize = 3;
      pageSize = 5;
      curPage = 1;
      authors = await authorService.ListAll((curPage - 1) *
         pageSize, pageSize, sortColumnName, sortDir, searchTerm);
      totalRecords = await authorService.Count(searchTerm);
      totalPages = (int)Math.Ceiling
                   (totalRecords / (decimal)pageSize);
      SetPagerSize("forward");
   }
   protected async Task DeleteAuthor(int id)
   {
      await authorService.Delete(id);
      authors = await authorService.ListAll((curPage - 1) *
         pageSize, pageSize, sortColumnName, sortDir, searchTerm);
   }
   private bool isSortedAscending;
   private string activeSortColumn;
   private async Task<List<Author>>
           SortRecords(string columnName, string dir)
   {
      return await authorService.ListAll((curPage - 1) * pageSize,
         pageSize, columnName, dir, searchTerm);
   }
   private async Task SortTable(string columnName)
   {
      if (columnName != activeSortColumn)
      {
         authors = await SortRecords(columnName, "ASC");
         isSortedAscending = true;
         activeSortColumn = columnName;
      }
      else
      {
         if (isSortedAscending)
         {
            authors = await SortRecords(columnName, "DESC");
         }
         else
         {
            authors = await SortRecords(columnName, "ASC");
         }
         isSortedAscending = !isSortedAscending;
      }
      sortColumnName = columnName;
      sortDir = isSortedAscending ? "ASC" : "DESC";
   }
   private string SetSortIcon(string columnName)
   {
      if (activeSortColumn != columnName)
      {
         return string.Empty;
      }
      if (isSortedAscending)
      {
         return "fa-sort-up";
      }
      else
      {
         return "fa-sort-down";
      }
   }
   public async Task refreshRecords(int currentPage)
   {
      authors = await authorService.ListAll((currentPage - 1)
         * pageSize, pageSize, sortColumnName, sortDir, searchTerm);
      curPage = currentPage;
      this.StateHasChanged();
   }
   public void SetPagerSize(string direction)
   {
      if (direction == "forward" && endPage < totalPages)
      {
         startPage = endPage + 1;
         if (endPage + pagerSize < totalPages)
         {
            endPage = startPage + pagerSize - 1;
         }
         else
         {
            endPage = totalPages;
         }
         this.StateHasChanged();
      }
      else if (direction == "back" && startPage > 1)
      {
         endPage = startPage - 1;
         startPage = startPage - pagerSize;
      }
      else
      {
         startPage = 1;
         endPage = totalPages;
      }
   }
   public async Task NavigateToPage(string direction)
   {
      if (direction == "next")
      {
         if (curPage < totalPages)
         {
            if (curPage == endPage)
            {
               SetPagerSize("forward");
            }
            curPage += 1;
         }
      }
      else if (direction == "previous")
      {
         if (curPage > 1)
         {
            if (curPage == startPage)
            {
               SetPagerSize("back");
            }
            curPage -= 1;
         }
      }
      await refreshRecords(curPage);
   }
   public void FilterRecords()
   {
      endPage = 0;
      this.OnInitializedAsync().Wait();
   }
}

ListBook.razor

  • It shows a list of books includes its authors and publisher.
@page "/listBook"
@inject IBookService bookService
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
   .sort-th {
      cursor: pointer;
   }
   .fa {
      float: right;
   }
   .btn-custom {
      color: black;
      float: left;
      padding: 8px 16px;
      text-decoration: none;
      transition: background-color .3s;
      border: 2px solid #000;
      margin: 0px 5px 0px 5px;
   }
</style>
<a class="btn btn-primary" href='/addBook'>Add new book</a>
@if (books == null)
{
   <p><em>Loading...</em></p>
}
else
{
   <div class="row col-md-3 pull-right">
      <input type="text" id="txtSearch" 
             placeholder="Search Titles..." class="form-control" 
             @bind="SearchTerm" @bind:event="oninput" />
   </div>
   <table class="table table-bordered table-hover">
      <thead>
         <tr>
            <th class="sort-th" 
               @onclick="@(() => SortTable("ISBN"))">I S B N
               <span class="fa @(SetSortIcon("ISBN"))"></span>
            </th>
            <th class="sort-th"
                @onclick="@(() => SortTable("Title"))">
               T i t l e
               <span class="fa @(SetSortIcon("Title"))"></span>
            </th>
            <th class="sort-th" 
               @onclick="@(() => SortTable("AuthorName"))">Author
               <span class="fa @(SetSortIcon("AuthorName"))"></span>
            </th>
            <th class="sort-th" 
               @onclick="@(() => SortTable("PubYear"))">Pub.<br />
               Year
               <span class="fa @(SetSortIcon("PubYear"))"></span>
            </th>
            <th class="sort-th" 
               @onclick="@(() => SortTable("PurchDate"))">
               Purchase<br />Date
               <span class="fa @(SetSortIcon("PurchDate"))"></span>
            </th>
            <th class="sort-th" 
               @onclick="@(() => SortTable("PubName"))">Publisher
               <span class="fa @(SetSortIcon("PubName"))"></span>
            </th>
            <th>Action</th>
         </tr>
      </thead>
      <tbody>
         @if (books == null || books.Count == 0)
         {
            <tr>
               <td colspan="3">
                  No Records to display
               </td>
            </tr>
         }
         else
         {
            long prevISBN = 0;
            foreach (var book in books)
            {
               if (@book.ISBN != prevISBN)
               {
                  <tr>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.ISBN</td>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.Title</td>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.AuthorName</td>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.PubYear</td>
                     <td><hr style="padding:0px;  margin:0px;">
                         @book.PurchDate.ToShortDateString()</td>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.PubName</td>
                     <td><hr style="padding:0px; margin:0px;">
                        <a class="btn btn-primary"
                           href='/editBook/@book.ISBN'>
                        &#8194;Edit&#8194;</a>&#8194;
                        <a class="btn btn-warning" @onclick="() =>
                           DeleteBook((long)book.ISBN)">Delete</a>
                     </td>
                  </tr>
               }
               else
               {
                  <tr>
                     <td></td>
                     <td></td>
                     <td><hr style="padding:0px; margin:0px;">
                         @book.AuthorName</td>
                  </tr>
               }
               prevISBN = (long)@book.ISBN;
            }
         }
      </tbody>
   </table>
   <div class="pagination">
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("previous"))>◀</button>
   @for (int i = startPage; i <= endPage; i++)
      {
         var currentPage = i;
         <button class="btn btn-custom pagebutton 
                 @(currentPage==curPage?"btn-info":"")"
                 @onclick=@(async () =>
                 await refreshRecords(currentPage))>@currentPage
         </button>
      }
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("next"))>▶</button>
   </div>
}
@code {
   private string searchTerm;
   private string SearchTerm
   {
      get { return searchTerm; }
      set { searchTerm = value; FilterRecords(); }
   }
   List<BookAuPub> books;
   #region Pagination
   int totalPages;
   int totalRecords;
   int curPage;
   int pagerSize;
   int pageSize;
   int startPage;
   int endPage;
   string sortColumnName = "PurchDate";
   string sortDir = "DESC";
   #endregion
   protected override async Task OnInitializedAsync()
   {
      //display total page buttons
      pagerSize = 3;
      pageSize = 8;
      curPage = 1;
      books = await bookService.ListAll((curPage - 1) * pageSize,
              pageSize, sortColumnName, sortDir, searchTerm);
      totalRecords = await bookService.Count(searchTerm);
      totalPages = (int)Math.Ceiling
                   (totalRecords / (decimal)pageSize);
      SetPagerSize("forward");
   }
   protected async Task DeleteBook(long isbn)
   {
      await bookService.Delete(isbn);
      books = await bookService.ListAll((curPage - 1) * pageSize,
              pageSize, sortColumnName, sortDir, searchTerm);
   }
   private bool isSortedAscending;
   private string activeSortColumn;
   private async Task<List<BookAuPub>> SortRecords
           (string columnName, string dir)
   {
      return await bookService.ListAll((curPage - 1) * pageSize,
             pageSize, columnName, dir, searchTerm);
   }
   private async Task SortTable(string columnName)
   {
      if (columnName != activeSortColumn)
      {
         books = await SortRecords(columnName, "ASC");
         isSortedAscending = true;
         activeSortColumn = columnName;
      }
      else
      {
         if (isSortedAscending)
         {
            books = await SortRecords(columnName, "DESC");
         }
         else
         {
            books = await SortRecords(columnName, "ASC");
         }
         isSortedAscending = !isSortedAscending;
      }
      sortColumnName = columnName;
      sortDir = isSortedAscending ? "ASC" : "DESC";
   }
   private string SetSortIcon(string columnName)
   {
      if (activeSortColumn != columnName)
      {
         return string.Empty;
      }
      if (isSortedAscending)
      {
         return "fa-sort-up";
      }
      else
      {
         return "fa-sort-down";
      }
   }
   public async Task refreshRecords(int currentPage)
   {
      books = await bookService.ListAll((currentPage - 1)
         * pageSize, pageSize, sortColumnName, sortDir, searchTerm);
      curPage = currentPage;
      this.StateHasChanged();
   }
   public void SetPagerSize(string direction)
   {
      if (direction == "forward" && endPage < totalPages)
      {
         startPage = endPage + 1;
         if (endPage + pagerSize < totalPages)
         {
            endPage = startPage + pagerSize - 1;
         }
         else
         {
            endPage = totalPages;
         }
         this.StateHasChanged();
      }
      else if (direction == "back" && startPage > 1)
      {
         endPage = startPage - 1;
         startPage = startPage - pagerSize;
      }
      else
      {
         startPage = 1;
         endPage = totalPages;
      }
   }
   public async Task NavigateToPage(string direction)
   {
      if (direction == "next")
      {
         if (curPage < totalPages)
         {
            if (curPage == endPage)
            {
               SetPagerSize("forward");
            }
            curPage += 1;
         }
      }
      else if (direction == "previous")
      {
         if (curPage > 1)
         {
            if (curPage == startPage)
            {
               SetPagerSize("back");
            }
            curPage -= 1;
         }
      }
      await refreshRecords(curPage);
   }
   public void FilterRecords()
   {
      endPage = 0;
      this.OnInitializedAsync().Wait();
   }
}

ListPublisher.razor

  • It lists publishers.
@page "/listPublisher"
@inject IPublisherService publisherService
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
   .sort-th {
      cursor: pointer;
   }
   .fa {
      float: right;
   }
   .btn-custom {
      color: black;
      float: left;
      padding: 8px 16px;
      text-decoration: none;
      transition: background-color .3s;
      border: 2px solid #000;
      margin: 0px 5px 0px 5px;
   }
</style>
<a class="btn btn-primary" href='/addPublisher/0'>
   Add new publisher</a>
@if (publishers == null)
{
   <p><em>Loading...</em></p>
}
else
{
   <div class="row col-md-3 pull-right">
      <input type="text" id="txtSearch" 
             placeholder="Search Names..." class="form-control" 
             @bind="SearchTerm" @bind:event="oninput" />
   </div>
   <table class="table table-bordered table-hover">
      <thead>
         <tr>
            <th class="sort-th" @onclick="@(() => SortTable("Id"))">
                Id<span class="fa @(SetSortIcon("Id"))"></span>
            </th>
            <th class="sort-th"
                @onclick="@(() => SortTable("Name"))">Name
               <span class="fa @(SetSortIcon("Name"))"></span>
            </th>
            <th class="sort-th"
                @onclick="@(() => SortTable("City"))">City
               <span class="fa @(SetSortIcon("City"))"></span>
            </th>
            <th class="sort-th"
                @onclick="@(() => SortTable("Country"))">Country
               <span class="fa @(SetSortIcon("Country"))"></span>
            </th>
            <th>Action</th>
         </tr>
      </thead>
      <tbody>
         @if (publishers == null || publishers.Count == 0)
         {
            <tr>
               <td colspan="3">
                  No Records to display
               </td>
            </tr>
         }
         else
         {
            foreach (var publisher in publishers)
            {
               <tr>
                  <td> <hr style="padding:0px; margin:0px;">
                     @publisher.Id
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @publisher.Name
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @publisher.City
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     @publisher.Country
                  </td>
                  <td><hr style="padding:0px; margin:0px;">
                     <a class="btn btn-primary"
                        href='/editPublisher/@publisher.Id'>
                        &#8194;Edit&#8194;</a>&#8194;
                     <a class="btn btn-warning" @onclick="() =>
                        DeletePublisher(publisher.Id)">Delete</a>
                  </td>
               </tr>
            }
         }
      </tbody>
   </table>
   <div class="pagination">
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("previous"))>◀</button>
   @for (int i = startPage; i <= endPage; i++)
      {
         var currentPage = i;
         <button class="btn btn-custom pagebutton 
                 @(currentPage==curPage?"btn-info":"")" 
                 @onclick =@(async () =>
                 await refreshRecords(currentPage))>@currentPage
         </button>
      }
      <button class="btn btn-custom" @onclick=@(async ()=>
              await NavigateToPage("next"))>▶</button>
   </div>
}
@code {
   private string searchTerm;
   private string SearchTerm
   {
      get { return searchTerm; }
      set { searchTerm = value; FilterRecords(); }
   }
   List<Publisher> publishers;
   #region Pagination
   int totalPages;
   int totalRecords;
   int curPage;
   int pagerSize;
   int pageSize;
   int startPage;
   int endPage;
   string sortColumnName = "Id";
   string sortDir = "ASC";
   #endregion
   protected override async Task OnInitializedAsync()
   {
      //display total page buttons
      pagerSize = 3;
      pageSize = 5;
      curPage = 1;
      publishers = await publisherService.ListAll((curPage - 1) * 
         pageSize, pageSize, sortColumnName, sortDir, searchTerm);
      totalRecords = await publisherService.Count(searchTerm);
      totalPages = (int)Math.Ceiling
                   (totalRecords / (decimal)pageSize);
      SetPagerSize("forward");
   }
   protected async Task DeletePublisher(int id)
   {
      await publisherService.Delete(id);
      publishers = await publisherService.ListAll((curPage - 1) * 
         pageSize, pageSize, sortColumnName, sortDir, searchTerm);
   }
   private bool isSortedAscending;
   private string activeSortColumn;
   private async Task<List<Publisher>>
           SortRecords(string columnName, string dir)
   {
      return await publisherService.ListAll((curPage - 1)*pageSize,
         pageSize, columnName, dir, searchTerm);
   }
   private async Task SortTable(string columnName)
   {
      if (columnName != activeSortColumn)
      {
         publishers = await SortRecords(columnName, "ASC");
         isSortedAscending = true;
         activeSortColumn = columnName;
      }
      else
      {
         if (isSortedAscending)
         {
            publishers = await SortRecords(columnName, "DESC");
         }
         else
         {
            publishers = await SortRecords(columnName, "ASC");
         }
         isSortedAscending = !isSortedAscending;
      }
      sortColumnName = columnName;
      sortDir = isSortedAscending ? "ASC" : "DESC";
   }
   private string SetSortIcon(string columnName)
   {
      if (activeSortColumn != columnName)
      {
         return string.Empty;
      }
      if (isSortedAscending)
      {
         return "fa-sort-up";
      }
      else
      {
         return "fa-sort-down";
      }
   }
   public async Task refreshRecords(int currentPage)
   {
      publishers = await publisherService.ListAll((currentPage - 1)
         * pageSize, pageSize, sortColumnName, sortDir, searchTerm);
      curPage = currentPage;
      this.StateHasChanged();
   }
   public void SetPagerSize(string direction)
   {
      if (direction == "forward" && endPage < totalPages)
      {
         startPage = endPage + 1;
         if (endPage + pagerSize < totalPages)
         {
            endPage = startPage + pagerSize - 1;
         }
         else
         {
            endPage = totalPages;
         }
         this.StateHasChanged();
      }
      else if (direction == "back" && startPage > 1)
      {
         endPage = startPage - 1;
         startPage = startPage - pagerSize;
      }
      else
      {
         startPage = 1;
         endPage = totalPages;
      }
   }
   public async Task NavigateToPage(string direction)
   {
      if (direction == "next")
      {
         if (curPage < totalPages)
         {
            if (curPage == endPage)
            {
               SetPagerSize("forward");
            }
            curPage += 1;
         }
      }
      else if (direction == "previous")
      {
         if (curPage > 1)
         {
            if (curPage == startPage)
            {
               SetPagerSize("back");
            }
            curPage -= 1;
         }
      }
      await refreshRecords(curPage);
   }
   public void FilterRecords()
   {
      endPage = 0;
      this.OnInitializedAsync().Wait();
   }
}

Code Modification

File NavMenu.razor Modify the code so it becomes as follows.

<div class="top-row pl-4 navbar navbar-dark">
   <a class="navbar-brand" href="">Library</a>
   <button class="navbar-toggler" @onclick="ToggleNavMenu">
      <span class="navbar-toggler-icon"></span>
   </button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
   <ul class="nav flex-column">
      <li class="nav-item px-3">
         <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
            <span class="oi oi-home" aria-hidden="true"></span>
            Home
         </NavLink>
      </li>
      <li class="nav-item px-3">
         <NavLink class="nav-link" href="listBook">
            <span class="oi oi-book" aria-hidden="true"></span>
            Books
         </NavLink>
      </li>
      <li class="nav-item px-3">
         <NavLink class="nav-link" href="listPublisher">
            <span class="oi oi-list-rich" aria-hidden="true"></span>
            Publishers
         </NavLink>
      </li>
      <li class="nav-item px-3">
         <NavLink class="nav-link" href="listAuthor">
            <span class="oi oi-list-rich" aria-hidden="true"></span>
            Authors
         </NavLink>
      </li>
   </ul>
</div>
@code {
   private bool collapseNavMenu = true;
   private string NavMenuCssClass => 
                  collapseNavMenu ? "collapse" : null;
   private void ToggleNavMenu()
   {
      collapseNavMenu = !collapseNavMenu;
   }
}

_Imports.razor Add the following namespace.

@using BookApp
@using BookApp.Shared
@using BookApp.Data
@using BookApp.Interfaces
@using BookApp.Entities

Startup.cs Add the following namespace.

using BookApp.Interfaces;
using BookApp.Data;

Add the following service.

//BookAuthor service  
services.AddScoped<IBookAuthorService, BookAuthorService>();
//Author service  
services.AddScoped<IAuthorService, AuthorService>();
//Publisher service  
services.AddScoped<IPublisherService, PublisherService>();
//Book service  
services.AddScoped<IBookService, BookService>();
//Register dapper in scope  
services.AddScoped<IDapperService, DapperService>();

There may be better ways to implement the checkbox list on CRUD operation that involves an M:N (many-to-many) relationship. Please inform me if you know it.

References

Blazor
Crud
Dapper
Sql Server
Checkbox List
Recommended from ReadMedium