Last updated on April 16th, 2023
Estimated reading time: 15 minutes
Blazor is a framework for building interactive client-side web UI with .NET. In this article we will create a fully functional Blazor site with Add/Edit/Delete using Blazor web assembly ,.NET 6 and Entity framework.
- Create rich interactive UIs using C# instead of JavaScript
- Share server-side and client-side app logic written in .NET.
- Render the UI as HTML and CSS for wide browser support, including mobile browsers.
- Integrate with modern hosting platforms, such as Docker.
- Build hybrid desktop and mobile apps with .NET and Blazor.
Prerequisites: Basic working knowledge of .NET Core API, Entity Framework and SQL Server.
Creating new Blazor web assembly project .NET Core hosted
Launch Visual Studio 2019 or higher and create new Project and select Blazor Project Template.
Check ASP.NET Core hosted check box and select .NET framework version 6. Default project structure would be created as below
Employee.Client project
This is a Blazor Web Assembly Project and all the client side code will reside in this project.
This will include the below items.
- Blazor components
- Razor pages
- Client side css libraries and Master Layout
- Services (Services are used to call .NET Core Api)
EmployeePortal.Server
This is the ASP.NET Core project. This project would have 3 major components.
- .NET Core API
- Data Access/Repository Layer
- Data Models
EmployeePortal.Shared
This Project will has all required data models and it will be shared by our client and server projects.
Data Access Layer and Services Set Up
Install EntityframeworkCore nuget packages as shown below.
Create a new Employee class in shared project. This is our Data Model.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EmployeePortal.Shared
{
[Table("Employee")]
public partial class Employee
{
public int EmployeeId { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name is too long.")]
public string FirstName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "Last name is too long.")]
public string LastName { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
public string Street { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string PhoneNumber { get; set; }
[StringLength(1000, ErrorMessage = "Comment length can't exceed 1000 characters.")]
public string Comment { get; set; }
}
}
Create a new Database “Hrms” in SQL Server. Create a new Employee Table as per the below schema.
CREATE TABLE [dbo].[Employee](
[EmployeeId] [int] IDENTITY(1,1) NOT NULL,
[FirstName] [varchar](256) NULL,
[LastName] [varchar](256) NULL,
[Email] [varchar](256) NULL,
[Street] [varchar](256) NULL,
[Zip] [varchar](256) NULL,
[City] [varchar](256) NULL,
[PhoneNumber] [varchar](256) NULL,
[Comment] [varchar](256) NULL
) ON [PRIMARY]
GO
Configure SQL Server connection
Under app settings in EmployeePortal.Server project configure to connect to SQL Server
{
"ConnectionStrings": {
"DefaultConnection": "Server=””;Database=”Hrms”;user="
username ";password="
password ";MultipleActiveResultSets=true;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
Employee Repository Set up
Register SQL connection and Employee Repository in Startup.cs file.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Linq;
using EmployeePortal.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.SqlServer;
using GemBox.Spreadsheet;
using ResourceDemandOpens.Client.Services;
namespace EmployeePortal.Server
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
services.AddRazorPages();
services.AddScoped<IEmployeeRepository, EmployeeRepository>();
services.AddScoped<ToastService>();
SpreadsheetInfo.SetLicense("FREE-LIMITED-KEY");
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
}
Employee Interface
The Interface contains method definitions of add/edit/get and delete employee data.
public interface IEmployeeRepository {
IEnumerable < Employee > GetAllEmployees();
Employee AddEmployee(Employee employee);
Employee GetEmployeeById(int employeeId);
Employee UpdateEmployee(Employee employee);
void DeleteEmployee(int employeeId);
}
Appdbcontext
public class AppDbContext: DbContext {
public AppDbContext(DbContextOptions < AppDbContext > options): base(options) {}
public DbSet < Employee > Employees {
get;
set;
}
}
Database Repository layer is ready to be consumed by .NET Core API
.NET CORE API
Add new Employee Controller in EmployeePortal.Server Project.The controller has the below-mentioned API methods.
GetAllEmployees():Get method to get all Employess from DB.
CreateEmployee(); Post method takes employee object and validates model before saving final data in database.
GetEmployeeById:Get method to get specfic Employee.This method required EmployeeID as parameter.
UpdateEmployee:Put mehod use to update specific Employee. This method required EmployeeID as parameter.
DeleteEmployee:Delete method use to delete Employee.This method required EmployeeID as parameter.
[Route("api/[controller]")]
[ApiController]
public class EmployeeController: Controller {
private readonly IEmployeeRepository _employeeRepository;
public EmployeeController(IEmployeeRepository employeeRepository) {
_employeeRepository = employeeRepository;
}
[HttpGet]
public IActionResult GetAllEmployees() {
return Ok(_employeeRepository.GetAllEmployees());
}
[HttpPost]
public IActionResult CreateEmployee([FromBody] Employee employee) {
if (employee == null) return BadRequest();
if (employee.FirstName == string.Empty || employee.LastName == string.Empty) {
ModelState.AddModelError("Name/FirstName", "The name or first name shouldn't be empty");
}
if (!ModelState.IsValid) return BadRequest(ModelState);
var createdEmployee = _employeeRepository.AddEmployee(employee);
return Created("employee", createdEmployee);
}
[HttpGet("{employeeId}")]
public IActionResult GetEmployeeById(int employeeId) {
return Ok(_employeeRepository.GetEmployeeById(employeeId));
}
[HttpPut]
public IActionResult UpdateEmployee([FromBody] Employee employee) {
if (employee == null) return BadRequest();
if (employee.FirstName == string.Empty || employee.LastName == string.Empty) {
ModelState.AddModelError("Name/FirstName", "The name or first name shouldn't be empty");
}
if (!ModelState.IsValid) return BadRequest(ModelState);
var employeeToUpdate = _employeeRepository.GetEmployeeById(employee.EmployeeId);
if (employeeToUpdate == null) return NotFound();
_employeeRepository.UpdateEmployee(employee);
return NoContent(); //success
}
[HttpDelete("{id}")]
public IActionResult DeleteEmployee(int id) {
if (id == 0) return BadRequest();
var employeeToDelete = _employeeRepository.GetEmployeeById(id);
if (employeeToDelete == null) return NotFound();
_employeeRepository.DeleteEmployee(id);
return NoContent(); //success
}
}
API layer is ready to be consumed by Blazor Client project.
Create and register data service in Blazor to consume .NET Core API
Create a new folder service in EmployeePortal.Client and add IEmployeeDataService.This will contain a definition of all the methods to be used in EmployeeDataService.
public interface IEmployeeDataService {
Task < IEnumerable < Employee >> GetAllEmployees();
Task < Employee > AddEmployee(Employee employee);
Task < Employee > GetEmployeeDetails(int employeeId);
Task UpdateEmployee(Employee employee);
Task DeleteEmployee(int employeeId);
}
public class EmployeeDataService: IEmployeeDataService {
private readonly HttpClient _httpClient;
public EmployeeDataService(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task < IEnumerable < Employee >> GetAllEmployees() {
return await JsonSerializer.DeserializeAsync < IEnumerable < Employee >> (await _httpClient.GetStreamAsync($ "api/employee"), new JsonSerializerOptions() {
PropertyNameCaseInsensitive = true
});
}
public async Task < Employee > AddEmployee(Employee employee) {
var employeeJson = new StringContent(JsonSerializer.Serialize(employee), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($ "api/employee", employeeJson);
if (response.IsSuccessStatusCode) {
return await JsonSerializer.DeserializeAsync < Employee > (await response.Content.ReadAsStreamAsync());
}
return null;
}
public async Task < Employee > GetEmployeeDetails(int employeeId) {
return await JsonSerializer.DeserializeAsync < Employee > (await _httpClient.GetStreamAsync($ "api/employee/{employeeId}"), new JsonSerializerOptions() {
PropertyNameCaseInsensitive = true
});
}
public async Task UpdateEmployee(Employee employee) {
var employeeJson = new StringContent(JsonSerializer.Serialize(employee), Encoding.UTF8, "application/json");
await _httpClient.PutAsync("api/employee", employeeJson);
}
public async Task DeleteEmployee(int employeeId) {
await _httpClient.DeleteAsync($ "api/employee/{employeeId}");
}
}
Register EmployeeDataService and IEmployeeDataService in program.cs of cllient app
builder.Services.AddHttpClient<IEmployeeDataService, EmployeeDataService>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
Creating Blazor razor pages and data binding
Add a new Razor component and EmployeePage class inside the pages folder.
public partial class EmployeePage: ComponentBase {
public IEnumerable < EmployeePortal.Shared.Employee > Employees {
get;
set;
}
[Inject]
public IEmployeeDataService EmployeeDataService {
get;
set;
}
public AddEmployeeDialog AddEmployeeDialog {
get;
set;
}
protected async override Task OnInitializedAsync() {
Employees = (await EmployeeDataService.GetAllEmployees()).ToList();
}
protected void QuickAddEmployee() {
AddEmployeeDialog.Show();
}
public async void AddEmployeeDialog_OnDialogClose() {
Employees = (await EmployeeDataService.GetAllEmployees()).ToList();
StateHasChanged();
}
}
Employee page class will inherit from the Component base.
OnInitializedAsync will be called when the componenet is been Initialized and we would call GetAllEmployees at this point in time.
IEmployeeDataService needs to be injected to call the GetAllEmployees method.
@page "/"
@using EmployeePortal.Client.Components;
<h1 class="page-title">All employees</h1>
@if (Employees == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Employee ID</th>
<th>First name</th>
<th>Last name</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var employee in Employees)
{
<tr>
<td>@employee.EmployeeId</td>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>
<a href="@($"detail/{employee.EmployeeId}")" class="btn btn-primary-details table-btn">
<i class="fas fa-info-circle"></i>
Details
</a>
<a Edit href="@($"edit/{employee.EmployeeId}")" class="btn btn-primary-edit table-btn">
<i class="fas fa-edit"></i>
Edit
</a>
</td>
</tr>
}
</tbody>
</table>
}
<button @onclick="QuickAddEmployee" class="btn btn-dark table-btn quick-add-btn"> + </button>
<AddEmployeeDialog @ref="AddEmployeeDialog" CloseEventCallback="@AddEmployeeDialog_OnDialogClose"></AddEmployeeDialog>
@page “/” directive specifies this is the default page when blazor application is loaded.
Import Employee models from shared EmployeePortal.Shared
Since we are using aync call to load employee data, we need to check if the Employees object is null before binding.
Add a new class for Detail.
public partial class Detail {
[Parameter]
public string EmployeeId {
get;
set;
}
public EmployeePortal.Shared.Employee Employee {
get;
set;
}
[Inject]
public NavigationManager NavigationManager {
get;
set;
}
[Inject]
public IEmployeeDataService EmployeeDataService {
get;
set;
}
protected async override Task OnInitializedAsync() {
Employee = await EmployeeDataService.GetEmployeeDetails(int.Parse(EmployeeId));
}
protected void NavigateToOverview() {
NavigationManager.NavigateTo("/");
}
}
Add a new razor component for Detail.razor.
@page "/detail/{EmployeeId}"
@if (@Employee == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<section class="employee-detail">
<h1 class="page-title">Details for @Employee.FirstName @Employee.LastName</h1>
<div class="col-12 row">
<div class="col-10 row">
<div class="col-xs-12 col-sm-8">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Employee ID</label>
<div class="col-sm-8">
<label type="text" class="form-control-plaintext">@Employee.EmployeeId</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">First name</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.FirstName</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Last name</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.LastName</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Email</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.Email</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Street</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.Street</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Zip</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.Zip</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">City</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.City</label>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Phone number</label>
<div class="col-sm-8">
<label type="text" readonly class="form-control-plaintext">@Employee.PhoneNumber</label>
</div>
</div>
</div>
</div>
</div>
<a class="btn btn-outline-primary" @onclick="@NavigateToOverview">Back to overview</a>
</section>
}
Create a new Edit class.
public partial class Edit {
[Inject]
public IEmployeeDataService EmployeeDataService {
get;
set;
}
[Parameter]
public string EmployeeId {
get;
set;
}
[Inject]
public NavigationManager NavigationManager {
get;
set;
}
public EmployeePortal.Shared.Employee Employee {
get;
set;
}
//used to store state of screen
protected string Message = string.Empty;
protected string StatusClass = string.Empty;
protected bool Saved;
protected override async Task OnInitializedAsync() {
Saved = false;
int.TryParse(EmployeeId, out
var employeeId);
if (employeeId == 0) //new employee is being created
{
Employee = new Employee {};
} else {
Employee = await EmployeeDataService.GetEmployeeDetails(int.Parse(EmployeeId));
}
}
protected async Task HandleValidSubmit() {
Saved = false;
if (Employee.EmployeeId == 0) {
var addedEmployee = await EmployeeDataService.AddEmployee(Employee);
if (addedEmployee != null) {
StatusClass = "alert-success";
Message = "New employee added successfully.";
Saved = true;
} else {
StatusClass = "alert-danger";
Message = "Something went wrong adding the new employee. Please try again.";
Saved = false;
}
} else {
await EmployeeDataService.UpdateEmployee(Employee);
StatusClass = "alert-success";
Message = "Employee updated successfully.";
Saved = true;
}
}
protected void HandleInvalidSubmit() {
StatusClass = "alert-danger";
Message = "There are some validation errors. Please try again.";
}
protected async Task DeleteEmployee() {
await EmployeeDataService.DeleteEmployee(Employee.EmployeeId);
StatusClass = "alert-success";
Message = "Deleted successfully";
Saved = true;
}
protected void NavigateToOverview() {
NavigationManager.NavigateTo("/");
}
}
Add a new razor component Edit.razor
@page "/edit/{EmployeeId}"
@using EmployeePortal.Shared;
@if (Employee == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
@if (!Saved)
{
<section class="employee-edit">
<h1 class="page-title">Details for @Employee.FirstName @Employee.LastName</h1>
<EditForm Model="@Employee" OnValidSubmit="@HandleValidSubmit"
OnInvalidSubmit="@HandleInvalidSubmit">
<DataAnnotationsValidator />
<ValidationSummary></ValidationSummary>
<div class="form-group row">
<label for="firstName" class="col-sm-3">First name: </label>
<InputText id="firstName" class="form-control col-sm-8" @bind-Value="@Employee.FirstName" placeholder="Enter first name"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.FirstName)" />
</div>
<div class="form-group row">
<label for="lastName" class="col-sm-3">Last name: </label>
<InputText id="lastName" class="form-control col-sm-8" @bind-Value="@Employee.LastName" placeholder="Enter last name"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.LastName)" />
</div>
<div class="form-group row">
<label for="email" class="col-sm-3">Email: </label>
<InputText id="email" class="form-control col-sm-8" @bind-Value="@Employee.Email" placeholder="Enter email"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.Email)" />
</div>
<div class="form-group row">
<label for="street" class="col-sm-3">Street: </label>
<InputText id="street" class="form-control col-sm-8" @bind-Value="@Employee.Street" placeholder="Enter street"></InputText>
</div>
<div class="form-group row">
<label for="zip" class="col-sm-3">Zip code: </label>
<InputText id="zip" class="form-control col-sm-8" @bind-Value="@Employee.Zip" placeholder="Enter zip code"></InputText>
</div>
<div class="form-group row">
<label for="city" class="col-sm-3">City: </label>
<InputText id="city" class="form-control col-sm-8" @bind-Value="@Employee.City" placeholder="Enter city"></InputText>
</div>
<div class="form-group row">
<label for="phonenumber" class="col-sm-3">Phone number: </label>
<InputText id="phonenumber" class="form-control col-sm-8" @bind-Value="@Employee.PhoneNumber" placeholder="Enter phone number"></InputText>
</div>
<div class="form-group row">
<label for="comment" class="col-sm-3">Comment: </label>
<InputTextArea id="comment" class="form-control col-sm-8" @bind-Value="@Employee.Comment" placeholder="Enter comment"></InputTextArea>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.Comment)" />
</div>
<button type="submit" class="btn btn-primary edit-btn">Save employee</button>
<a class="btn btn-danger" @onclick="@DeleteEmployee">
Delete
</a>
<a class="btn btn-outline-primary" @onclick="@NavigateToOverview">Back to overview</a>
</EditForm>
</section>
}
else
{
<div class="alert @StatusClass">@Message</div>
}
}
Run the application on your local machine.You will able to see a list of employees and also able to Edit and Delete using our UI.