Application data
.NET .NET Core ASP.NET Core Blazor C# Entity Framework Core Razor Visual Studio

How to use Entity Framework Core with ASP.NET Core Blazor Server Applications

Welcome to today’s post.

In today’s post I will show how to abstract data access within an ASP.NET Core Blazor web server application with integration with Entity Framework Core.

In the previous post I showed how to create a starter Blazor web server application within Visual Studio 2022.

I also explained some of the similarities and differences between ASP.NET Core Blazor server applications and ASP.NET Core MVC applications.

 In a previous post I showed how to use Entity Framework Core to integrate SQL server data within an ASP.NET Core Web API application.

In this post I will be showing how to configure an existing SQL Server database into the .NET Core Blazor application from the startup sequence within Program.cs. With .NET Core versions prior to version 6.0 I showed how to integrate SQL from Startup.cs

The first task we do is to add a connection string for our SQL data into the AppSettings.json file.

"ConnectionStrings": {
    "AppDbContext": "Server=localhost;Database=aspnet-BookCatalog;User Id=????;Password=????;Trust Server Certificate=true;",
},

In the next section, I will show how to uses C# classes to provide data persistence from the SQL database.

Declare a Class for Data Persistence

Next, we declare a model (or DTO) class to contain the data from our SQL tables. This will be used throughout each data CRUD operation and when selecting and filtering SQL data from database.

Note that fields with the [Required] data annotation needs in this case to have the value within the corresponding database field as a non-null value, but is set to nullable (with the ? operator) to allow the property in the class to be unassigned within any of the interfaces. Additional user interface validations can enforce these rules.

Below is the model class BookViewModel.cs within the \BookLoanBlazorServerApp\Model sub-folder:

using System.ComponentModel.DataAnnotations;

namespace BookLoanBlazorServerApp.Models
{
    [Serializable]
    public class BookViewModel
    {
        public int ID { get; set; }

        [Required]
        public string? Title { get; set; }

        [Required]
        public string? Author { get; set; }

        public int YearPublished { get; set; }

        public string? Genre { get; set; }

        public string? Edition { get; set; }

        public string? ISBN { get; set; }

        [Required]
        public string? Location { get; set; }

        [Required]
        public string? MediaType { get; set; }

        public DateTime DateCreated { get; set; }

        public DateTime DateUpdated { get; set; }

        public BookViewModel() { }
    }
}

To make use of the above class will require us to create a data context. I will show how this is done in the next section.

Creating a Data Context

Next, we create a class that derives from the DbContext class to allow us to reference tables within our SQL data source from the Entity Framework Core API.

Before we can implement the data context class, we will need to install the following NuGet packages using the NuGet Package Manager:

Once we have installed the above packages, we then add following ApplicationDbContext.cs file to subfolder \BookLoanBlazorServerApp\Data:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BookLoan.Models;

namespace BookLoan.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.Entity<BookViewModel>().ToTable("Books");
            builder.Entity<GenreViewModel>().ToTable("Genres");
        }

        public DbSet<BookViewModel> Books { get; set; }
        public DbSet<GenreViewModel> Genres { get; set; }
    }
}

To consume the data, we will need a data service class. I will implement one in the next section.

Implement a Data Service Class

Before we can complete configuration with data and integrate services, we will need a service class for our Book table.

We implement an interface and a service class, IBookService.cs and BookService.cs, which we move into the \BookLoanBlazorServerApp\Services sub-folder.  The interface IBookService.cs is shown below:

using System.Collections.Generic;
using BookLoanBlazorServerApp.Models;

namespace BookLoanBlazorServerApp.Services
{
    public interface IBookService
    {
        Task<List<BookViewModel>> GetBooks();
        Task<List<GenreViewModel>> GetGenres();
        Task<List<BookViewModel>> GetBooksFilter(string filter);
        Task<BookViewModel> GetBook(int id);
        Task<int> SaveBook(BookViewModel vm);
        Task<BookViewModel> UpdateBook(int id, BookViewModel vm);
        Task<bool> DeleteBook(int id);
    }
}

The service class BookService.cs is shown below:

using Microsoft.EntityFrameworkCore;
using BookLoanBlazorServerApp.Data;
using BookLoanBlazorServerApp.Models;

namespace BookLoanBlazorServerApp.Services
{
    public class BookService : IBookService
    {
        readonly ApplicationDbContext _db;
        private readonly ILogger _logger;

        public BookService(ApplicationDbContext db,
            ILogger<BookService> logger)
        {
            _db = db;
            _logger = logger;
        }

        /// <summary>
        /// GetBooks()
        /// </summary>
        /// <returns></returns>
        public async Task<List<BookViewModel>> GetBooks()
        {
            return await _db.Books.ToListAsync(); 
        }

        /// <summary>
        /// GetBooksFilter()
        /// </summary>
        /// <returns></returns>
        public async Task<List<BookViewModel>> GetBooksFilter(string filter)
        {
            List<BookViewModel> bookList = new List<BookViewModel>();
            var books = await _db.Books
                .Where(b => b.Title!.Contains(filter)).ToListAsync();
            return books;
        }

        /// <summary>
        /// GetBook()
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<BookViewModel> GetBook(int id)
        {
            var book = await _db.Books.Where(b => b.ID == id).SingleOrDefaultAsync();
            return book!;
        }

        /// <summary>
        /// SaveBook()
        /// </summary>
        /// <param name="vm"></param>
        /// <returns></returns>
        public async Task<int> SaveBook(BookViewModel vm)
        {
            try
            {
                var newBook = await _db.Books.AddAsync(vm);
                await _db.SaveChangesAsync();
                var newId = newBook.CurrentValues.GetValue<int>("ID");
                return newId;
            }
            catch
            {
                return 0;
            }
        }

        /// <summary>
        /// UpdateBook()
        /// </summary>
        /// <param name="Id"></param>
        /// <param name="vm"></param>
        /// <returns></returns>
        public async Task<BookViewModel> UpdateBook(int Id, BookViewModel vm)
        {
            var book = await _db.Books
                .Where(b => b.ID == Id)
                .SingleOrDefaultAsync();

            if (book != null)
            {
                string originalEdition = book.Edition!;
                book.Title = vm.Title;
                book.Author = vm.Author;
                book.Edition = vm.Edition;
                book.Genre = vm.Genre;
                book.ISBN = vm.ISBN;
                book.Location = vm.Location;
                book.YearPublished = vm.YearPublished;

                this._db.Update(book);
            }
            return book!;
        }

        public async Task<bool> DeleteBook(int id)
        {
            var book = await _db.Books
                .Where(b => b.ID == id)
                .SingleOrDefaultAsync();

            if ( book != null ) {
                _db.Books.Remove(book!);
                await _db.SaveChangesAsync();
            }

            return (book != null);
        }

        public async Task<List<GenreViewModel>> GetGenres()
        {
            return await _db.Genres.ToListAsync();
        }
    }
}

In the program startup sequence, we apply the following configurations:

  1. Setup the Razor and Blazor Server services.
  2. Setup SQL database connectivity from the connection string through the ApplicationDbContext class.
  3. We bind the interface to the BookService class implementation and add it to the service collection (this is just dependency injection).
  4. We build the web app pipeline.
  5. Map routes.
  6. Run the app.

The setup in Program.cs is shown below:

using BookLoanBlazorServerApp.Data;
using BookLoanBlazorServerApp.Services;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

string connStr = "AppDbContext";

try
{
    builder.Services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseSqlServer(builder.Configuration.GetConnectionString(connStr));
    });
}
catch
{
    Console.WriteLine("Error: Cannot connect to database.");
}

builder.Services.AddTransient<IBookService, BookService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

Now that we have setup the data access, the first data operation we will integrate is data retrieval. This means that Entity Framework will execute SELECT statements against our SQL data store and return results through our model/DTO classes.

Selecting SQL Data using Entity Framework

The data is selected from the SQL database Book table from the following method within he BookService class:

/// <summary>
/// GetBooks()
/// </summary>
/// <returns></returns>
public async Task<List<BookViewModel>> GetBooks()
{
    return await _db.Books.ToListAsync(); 
}

Note that the above is an asynchronous task, which is non-blocking. What this means is that our interface will not wait for the data retrieval operation to complete and will start rendering, so we will include some additional conditional checks in our markup script to ensure the data is presented when the retrieval task has completed.

Presenting Data with a Razor Component User Interface

I will now show how to create a Razor component from Visual Studio Solution Explorer.

Select the Pages subfolder in the solution within Solution Explorer, right-click and select the Add New Item… context menu item.

In the context menu, select the Razor Component… option:

In the dialog that opens, select Razor Component, enter a name for the component source file, then click Add.

Repeat the above component creation with names:

ListBooks.razor

CreateBook.razor

EditBook.razor

StatusPage.razor

After the above pages and source files are implemented and generated, the solution structure will be as follows:

We will now create links to the following pages and routing path:

Description         Routing Path
Create a Book    createbook
List Books            listbooks

In our left-sided navigation menu, apply the following task: 

Open file Navmenu.razor, then remove existing links (for the Weather example) and adding links for <NavLink..> elementswith attributes href=”createbook” and href=”listbooks” as shown below:

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="createbook">
                <span class="oi oi-plus" aria-hidden="true"></span> Create a Book
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="listbooks">
                <span class="oi oi-plus" aria-hidden="true"></span> List Books
            </NavLink>
        </div>
    </nav>
</div>

Save the above navigation razor file. Once the above task is completed, build, and run the application.

If successful, you should see the new links on the left navigation panel as shown:

Open the empty Razor component file, ListBooks.razor, which is saved in the \BookLoanBlazorServerApp\Pages sub-solder. You will see the following lines of script:

<h3>ListBooks</h3>

@code {
}

Now replace the above with the following script and code to list the book data:

@page "/listbooks"
@using BookLoanBlazorServerApp.Services
@using BookLoanBlazorServerApp.Models
@inject IBookService LibraryBookService

<PageTitle>List Books</PageTitle>

<h3>List Books</h3>

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Author</th>
                <th>ISBN</th>
                <th>Year Published</th>
                <th>Genre</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Title</td>
                    <td>@book.Author</td>
                    <td>@book.ISBN</td>
                    <td>@book.YearPublished</td>
                    <td>@book.Genre</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private List<BookViewModel>? books;

    protected override async Task OnInitializedAsync()
    {
        books = await LibraryBookService.GetBooks();
    }
}

The Book service has been injected into the Razor component and bound to the concrete class instance LibraryBookService by using the following line:

@inject IBookService LibraryBookService

Also, that as I explained earlier, the script has the following condition:

@if (books == null)
{
    <p><em>Loading...</em></p>
}

to check for null list of books while the book data is being loaded asynchronously within OnInitializedAsync() from the service, BookService.

After rebuilding and running the app, click on the List Books navigation link, after a short delay you should see the following page full of records (provide there are records in your table):

That above completes the first SQL data retrieval operation.

I will next show how to add a record into our Book table.

Creating Data with a Razor Component User Interface

Data is selected from the SQL database Book table from the following method within he BookService class:

/// <summary>
/// SaveBook()
/// </summary>
/// <param name="vm"></param>
/// <returns></returns>
public async Task<int> SaveBook(BookViewModel vm)
{
    try
    {
        var newBook = await _db.Books.AddAsync(vm);
        await _db.SaveChangesAsync();
        var newId = newBook.CurrentValues.GetValue<int>("ID");
        return newId;
    }
    catch
    {
        return 0;
    }
}

Note, like the data retrieval task we implemented earlier, the above is an asynchronous task. When the record is created from the above method, it will return a book identifier (primary key) integer value that is greater than zero. If a record cannot be created, an integer value of zero is returned to the calling Razor component.

Open the empty Razor component file, CreateBook.razor, which is saved in the \BookLoanBlazorServerApp\Pages sub-solder. You will see the following lines of script:

<h3>CreateBook</h3>

@code {
}

Now replace the above with the following script and code to create the book data:

@page "/createbook"
@using Microsoft.AspNetCore.Components
@using BookLoanBlazorServerApp.Services
@using BookLoanBlazorServerApp.Models
@inject IBookService LibraryBookService
@inject NavigationManager PageNavigation

<PageTitle>Create Book</PageTitle>

<h3>Create Book</h3>

<div class="form-field">
    <label>Title:</label>
    <div>
        <input @bind="book.Title" />
    </div>
</div>

<div class="form-field">
    <label>Author:</label>
    <div>
        <input @bind="book.Author" />
    </div>
</div>

<div class="form-field">
    <label>Year Published:</label>
    <div>
        <input @bind="book.YearPublished" />
    </div>
</div>

<div class="form-field">
    <label>Genre:</label>
    <div>
        <input @bind="book.Genre" />
    </div>
</div>

<div class="form-field">
    <label>Edition:</label>
    <div>
        <input @bind="book.Edition" />
    </div>
</div>

<div class="form-field">
    <label>ISBN:</label>
    <div>
        <input @bind="book.ISBN" />
    </div>
</div>

<div class="form-field">
    <label>Location:</label>
    <div>
        <input @bind="book.Location" />
    </div>
</div>

<div class="form-field">
    <label>Media Type:</label>
    <div>
        <input @bind="book.MediaType" />
    </div>
</div>

<br />

<button class="btn btn-primary"
    @onclick="SaveBook">
    Save Book
</button>


@code {
    private BookViewModel book = new();

    // Save entered book properties.
    private async void SaveBook()
    {
        var bookId = await LibraryBookService.SaveBook(book!);
        PageNavigation.NavigateTo($"statuspage/?id={bookId}");
    }
}

In the above script, we have instantiated an internal object to store a copy of the book properties before we can save it to the data store. Properties of the object are then bound to each user input using the @bind attribute:

<input @bind="book.Title" />

After we save the record, we navigate to a status page. To be able to use page navigation, we have injected the NavigationManager class to the PageNavigation object:

@inject NavigationManager PageNavigation

The page is redirected to a status page with the new book identifier as follows:

PageNavigation.NavigateTo($"statuspage/?id={bookId}");

The status page razor component, StatusPage.razor, is saved in the\BookLoanBlazorServerApp\Pages subfolder.  Its script is shown below:

@page "/statuspage"

<PageTitle>Status Page</PageTitle>

<h1>Status Page</h1>

@if (ID == 0)
{
    <p>No Book has been added.</p>
}
else
{
    <p>Book with ID : {@ID} has been created!</p>
}

@code {
    [Parameter]
    [SupplyParameterFromQuery]
    public int? ID { get; set; }
}

The above could be modified to be more general purpose for different entities and actions (data insertion, update etc.)

Next, build and run the app, then click on the Create a Book navigation link. The following page will appear:

Enter some values into each input field.

I have entered the following input values:

After clicking Save Book, the record is saved to the database.

Below is an excerpt from local values from debug breakpoints before calling the save book service method:

We can see that the input values have bound to the DTO and are ready to be saved.

In the service method SaveBook(), we can see how to identifier key for the new book is retrieved from the context using the GetValue<T>(..)  method of the CurrentValues property of the Books entity. 

The identifier is returned to the calling component: 

The status page is then displayed with the identifier for the new record:

When we navigate to the list books page, the new record is shown as the bottom of the list as shown:

The above completes the SQL data insertion operation.

I will next show how to update record within our Book table.

Updating Data with a Razor Component User Interface

Data is updated in the SQL database Book table from the following method within he BookService class:

/// <summary>
/// UpdateBook()
/// </summary>
/// <param name="Id"></param>
/// <param name="vm"></param>
/// <returns></returns>
public async Task<BookViewModel> UpdateBook(int Id, BookViewModel vm)
{
    var book = await _db.Books
        .Where(b => b.ID == Id)
        .SingleOrDefaultAsync();

    if (book != null)
    {
        string originalEdition = book.Edition!;
        book.Title = vm.Title;
        book.Author = vm.Author;
        book.Edition = vm.Edition;
        book.Genre = vm.Genre;
        book.ISBN = vm.ISBN;
        book.Location = vm.Location;
        book.YearPublished = vm.YearPublished;

        this._db.Update(book);
	    await _db.SaveChangesAsync();
    }
    return book!;
}

Following the update, a copy of the book object is returned to the calling component.

Before I show the edit component, I will update the list books component to expose a link to each record. Add the Action column and a hyperlink with description, Edit to the editbook route to each row cell as shown:

…
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Author</th>
                <th>ISBN</th>
                <th>Year Published</th>
                <th>Genre</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Title</td>
                    <td>@book.Author</td>
                    <td>@book.ISBN</td>
                    <td>@book.YearPublished</td>
                    <td>@book.Genre</td>
                    <td>
                        <a href="editbook/?id=@book.ID">Edit</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
…

The list books component when built and run will render with the Edit action showing in the Action column as shown:

Open the empty Razor component file, EditBook.razor, which is saved in the \BookLoanBlazorServerApp\Pages sub-solder. You will see the following lines of script:

<h3>EditBook</h3>

@code {

}

Now replace the above with the following script and code to edit the book data:

@page "/editbook"
@using Microsoft.AspNetCore.Components
@using BookLoanBlazorServerApp.Services
@using BookLoanBlazorServerApp.Models
@inject IBookService LibraryBookService
@inject NavigationManager PageNavigation
@inject IJSRuntime JS

<PageTitle>Edit Book</PageTitle>

<h3>Edit Book</h3>

@if (book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="form-field">
        <label>ID:</label>
        <div>
            <label>@book.ID</label>
        </div>
    </div>

    <div class="form-field">
        <label>Title:</label>
        <div>
            <input @bind="book!.Title" />
        </div>
    </div>

    <div class="form-field">
        <label>Author:</label>
        <div>
            <input @bind="book.Author" />
        </div>
    </div>

    <div class="form-field">
        <label>Year Published:</label>
        <div>
            <input @bind="book.YearPublished" />
        </div>
    </div>

    <div class="form-field">
        <label>Genre:</label>
        <div>
            <input @bind="book.Genre" />
        </div>
    </div>

    <div class="form-field">
        <label>Edition:</label>
        <div>
            <input @bind="book.Edition" />
        </div>
    </div>

    <div class="form-field">
        <label>ISBN:</label>
        <div>
            <input @bind="book.ISBN" />
        </div>
    </div>

    <div class="form-field">
        <label>Location:</label>
        <div>
            <input @bind="book.Location" />
        </div>
    </div>

    <div class="form-field">
        <label>Media Type:</label>
        <div>
            <input @bind="book.MediaType" />
        </div>
    </div>

    <br />

    <button class="btn btn-primary"
        @onclick="UpdateBook">
        Update Changes
    </button>

    <br />
}

@code {
    [Parameter]
    [SupplyParameterFromQuery]
    public int ID { get; set; }

    private BookViewModel? book;

    private string? message = ""; 

    protected override async Task OnInitializedAsync()
    {
        book = await LibraryBookService.GetBook(ID);
    }

    // Update entered book properties.
    private async void UpdateBook()
    {
        var result = await LibraryBookService.UpdateBook(ID, book!);
        if (result != null)
            message = "Record has been successfully updated!";
        else
            message = "Unable to update record.";
        await JS.InvokeVoidAsync("alert", message);
    }
}

In the above script, instead of redirecting to a page to let the user know if the record was updated, we popup a JavaScript alert to the user with a message to confirm if the record has been updated or not.

To use JavaScript within a Razor component, we injected the class IJSRuntime into the JS object as shown:

@inject IJSRuntime JS

Next, build and run the app, then click on the Edit hyperlink on a row within the List Books page. The following page loading will appear:

Then once the data has been retrieved, the input controls will display the values as shown:

Change the value of the ISBN field to 22222222:

Click on the Update Changes button.

If successful, toy will see the following alert dialog confirming successful update:

Back to the List Books page you will see the ISBN column of the record display with the amended value:

The above completes the SQL data update operation.

I will next show how to remove a record within our Book table.

Removing Data with a Razor Component User Interface

Data is deleted in the SQL database Book table from the following method within he BookService class:

public async Task<bool> DeleteBook(int id)
{
    var book = await _db.Books
        .Where(b => b.ID == id)
        .SingleOrDefaultAsync();

    if ( book != null ) {
        _db.Books.Remove(book!);
        await _db.SaveChangesAsync();
    }

    return (book != null);
}

To delete records from the same Book table, we do not need any additional component as the edit component can be extended to include deletion functionality.

I will show how.

Open the EditBook.razor component.

Add the following HTML markup for a delete button below the update button as shown:

<br />
<br />

<button class="btn btn-primary"
        @onclick="DeleteBook">
        Delete
</button>

<br />

In the @code{ … } block, we add the additional method to delete the book record:

// Delete book on confirmation.
private async void DeleteBook()
{
    try
    {
        var decision = await JS.InvokeAsync<bool>("confirm", "Are you sure you want this record removed?");
        if (decision == true)
        {
            await JS.InvokeVoidAsync("alert", "Record has been successfully removed!");

            var result = await LibraryBookService.DeleteBook(ID!);
            if (result == true)
                message = "Record has been successfully updated!";
            await JS.InvokeVoidAsync("alert", message);

            PageNavigation.NavigateTo($"listbooks");
        }
    }
    catch (System.Threading.Tasks.TaskCanceledException)
    {            
    }
}

The above deletion code includes the following JavaScript confirmation dialog to confirm deletion:

var decision = await JS.InvokeAsync<bool>("confirm", "Are you sure you want this record removed?");

When the JavaScript confirm dialog returns True, then the deletion block is executed, else no deletion occurs.

Next, build and run the app, then click on the Edit hyperlink on a row within the List Books page.

The following page loading will appear (after the loading is rendered) with the Delete button at the bottom of the form:

After clicking on the Delete action, you will see the confirmation dialog:

If the Ok action is selected, the alert dialog will display confirming record deletion:

The above overview has shown us how to integrate SQL data and implement data manipulation operations within an ASP.NET Core Blazor server application.

In future posts I will show how to enhance the application with data and user interface validations and improve the user experience with drop downs and dialogs.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial