CRUD using Blazor and Entity Framework Core

CRUD using Blazor and Entity Framework Core

In my last post (Get Started with Blazor), we discussed the Blazor Framework, Hosting models and how to set up authentication and authorization in a Blazor server-side application with an example.  In this post, I am going to explain CRUD using Blazor and Entity Framework Core.  I will use the sample application we have created in the previous post and extend it with the new functionalities we are going to talk about in this post.  You will be familiar with the following topics after you read this post.

  • Entity Framework Core – Setup in Blazor application
  • Using Bootstrap Modals
  • Child Components
  • Validation
  • JavaScript Interop
  • Communication Between Components
  • Templated Components using Render Fragments (Dynamic Content)

Okay, Let’s get started.

Prerequisites

  • Visual Studio 2019
  • Install .NET Core 3.0
  • Install Blazor Templates

Entity Framework Core – Setup

To begin with, make sure you have the following NuGet packages installed in your project.

EF Core NuGet Packages
(Package versions in the image were latest at the time of writing this post)

For the purpose of this post, I create a model called ToDo.cs and we will use this model for CRUD operations.

// ToDo.cs

public class ToDo
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string Status { get; set; }

    [Required]
    public DateTime DueDate { get; set; }
}

Add the entity in the ApplicationDbContext.cs

// ApplicationDbContext.cs

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
    public DbSet<ToDo> ToDoList { get; set; }

    public override int SaveChanges()
    {
        return base.SaveChanges();
    }
}

Now we have added the entity and made required changes in dbcontext.  Add the migration and create the table in the database through the package manager console.

Package Manager Console

Data access service

We have created the table in the database.  Create a service to access the table and perform CRUD operations.  This service implements from an interface and the interface is configured in the startup for dependency injection.

// ToDoListService.cs

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using BlazorApp.Data;

namespace BlazorApp.Services
{
    public interface IToDoListService
    {
        Task<List<ToDo>> Get();
        Task<ToDo> Get(int id);
        Task<ToDo> Add(ToDo toDo);
        Task<ToDo> Update(ToDo toDo);
        Task<ToDo> Delete(int id);
    }
    public class ToDoListService : IToDoListService
    {
        private readonly ApplicationDbContext _context;

        public ToDoListService(ApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<List<ToDo>> Get()
        {
            return await _context.ToDoList.ToListAsync();
        }

        public async Task<ToDo> Get(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);
            return toDo;
        }

        public async Task<ToDo> Add(ToDo toDo)
        {
            _context.ToDoList.Add(toDo);
            await _context.SaveChangesAsync();
            return toDo;
        }

        public async Task<ToDo> Update(ToDo toDo)
        {
            _context.Entry(toDo).State = EntityState.Modified;
            await _context.SaveChangesAsync();
            return toDo;
        }

        public async Task<ToDo> Delete(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);
            _context.ToDoList.Remove(toDo);
            await _context.SaveChangesAsync();
            return toDo;
        }
    }
}

Okay, the data access part is now completed.  Make sure, you have added the following code at the end of ConfigureServices method in startup.cs.  This is to inject ToDoListService.

// Startup.cs

services.AddTransient<IToDoListService, ToDoListService>();

Details Page

Let’s create a page to list the records from the database.  I have copied the default FetchData.razor and made changes to show the sample records populated in the To-Do List table from the database.  NavMenu.razor has been changed to show the “To-Do List” link.

// ToDoList.razor

@page "/todolist"

@using BlazorApp.Data
@using BlazorApp.Services
@inject IToDoListService service

<h1>To Do List</h1>

<p>This component demonstrates fetching data from Database.</p>

@if (toDoList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Task</th>
                <th>Status</th>
                <th>Due Date</th>
                <th>Edit</th>
                <th>Delete</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var toDoItem in toDoList)
            {
                <tr>
                    <td>@toDoItem.Name</td>
                    <td>@toDoItem.Status</td>
                    <td>@toDoItem.DueDate.ToShortDateString()</td>
                    <td><input type="button" class="btn btn-primary" value="Edit" /></td>
                    <td><input type="button" class="btn btn-danger" value="Delete" /></td>
                </tr>
            }
        </tbody>
    </table>
}
<div>
    <input type="button" data-toggle="modal" data-target="#taskModal" class="btn btn-primary" value="Add Task" />
</div>


@code {
        List<ToDo> toDoList;

    protected override async Task OnInitializedAsync()
    {
        toDoList = await service.Get();
    }
}

This code is very similar to FetchData.razor except this is fetching data from the database using Entity Framework Core.  The service IToDoListService is injected at the top.

Now we have to call the service to fetch the data.  The right place to make a service call is inside OnInitializedAsync.  It is one of the Blazor Lifecycle methods.  It is executed when the component is completely loaded.  You can use this method to load data by calling services because the control rendering will happen after this method.  I have also added the code to display buttons for CRUD operations but not wired with any code as of now.  When you run the application, you will see a page similar to this.

CRUD - Retrieve Operation

Add Bootstrap

To use bootstrap modal dialog, I have added jQuery and Bootstrap libraries using “Add Client-side Library”.  You can find the option by right-clicking your project then Add -> Client-side Library

Library Manager

Include the following lines inside the body of _Host.cshtml

// _Host.cshtml

<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script>

Child Components

Before we proceed with the CRUD implementations, we need to know about child components.  Blazor apps are based on components.  Components are reusable building blocks, it can be an individual control or a block with multiple controls.  These component classes are written in razor markup.

Components can include other components.  You can add a component inside others using the component name in an HTML syntax. We will use this concept to create bootstrap modals as child components for Add/Edit and Confirmation dialogs.

Add Task

The next step is to create a razor component to accept the input from the user for a new To-Do item. I have created a razor component named TaskDetail.razor.

// TaskDetail.razor

@using BlazorAppp.Data
@using BlazorAppp.Services
@inject IToDoListService service

<div class="modal" tabindex="-1" role="dialog" id="taskModal">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Task Detail</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <EditForm Model="@TaskObject" OnValidSubmit="@HandleValidSubmit">
                    <div class="form-group">
                        <label for="taskName">Task Name</label>
                        <input type="hidden" @bind-value="@TaskObject.Id" />
                        <InputText id="name" class="form-control" @bind-Value="@TaskObject.Name" />
                    </div>
                    <div class="form-group">
                        <label for="status">Status</label>
                        <InputSelect id="Summary" class="form-control"
                                     @bind-Value="TaskObject.Status">
                            <option value="">Select</option>
                            @foreach (var status in TaskStatusList)
                            {
                                <option value="@status">
                                    @status
                                </option>
                            }
                        </InputSelect>
                    </div>
                    <div class="form-group">
                        <label for="dueDate">Due Date</label>
                        <input type="date" id="addition" name="math" @bind-value="@TaskObject.DueDate" />
                    </div>
                    <button type="submit" class="btn btn-primary">Submit</button>
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                </EditForm>
            </div>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public ToDo TaskObject { get; set; }

    List<string> TaskStatusList = new List<string>() { "New", "In Progress", "Completed" };

    private async void HandleValidSubmit()
    {

    }
}

In the above code, we have a form defined using the EditForm component.  You can see the EditForm has a model that is passed from the parent component.  The properties of the model are bind to the input controls using bind-valueHandleValidSubmit is triggered when the form successfully submits.

I have also declared the TaskDetail Component inside ToDoList component and pass an empty ToDo object.  Find below the ToDoList component with changes.

// ToDoList.razor

@page "/todolist"

@using BlazorAppp.Data
@using BlazorAppp.Services
@inject IToDoListService service

<h1>To Do List</h1>

<p>This component demonstrates fetching data from Database.</p>

// Code omitted for brevity

<div>
    <input type="button" data-toggle="modal" data-target="#taskModal" class="btn btn-primary" value="Add Task" @onclick="(() => InitializeTaskObject())" />
</div>

<TaskDetail TaskObject=taskObject></TaskDetail>

@code {
        List<ToDo> toDoList;
        ToDo taskObject = new ToDo();

    protected override async Task OnInitializedAsync()
    {
        toDoList = await service.Get();
    }
    private void InitializeTaskObject()
    {
        taskObject = new ToDo();
        taskObject.DueDate = DateTime.Now;
    }
}

When you click on the “Add New” button, you will see a modal dialog similar to this.

Add New Task Dialog

What are the next steps?

  • Validate the user input
  • Save the data to the database
  • Refresh the data in the page to show the new record

Validation

We will use data annotations in the model to validate the user input.  I have changed the model to have custom validation messages

// ToDo.cs

public class ToDo
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "Task name is required")]
    [StringLength(15, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Status is required")]
    public string Status { get; set; }

    [Required(ErrorMessage = "Due Date is required")]
    public DateTime DueDate { get; set; }
}

We also need to add DataAnnotationsValidator component which attaches validation support using data annotations.  To display the validation messges we use ValidationSummary component.  Both the components are added to TaskDetail.razor inside the EditForm component.

// ToDoList.razor

<DataAnnotationsValidator />
<ValidationSummary />

With these changes, if I run the application and try to submit an invalid form, I will get an error screen similar to the one below.

Blazor Validation Errors

You can see the validation errors in the Validation Summary section.  If you want to show the validation message next to each control instead of a summary, remove the ValidationSummary component and have the following pattern of code next to each input element.

// TaskDetail.razor

<ValidationMessage For="@(() => TaskObject.Name)" />

With this, the validation errors will be displayed next to the respective control similar to this.

Blazor Validation Errors Adjacent

Save Data

Now we received the data from the user and validated it.  Let’s save the data to the database.  As I mentioned earlier, HandleValidSubmit is triggered when the form successfully submits after it passes the validation.  We have to add the “save” logic in the method.

// TaskDetail.razor

private async void HandleValidSubmit()
{
    await service.Add(TaskObject);
}

JavaScript Interop

The data is saved to the database but the modal dialog is still open.  We need to call JavaScript code from .NET code to close the dialog.  To call JavaScript function from C#, use the IJSRuntime abstraction.  The InvokeAsync<T> method takes an identifier for the JavaScript function that you wish to invoke along with any number of JSON-serializable arguments.

First, we have to create a JavaScript method to close the bootstrap dialog by getting the id of the dialog.  The second is to inject the IJSRuntime and  lastly use the injected object to issue JavaScript Interop calls.  The following JavaScript code is added to the _Host.cshtml.

// _Host.cshtml

<script>
    function ShowModal(modalId) {
        $('#'+modalId).modal('show');
    }
    function CloseModal(modalId) {
        $('#'+modalId).modal('hide');
    }
</script>

After the changes, TaskDetail.razor looks like this

// TaskDetail.razor

@using BlazorAppp.Data
@using BlazorAppp.Services
@inject IToDoListService service
@inject IJSRuntime jsRuntime

// Code omitted for brevity

@code {
    [Parameter]
    public ToDo TaskObject { get; set; }

    List<string> TaskStatusList = new List<string>() { "New", "In Progress", "Completed" };

    private async Task CloseTaskModal()
    {
        await jsRuntime.InvokeAsync<object>("CloseModal", "taskModal");
    }

    private async void HandleValidSubmit()
    {
        await service.Add(TaskObject);
        await CloseTaskModal();
    }
}

Communication Between Components

Now the data is saved and the modal is closed, but I cannot see the newly added item in the grid.  I can see the data only if I refresh the browser.  I have to tell the parent component to refresh itself to display the new set of data.  Components can offer callbacks that parent components can use to react on events raised by their child components.  Let’s see how to implement this.  I have declared an action DataChanged and invoke it in the HandleValidSubmit method of TaskDetail.razor

// TaskDetail.razor

@code {
    [Parameter]
    public ToDo TaskObject { get; set; }

    [Parameter]
    public Action DataChanged { get; set; }

    List<string> TaskStatusList = new List<string>() { "New", "In Progress", "Completed" };

    private async Task CloseTaskModal()
    {
        await jsRuntime.InvokeAsync<object>("CloseModal", "taskModal");
    }

    private async void HandleValidSubmit()
    {
        await service.Add(TaskObject);
        await CloseTaskModal();
        DataChanged?.Invoke(); 
    }

}

The parent component ToDoList.razor can handle the DataChanged event like this

// ToDoList.razor

// Code omitted for brevity
<TaskDetail TaskObject=taskObject DataChanged="@DataChanged">

</TaskDetail>

@code {
        List<ToDo> toDoList;
        ToDo taskObject = new ToDo();

    protected override async Task OnInitializedAsync()
    {
        toDoList = await service.Get();
    }
    private void InitializeTaskObject()
    {
        taskObject = new ToDo();
        taskObject.DueDate = DateTime.Now;
    }

    private async void DataChanged()
    {
        toDoList = await service.Get();
        StateHasChanged();
    }
}

With this, we can add a new record after validation, close the dialog upon save and refresh the parent component.

Edit task

Let’s see how to reuse the child component and the ToDoListService to edit an existing record.  Okay, what are all the changes we need to make?

First, we need to wire up the “Edit” button to open the child component TaskDetail.razor.  It also has to pass the selected task detail to the child component.  I have modified the edit button to open the modal and call a method to set the selected object.

// ToDoList.razor

// Code omitted for brevity
<td><input type="button" class="btn btn-primary" @onclick="(() => PrepareForEdit(toDoItem))" data-toggle="modal" data-target="#taskModal" value="Edit" /></td>

// ...

@code {
        List<ToDo> toDoList;
        ToDo taskObject = new ToDo();
//...  

    private void PrepareForEdit(ToDo task)
    {
        taskObject = task;
    }
}

The validations we have implemented to add new task will work automatically as we are using the same child component for Edit as well.  Once the user modified the data and click save, HandleValidSubmit will be triggered.  We have to distinguish between a new record and an existing record to make the appropriate service call.  I have used the Id property to differentiate the records.

// TaskDetail.razor

private async void HandleValidSubmit()
{
    if (TaskObject.Id == 0)
    {
        await service.Add(TaskObject);
    }
    else
    {
        await service.Update(TaskObject);
    }
    await CloseTaskModal();
    DataChanged?.Invoke(); 
}

Render Fragments – Dynamic Content

As of now, there is no difference between Add and Edit dialogs.  The captions and the controls in both the dialogues are the same as we are resuing the dialog.  The next requirement is to show some visual difference between the dialogs but the reusability of the child component should continue.  A render fragment represents a segment of UI to render.  We can pass dynamic content within component tag elements and can be rendered in child components using RenderFragment property.  Okay, how to achieve that?

First, declare the RenderFragement property in the child component and then substitute the title of the modal dialog with the property.

// TaskDetail.razor

//...
<h5 class="modal-title">@CustomHeader</h5>

//...
@code {
    //..

    [Parameter]
    public RenderFragment CustomHeader { get; set; }

    //..
}

Now in the parent component, we pass the dynamic content within the child component tags.  The dynamic content value is set in the add and edit methods.  I just pass the inner text as a fragment here, you can pass an HTML element or a nested HTML element.

// ToDoList.razor

//..
<TaskDetail TaskObject=taskObject DataChanged="@DataChanged">
    <CustomHeader>@customHeader</CustomHeader>
</TaskDetail>

@code {
        List<ToDo> toDoList;
        ToDo taskObject = new ToDo();
    string customHeader = string.Empty;

    //..

    private void InitializeTaskObject()
    {
        taskObject = new ToDo();
        taskObject.DueDate = DateTime.Now;
        customHeader = "Add New Task";
    }

    private void PrepareForEdit(ToDo task)
    {
        customHeader = "Edit Task";
        taskObject = task;
    }

    // ..
}

See how the caption of the dialog changed between add and edit dialogs.

After Render Fragment

Delete Task

We have implemented add and edit functionalities with validation.  Let’s start work on delete now.  Though we cannot reuse the child component we already created, we can still use the JavaScript Interop. First, we need to wire up the delete button with some event.  That event will set the selected task object and pass the information to the child component.  Upon confirmation from the child component for delete, we need to delete the record and refresh the task list.

//ToDoList.razor

// Code omitted for brevity
<td><input type="button" class="btn btn-danger" @onclick="(() => PrepareForDelete(toDoItem))" data-toggle="modal" data-target="#confirmDeleteModal" value="Delete" /></td>

//..
<ConfirmDialog OnClick="@Delete" />
<TaskDetail TaskObject=taskObject DataChanged="@DataChanged">
    <CustomHeader>@customHeader</CustomHeader>
</TaskDetail>

@code {
    List<ToDo> toDoList;
    ToDo taskObject = new ToDo();
    string customHeader = string.Empty;

    //..

    private void PrepareForDelete(ToDo task)
    {
        taskObject = task;
    }

    private async Task Delete()
    {
        var task = await service.Delete(taskObject.Id);
        await jsRuntime.InvokeAsync<object>("CloseModal", "confirmDeleteModal");
        toDoList = await service.Get();
        taskObject = new ToDo();
    }
}

Next, we need to create a child component (ConfirmDialog.razor) to get the confirmation from the user on the Delete button click.  This is again a bootstrap modal dialog.

<div class="modal" tabindex="-1" role="dialog" id="confirmDeleteModal">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Confirmation</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <p>Do you want to delete the record?</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary" @onclick="OnClick">Yes</button>
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
            </div>
        </div>
    </div>
</div>

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

    [Parameter]
    public EventCallback<MouseEventArgs> OnClick { get; set; }
}

Summary

This post tried to explain about using Entity Framework Core with Blazor and implement CRUD functionalities.  We also discussed the usage of Bootstrap, Validations using Data Annotations and Creation of Dynamic content using RenderFragment.

The full implementation of this post will be available in Github

Further reading

The following are some of the links you can refer if you want to learn more about Blazor.

Leave a Reply

Close Menu
Share via
Copy link
Powered by Social Snap