Author : MD TAREQ HASSAN | Updated : 2020/08/06
Databse First Approach - Table column type is string and mapped to enum
Databse First Approach - Table column type is int and data fetched by eager loading
Entity Models
EntityModels/EmployeeRegion.cs
public class EmployeeRegion
{
// Belongs to EF Core Data Project
// EmployeeRegion table contains list of (Region id, Region name)
public int Id { get; set; }
public string Name { get; set; }
}
EntityModels/Employee.cs
public class Employee
{
// Belongs to EF Core Data Project
public string Name { get; set; }
public int RegionId { get; set; } // Foreign key, will be used to eagerly fetch EmployeeRegion.Name
// ... ... ... other properties
public EmployeeRegion EmployeeRegion { get; set; } // navigational property (1 to 1 relationship)
// Eager loading of EmployeeRegion: .Include(e => e.EmployeeRegion)
// Employee.EmployeeRegion.Name will be mapped to UIModel.RegionName by AutoMapper
}
UI Models
Models/EmployeeModel.cs
public class RegionModel
{
// List<RegionModel>: to use as search filter dropdown (html select) data source
public int Id { get; set; }
public string Name { get; set; }
}
public class EmployeeModel
{
// For UI, as data for html table (each EmployeeModel.Prop will be used in <td>)
public int Id { get; set; }
public string Name { get; set; }
public string RegionName { get; set; } // Entity.EmployeeRegion.Name will be mapped by AutoMapper)
// ... ... ... other properties
}
Models/EmployeeSearchModel.cs
public class EmployeeSearchModel
{
// will be usued by service to filter
public int? EmployeeRegionId { get; set; }
public string EmployeeName { get; set; }
}
Extensions
Extensions/SearchFieldMutator.cs
using System;
using System.Linq;
namespace Blazor.Extensions
{
public delegate IQueryable<TItem> QueryMutator<TItem, TSearch>(IQueryable<TItem> items, TSearch search);
/// <summary>
/// Query mutator for search filter
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <typeparam name="TSearch"></typeparam>
public class SearchFieldMutator<TItem, TSearch>
{
public Predicate<TSearch> Condition { get; set; }
public QueryMutator<TItem, TSearch> Mutator { get; set; }
public SearchFieldMutator(Predicate<TSearch> condition, QueryMutator<TItem, TSearch> mutator)
{
Condition = condition;
Mutator = mutator;
}
public IQueryable<TItem> Apply(TSearch search, IQueryable<TItem> query)
{
return Condition(search) ? Mutator(query, search) : query;
}
}
}
Services
IEmployeeService.cs
public interface IEmployeeService
{
Task<List<EmployeeModel>> FetchEmployeesAsync(EmployeeSearchModel searchModel = null, int? page = null);
// ... ... ...
}
EmployeeService.cs
public class EmployeeService : IEmployeeService
{
private readonly EmployeeDbContext _dbContext;
private readonly IMapper _mapper;
#region Search Field Mutators
private static readonly List<SearchFieldMutator<Employee, EmployeeSearchModel>> SearchFieldMutators = new List<SearchFieldMutator<Employee, EmployeeSearchModel>>() {
new SearchFieldMutator<Employee, EmployeeSearchModel>(searchModel => searchModel.EmployeeRegionId != null, (query, search) => query.Where(employeeEntity => employeeEntity.RegionId == search.EmployeeRegionId)),
new SearchFieldMutator<Employee, EmployeeSearchModel>(searchModel => !string.IsNullOrWhiteSpace((searchModel.EmployeeName), (query, search) => query.Where(employeeEntity => employeeEntity.Name.Contains(search.EmployeeName))),
};
#endregion
public EmployeeService(EmployeeDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<List<EmployeeModel>> FetchEmployeesAsync(EmployeeSearchModel searchModel, int? page)
{
if (searchModel == null && !page.HasValue) // no need to perform filtering
{
return;
}
var query = _dbContext.Employees
.Include(entity => entity.EmployeeRegion) // eager loading
.AsQueryable();
//
// apply search criteria
//
if (searchModel != null)
{
foreach (var mutator in SearchFieldMutators)
{
query = mutator.Apply(searchModel, query);
}
}
List<Employee> filteredEntityList;
//
// apply paging if needed
//
if (page.HasValue && page.Value >= 1)
{
var skipSize = Constants.Paging.ItemsPerPage * (page.Value - 1); // i.e. ItemsPerPage = 25
var takeSize = Constants.Paging.ItemsPerPage;
filteredEntityList = await query.Skip(skipSize).Take(takeSize).ToListAsync();
}
else
{
filteredEntityList = await query.ToListAsync();
}
// Use automapper to get UI models from entity models
var employees = _mapper.Map<List<EmployeeModel>>(filteredEntityList);
return employees;
}
// ... ... ...
}
SearchFilterDataProvider
public interface ISearchFilterDataProvider
{
Task<List<RegionModel>> FetchRegionListAsync();
// ... ... ...
}
public class SearchFilterDataProvider : ISearchFilterDataProvider
{
private readonly EmployeeDbContext _dbContext;
private readonly IMapper _mapper;
public SearchFilterDataProvider(EmployeeDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<List<RegionModel>> FetchRegionListAsync()
{
var regionEntities = await _dbContext.EmployeeRegions.ToListAsync();
var regionList = _mapper.Map<List<RegionModel>>(regionEntities);
return regionList;
}
// ... ... ...
}
AutoMapper profiles
AutoMapperProfiles/EmployeeAutoMapperProfile.cs
using AutoMapper;
namespace Blazor.AutoMapperProfiles
{
public class EmployeeAutoMapperProfile : Profile
{
public EmployeeAutoMapperProfile()
{
CreateMap<Employee, EmployeeModel>()
.ForMember(targetModel => targetModel.Id, option => option.MapFrom(entity => entity.Id))
.ForMember(targetModel => targetModel.Name, option => option.MapFrom(entity => entity.Name))
// ... ... ...
.ForMember(targetModel => targetModel.RegionName, option => option.MapFrom(entity => entity.EmployeeRegion.Name));
CreateMap<RegionModel, EmployeeRegion>()
.ForMember(targetModel => targetModel.Id, options => options.MapFrom(entity => entity.Id))
.ForMember(targetModel => targetModel.Name, options => options.MapFrom(entity => entity.Name));
// ... ... ...
}
}
}
Employee List
Index.razor.cs
public class IndexBase : ComponentBase
{
#region Dependency Injection
[Inject]
protected ISearchFilterDataProvider SearchFilterDataProvider { get; set; }
[Inject]
protected IEmployeeService EmployeeService { get; set; }
[Inject]
protected NavigationManager NavigationManager { get; set; }
#endregion
#region Search Related
protected List<RegionModel> RegionList { get; set; } = new List<RegionModel>();
protected EmployeeSearchModel SearchModel { get; set; } = new EmployeeSearchModel();
#endregion
protected List<EmployeeModel> Employees;
protected override async Task OnInitializedAsync()
{
Log.Information("Initializing required data");
RegionList = await SearchFilterDataProvider.FetchRegionListAsync();
Employees = await EmployeeService.FetchEmployeesAsync();
}
protected async Task OnSearchAsync()
{
Log.Information("Search button pressed...");
// if you need paging, add "[Parameter] protected int? Page;" property & use .FetchEmployeesAsync(SearchModel,Page)
Employees = await EmployeeService.FetchEmployeesAsync(SearchModel);
NavigationManager.NavigateTo($"/{EndPoint.Employees}"); // EndPoint is a constant => EndPoint.Employees = "/employees"
}
}
Index.razor
@page "/"
@page "/employees"
@inherits IndexBase
<div class="row">
<div class="col-3 offset-9 text-right mb-5">
<button class="btn btn-success" @onclick="@( () => NavigationManager.NavigateTo("employees/create") )">
Create
</button>
</div>
</div>
<div class="row mb-2">
<div class="form-group col-3">
<label for="@nameof(SearchModel.EmployeeRegionId)">Location:</label>
<select id="@nameof(SearchModel.EmployeeRegionId)" class="form-control" @bind="@SearchModel.EmployeeRegionId">
<option value="" selected>Select location</option>
@foreach (var Region in RegionList)
{
<option value="@Region.Id">@Region.Name</option>
}
</select>
</div>
<div class="form-group col-3">
<div class="col-6 form-group">
<input type="text" id="@nameof(SearchModel.EmployeeName)" class="form-control" placeholder="Name" @bind="@SearchModel.EmployeeName"/>
</div>
</div>
<div class="col">
<button class="btn btn-info" @onclick="@(async () => await OnSearchAsync() )">
Search
</button>
</div>
</div>
@if (EmployeeNamees == null)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row">
<div class="col-12">
<table class="table table-hover table-ellipsis">
<thead class="bg-primary">
<tr>
<th>Name</th>
<th>RegionName</th>
<th>...</th>
<th colspan="2"></th>
</tr>
</thead>
<tbody>
@foreach (var employee in Employees)
{
<tr>
<td>@employee.Name</td>
<td>@employee.RegionName</td>
<td>...</td>
<td class="text-right pr-0">
<button class="btn btn-outline-primary" @onclick="@( () => NavigationManager.NavigateTo($"employees/edit/{employee.Id}") )">
Edit
</button>
</td>
<td class="text-right pr-0">
<button class="btn btn-outline-danger ml-2" @onclick="@( () => NavigationManager.NavigateTo($"employees/delete/{employee.Id}") )">
Delete
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}