Saturday, November 30, 2019

Create a Single Page App with Blazor Server and Entity Framework Core 3.0

Blazor is a new framework built by Microsoft for creating interactive client-side web UI with .NET codebase. We can write both client-side and server-side code in C#.NET itself. I have already written an article about Blazor on C# Corner. Please refer to below article for more basics about Blazor framework.
  • Create A Simple Blazor Server Application With .NET Core 3.0

We will create a simple Employee app using Blazor. In this app, we can add, edit, read and delete employee information. We will use entity framework core 3.0 to store and retrieve data into SQL database.
 

Create Blazor app in Visual Studio 2019

 
Visual Studio latest version is shipped with .NET Core 3.0 SDK. We can choose Blazor template and create a new Blazor project.
 
 
 
We are going to use entity framework core and data migration commands to create SQL database and table. We must install “Microsoft.EntityFrameworkCore.SqlServer” and “Microsoft.EntityFrameworkCore.Tools” libraries to perform these actions.
 
 
 
 
 
Currently, Blazor don’t support Json methods like “GetJsonAsync”, “PostJsonAsync” and “PutJsonAsync” in HttpClient class. We will create a custom HttpClient class to add these methods. Hence, we must install “Newtonsoft.Json” library also to the project.
 
 
 
We can create an Employee class and SqlDbContext classes.
 
Employee.cs
  1. namespace BlazorEmployeeEFCore.Data  
  2. {  
  3.     public class Employee  
  4.     {  
  5.         public string Id { getset; }  
  6.         public string Name { getset; }  
  7.         public string Department { getset; }  
  8.         public string Designation { getset; }  
  9.         public string Company { getset; }  
  10.     }  
  11. }  
SqlDbContext.cs
  1. using Microsoft.EntityFrameworkCore;  
  2.   
  3. namespace BlazorEmployeeEFCore.Data  
  4. {  
  5.     public class SqlDbContext : DbContext  
  6.     {  
  7.         public SqlDbContext(DbContextOptions<SqlDbContext> options)  
  8.            : base(options)  
  9.         {  
  10.         }  
  11.         public DbSet<Employee> Employees { getset; }  
  12.     }  
  13. }  
SqlDbContext class inherits DbContext class and contains an Employee property to perform all SQL related operations.
 
We can add SQL connection string in the appsettings.json file.
 
 
 
We must register the SqlDbContext class in the startup class as well. 
 
I have enabled the detailed error option also for Blazor application. So that, we can see the detailed error messages in the developer console, if any occurs.
 

Create SQL database and table using DB migration

 
We are ready for creating SQL database and table using DB migration with code first approach. We have already added “Microsoft.EntityFrameworkCore.Tools” library for this purpose.
 
Open “Package Manager Console” from Tools menu and use below command to create migration script.
 
add-migration Initial
 
This command will create a new migration script class file under a new “Migrations” folder.
 
We can use below command to execute the migration script class.
 
update-database
 
We have used the default local SQL server available with Visual Studio. If you check with SQL server object explorer, you can see a new database and a table created in local server after DB migration.
 
We can create an Employees controller for Web API service. We will consume the web methods from this controller in our Blazor components for CRUD operations later.
 
EmployeesController.cs
  1. using Microsoft.AspNetCore.Mvc;  
  2. using Microsoft.EntityFrameworkCore;  
  3. using System;  
  4. using System.Collections.Generic;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace BlazorEmployeeEFCore.Data  
  8. {  
  9.     [Route("api/[controller]")]  
  10.     [ApiController]  
  11.     public class EmployeesController : ControllerBase  
  12.     {  
  13.         private readonly SqlDbContext _dbContext;  
  14.   
  15.         public EmployeesController(SqlDbContext dbContext)  
  16.         {  
  17.             _dbContext = dbContext;  
  18.         }  
  19.   
  20.         [HttpGet]  
  21.         [Route("Get")]  
  22.         public async Task<List<Employee>> Get()  
  23.         {  
  24.             return await _dbContext.Employees.ToListAsync();  
  25.         }  
  26.   
  27.         [HttpPost]  
  28.         [Route("Create")]  
  29.         public async Task<bool> Create([FromBody]Employee employee)  
  30.         {  
  31.             if (ModelState.IsValid)  
  32.             {  
  33.                 employee.Id = Guid.NewGuid().ToString();  
  34.                 _dbContext.Add(employee);  
  35.                 try  
  36.                 {  
  37.                     await _dbContext.SaveChangesAsync();  
  38.                     return true;  
  39.                 }  
  40.                 catch (DbUpdateException)  
  41.                 {  
  42.                     return false;  
  43.                 }  
  44.             }  
  45.             else  
  46.             {  
  47.                 return false;  
  48.             }  
  49.         }  
  50.   
  51.         [HttpGet]  
  52.         [Route("Details/{id}")]  
  53.         public async Task<Employee> Details(string id)  
  54.         {  
  55.             return await _dbContext.Employees.FindAsync(id);  
  56.         }  
  57.   
  58.         [HttpPut]  
  59.         [Route("Edit/{id}")]  
  60.         public async Task<bool> Edit(string id, [FromBody]Employee employee)  
  61.         {  
  62.             if (id != employee.Id)  
  63.             {  
  64.                 return false;  
  65.             }  
  66.   
  67.             _dbContext.Entry(employee).State = EntityState.Modified;  
  68.             await _dbContext.SaveChangesAsync();  
  69.             return true;  
  70.         }  
  71.   
  72.         [HttpDelete]  
  73.         [Route("Delete/{id}")]  
  74.         public async Task<bool> DeleteConfirmed(string id)  
  75.         {  
  76.             var employee = await _dbContext.Employees.FindAsync(id);  
  77.             if (employee == null)  
  78.             {  
  79.                 return false;  
  80.             }  
  81.   
  82.             _dbContext.Employees.Remove(employee);  
  83.             await _dbContext.SaveChangesAsync();  
  84.             return true;  
  85.         }  
  86.     }  
  87. }  
We have added all the logic for CRUD actions inside the controller. All the methods are self-explanatory.
 
You must add “MapControllers” end point in “Configure” method inside the Startup class to get service calls from Web API controller.
 
 
As I mentioned earlier, currently HttpClient class doesn’t contain “GetJsonAsync”, “PostJsonAsync”, and “PutJsonAsyc” methods. I have already raised an issue in Microsoft GitHub account for adding these methods. Please support, if you can.
 
We can create a custom HttpClient class to add these missing methods to handle json data.
 
CustomHttpClient.cs
  1. using Newtonsoft.Json;  
  2. using System.Net.Http;  
  3. using System.Text;  
  4. using System.Threading.Tasks;  
  5.   
  6. namespace BlazorEmployeeEFCore.Data  
  7. {  
  8.     public class CustomHttpClient : HttpClient  
  9.     {  
  10.         public async Task<T> GetJsonAsync<T>(string requestUri)  
  11.         {  
  12.             HttpClient httpClient = new HttpClient();  
  13.             var httpContent = await httpClient.GetAsync(requestUri);  
  14.             string jsonContent = httpContent.Content.ReadAsStringAsync().Result;  
  15.             T obj = JsonConvert.DeserializeObject<T>(jsonContent);  
  16.             httpContent.Dispose();  
  17.             httpClient.Dispose();  
  18.             return obj;  
  19.         }  
  20.         public async Task<HttpResponseMessage> PostJsonAsync<T>(string requestUri, T content)  
  21.         {  
  22.             HttpClient httpClient = new HttpClient();  
  23.             string myContent = JsonConvert.SerializeObject(content);  
  24.             StringContent stringContent = new StringContent(myContent, Encoding.UTF8, "application/json");  
  25.             var response = await httpClient.PostAsync(requestUri, stringContent);  
  26.             httpClient.Dispose();  
  27.             return response;  
  28.         }  
  29.         public async Task<HttpResponseMessage> PutJsonAsync<T>(string requestUri, T content)  
  30.         {  
  31.             HttpClient httpClient = new HttpClient();  
  32.             string myContent = JsonConvert.SerializeObject(content);  
  33.             StringContent stringContent = new StringContent(myContent, Encoding.UTF8, "application/json");  
  34.             var response = await httpClient.PutAsync(requestUri, stringContent);  
  35.             httpClient.Dispose();  
  36.             return response;  
  37.         }  
  38.     }  
  39. }  
In GetJsonAsync method, we have fetched the data from Web API service and converted string data to object. We can pass object type as a generic parameter. In our case, Employee is the object type.
 
In PostJsonAsync and PutJsonAsync methods, we have serialized the object type data to string content and call PostAsync and PutAsync methods respectively. These methods already available in HttpClient class.
 
We must pass the base URL endpoint from each Blazor component to call the Web API methods. We can store the application base URL inside the appsettings.js file and create a service to get these values from appsettings file. We can inject this service from each Blazor components.
 
AppSettingsService.cs
  1. using Microsoft.Extensions.Configuration;  
  2.   
  3. namespace BlazorEmployeeEFCore.Data  
  4. {  
  5.     public class AppSettingsService  
  6.     {  
  7.         private readonly IConfiguration _config;  
  8.         public AppSettingsService(IConfiguration config)  
  9.         {  
  10.             _config = config;  
  11.         }  
  12.         public string GetBaseUrl()  
  13.         {  
  14.             return _config.GetValue<string>("MySettings:BaseUrl");  
  15.         }  
  16.     }  
  17. }  
We can store the base URL in the appsettings configuration file.
 
 
 
For my application, port number is 5000. In your case, it may be different. Please give the correct port number in appsettings.
 
We must register CustomHttpClient and AppSettingsService in the Startup class to inject from Blazor components.
 
 
 
We can create four Blazor components inside the “Pages” folder to perform all CRUD actions.
 
ListEmployees.razor
  1. @using BlazorEmployeeEFCore.Data  
  2.   
  3. @page "/listemployees"  
  4.   
  5. @inject CustomHttpClient Http  
  6. @inject AppSettingsService AppSettingsService  
  7.   
  8. <h2>Employee Details</h2>  
  9. <p>  
  10.     <a href="/addemployee">Create New Employee</a>  
  11. </p>  
  12. @if (employees == null)  
  13. {  
  14.     <img src="./basicloader.gif" />  
  15. }  
  16. else  
  17. {  
  18.     <table class='table'>  
  19.         <thead>  
  20.             <tr>  
  21.                 <th>Name</th>  
  22.                 <th>Department</th>  
  23.                 <th>Designation</th>  
  24.                 <th>Company</th>  
  25.             </tr>  
  26.         </thead>  
  27.         <tbody>  
  28.             @foreach (var employee in employees)  
  29.             {  
  30.                 <tr>  
  31.                     <td>@employee.Name</td>  
  32.                     <td>@employee.Department</td>  
  33.                     <td>@employee.Designation</td>  
  34.                     <td>@employee.Company</td>  
  35.                     <td>  
  36.                         <a href='/editemployee/@employee.Id'>Edit</a>  
  37.                         <a href='/deleteemployee/@employee.Id'>Delete</a>  
  38.                     </td>  
  39.                 </tr>  
  40.   
  41.             }  
  42.         </tbody>  
  43.     </table>  
  44. }  
  45.   
  46. @code {  
  47.     Employee[] employees;  
  48.     string baseUrl;  
  49.   
  50.     protected override async Task OnInitializedAsync()  
  51.     {  
  52.         baseUrl = AppSettingsService.GetBaseUrl();  
  53.         employees = await Http.GetJsonAsync<Employee[]>(baseUrl + "/api/employees/get");  
  54.     }  
  55. }     
We have injected CustomHttpClient and AppSettingsService service in above component. We have called the GetJsonAsync method inside the OnInitializedAsync event. This is the initial page lifecycle event of a Blazor application.
 
AddEmpoyee.razor
  1. @using BlazorEmployeeEFCore.Data  
  2.   
  3. @page "/addemployee"  
  4.   
  5. @inject CustomHttpClient Http  
  6. @inject NavigationManager NavigationManager  
  7. @inject AppSettingsService AppSettingsService  
  8.   
  9. <h2>Create Employee</h2>  
  10. <hr />  
  11. <form>  
  12.     <div class="row">  
  13.         <div class="col-md-8">  
  14.             <div class="form-group">  
  15.                 <label for="Name" class="control-label">Name</label>  
  16.                 <input for="Name" class="form-control" @bind="@employee.Name" />  
  17.             </div>  
  18.             <div class="form-group">  
  19.                 <label for="Department" class="control-label">Department</label>  
  20.                 <input for="Department" class="form-control" @bind="@employee.Department" />  
  21.             </div>  
  22.             <div class="form-group">  
  23.                 <label for="Designation" class="control-label">Designation</label>  
  24.                 <input for="Designation" class="form-control" @bind="@employee.Designation" />  
  25.             </div>  
  26.             <div class="form-group">  
  27.                 <label for="Company" class="control-label">Company</label>  
  28.                 <input for="Company" class="form-control" @bind="@employee.Company" />  
  29.             </div>  
  30.         </div>  
  31.     </div>  
  32.     <div class="row">  
  33.         <div class="col-md-4">  
  34.             <div class="form-group">  
  35.                 <input type="button" class="btn btn-primary" @onclick="@CreateEmployee" value="Save" />  
  36.                 <input type="button" class="btn" @onclick="@Cancel" value="Cancel" />  
  37.             </div>  
  38.         </div>  
  39.     </div>  
  40. </form>  
  41.   
  42. @code {  
  43.   
  44.     Employee employee = new Employee();  
  45.     string baseUrl;  
  46.   
  47.     protected async Task CreateEmployee()  
  48.     {  
  49.         baseUrl = AppSettingsService.GetBaseUrl();  
  50.         await Http.PostJsonAsync(baseUrl + "/api/employees/create", employee);  
  51.         NavigationManager.NavigateTo("listemployees");  
  52.     }  
  53.   
  54.     void Cancel()  
  55.     {  
  56.         NavigationManager.NavigateTo("listemployees");  
  57.     }  
  58. }     
We have injected NavigationManager class also in this component. We can control the page navigation with this class. We have also used the “@bind” directive to handle two-way data binding in Blazor. “@onclick” is an event handler used for calling a method.
 
EditEmployee.razor
  1. @using BlazorEmployeeEFCore.Data  
  2.   
  3. @page "/editemployee/{id}"  
  4.   
  5. @inject CustomHttpClient Http  
  6. @inject NavigationManager NavigationManager  
  7. @inject AppSettingsService AppSettingsService  
  8.   
  9. <h2>Edit Employee</h2>  
  10. <hr />  
  11. <form>  
  12.     <div class="row">  
  13.         <div class="col-md-8">  
  14.             <div class="form-group">  
  15.                 <label for="Name" class="control-label">Name</label>  
  16.                 <input for="Name" class="form-control" @bind="@employee.Name" />  
  17.             </div>  
  18.             <div class="form-group">  
  19.                 <label for="Department" class="control-label">Department</label>  
  20.                 <input for="Department" class="form-control" @bind="@employee.Department" />  
  21.             </div>  
  22.             <div class="form-group">  
  23.                 <label for="Designation" class="control-label">Designation</label>  
  24.                 <input for="Designation" class="form-control" @bind="@employee.Designation" />  
  25.             </div>  
  26.             <div class="form-group">  
  27.                 <label for="Company" class="control-label">Company</label>  
  28.                 <input for="Company" class="form-control" @bind="@employee.Company" />  
  29.             </div>  
  30.         </div>  
  31.     </div>  
  32.     <div class="row">  
  33.         <div class="form-group">  
  34.             <input type="button" class="btn btn-primary" @onclick="@UpdateEmployee" value="Update" />  
  35.             <input type="button" class="btn" @onclick="@Cancel" value="Cancel" />  
  36.         </div>  
  37.     </div>  
  38. </form>  
  39.   
  40. @code {  
  41.   
  42.     [Parameter]  
  43.     public string id { getset; }  
  44.     string baseUrl;  
  45.   
  46.     Employee employee = new Employee();  
  47.   
  48.     protected override async Task OnInitializedAsync()  
  49.     {  
  50.         baseUrl = AppSettingsService.GetBaseUrl();  
  51.         employee = await Http.GetJsonAsync<Employee>(baseUrl + "/api/employees/details/" + id);  
  52.     }  
  53.   
  54.     protected async Task UpdateEmployee()  
  55.     {  
  56.         await Http.PutJsonAsync(baseUrl + "/api/employees/edit/" + id, employee);  
  57.         NavigationManager.NavigateTo("listemployees");  
  58.     }  
  59.   
  60.     void Cancel()  
  61.     {  
  62.         NavigationManager.NavigateTo("listemployees");  
  63.     }  
  64. }     
DeleteEmployee.razor
  1. @using BlazorEmployeeEFCore.Data  
  2.   
  3. @page "/deleteemployee/{id}"  
  4.   
  5. @inject CustomHttpClient Http  
  6. @inject NavigationManager NavigationManager  
  7. @inject AppSettingsService AppSettingsService  
  8.   
  9. <h2>Delete</h2>  
  10. <p>Are you sure you want to delete this Employee with Id :<b> @id</b></p>  
  11. <br />  
  12. <div class="col-md-4">  
  13.     <table class="table">  
  14.         <tr>  
  15.             <td>Name</td>  
  16.             <td>@employee.Name</td>  
  17.         </tr>  
  18.         <tr>  
  19.             <td>Department</td>  
  20.             <td>@employee.Department</td>  
  21.         </tr>  
  22.         <tr>  
  23.             <td>Designation</td>  
  24.             <td>@employee.Designation</td>  
  25.         </tr>  
  26.         <tr>  
  27.             <td>Company</td>  
  28.             <td>@employee.Company</td>  
  29.         </tr>  
  30.     </table>  
  31.     <div class="form-group">  
  32.         <input type="button" value="Delete" @onclick="@Delete" class="btn btn-primary" />  
  33.         <input type="button" value="Cancel" @onclick="@Cancel" class="btn" />  
  34.     </div>  
  35. </div>  
  36.   
  37. @code {  
  38.   
  39.     [Parameter]  
  40.     public string id { getset; }  
  41.     string baseUrl;  
  42.     Employee employee = new Employee();  
  43.   
  44.     protected override async Task OnInitializedAsync()  
  45.     {  
  46.         baseUrl = AppSettingsService.GetBaseUrl();  
  47.         employee = await Http.GetJsonAsync<Employee>(baseUrl + "/api/employees/details/" + id);  
  48.     }  
  49.   
  50.     protected async Task Delete()  
  51.     {  
  52.         await Http.DeleteAsync(baseUrl + "/api/employees/delete/" + id);  
  53.         NavigationManager.NavigateTo("listemployees");  
  54.     }  
  55.   
  56.     void Cancel()  
  57.     {  
  58.         NavigationManager.NavigateTo("listemployees");  
  59.     }  
  60. }     
We can modify the NavMenu component inside the “Shared” folder to add routing for Employee data.
 
NavMenu.razor
  1. <div class="top-row pl-4 navbar navbar-dark">  
  2.     <a class="navbar-brand" href="">BlazorEmployeeEFCore</a>  
  3.     <button class="navbar-toggler" @onclick="ToggleNavMenu">  
  4.         <span class="navbar-toggler-icon"></span>  
  5.     </button>  
  6. </div>  
  7.   
  8. <div class="@NavMenuCssClass" @onclick="ToggleNavMenu">  
  9.     <ul class="nav flex-column">  
  10.         <li class="nav-item px-3">  
  11.             <NavLink class="nav-link" href="" Match="NavLinkMatch.All">  
  12.                 <span class="oi oi-home" aria-hidden="true"></span> Home  
  13.             </NavLink>  
  14.         </li>  
  15.         <li class="nav-item px-3">  
  16.             <NavLink class="nav-link" href="counter">  
  17.                 <span class="oi oi-plus" aria-hidden="true"></span> Counter  
  18.             </NavLink>  
  19.         </li>  
  20.         <li class="nav-item px-3">  
  21.             <NavLink class="nav-link" href="fetchdata">  
  22.                 <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data  
  23.             </NavLink>  
  24.         </li>  
  25.         <li class="nav-item px-3">  
  26.             <NavLink class="nav-link" href="listemployees">  
  27.                 <span class="oi oi-list-rich" aria-hidden="true"></span> Employee data  
  28.             </NavLink>  
  29.         </li>  
  30.     </ul>  
  31. </div>  
  32.   
  33. @code {  
  34.     bool collapseNavMenu = true;  
  35.   
  36.     string NavMenuCssClass => collapseNavMenu ? "collapse" : null;  
  37.   
  38.     void ToggleNavMenu()  
  39.     {  
  40.         collapseNavMenu = !collapseNavMenu;  
  41.     }  
  42. }  
We have completed the entire coding. We can run the application now.
 
 
 
We can click the Employee data menu link to navigate the employee list page.
 
 
We have already added a simple spinner in this component. We can click “Create New Employee” link to create a new employee record.
 
 
 
We can perform the other CRUD operations like edit and delete as well in this application.
 
 
I have noticed one interesting thing that, even though we have called various Web API methods, these were not triggered in network tab of developer console.
 
 
 
We can see only one “negotiate” action in entire web requests. This will improve the performance of entire application.

No comments:

Post a Comment

Lab 09: Publish and subscribe to Event Grid events

  Microsoft Azure user interface Given the dynamic nature of Microsoft cloud tools, you might experience Azure UI changes that occur after t...