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>
}