For better data manipulation, we suggest to a a Factory class working with service class to provide data mapping and data manipulation. Inside the framework, we always return the Entity Record from database which is the raw data from database. But we are not recommended to return the data structure directly to frontend view. In the actual working environment, the data input and output model are different when calling different functions, and some data we may not want to transfer to frontend because of data security. So, we suggest to create input and output data model instead of directly using data table (Entity).
Creating Model Classes
For Model Classes, we usually treated as 3 types
- Data Input : Request Class, we always named as XXXXRequest. e.g. ClientUpdateRequest, UserApprovalRequest
- Data Output : Response Class, we always named as XXXXXModel. e.g. ClientModel, ClientListModel, AuthenicateModel
- Data Searching Parameters : Command Class, we always named as XXXXCommand. e.g. ClientSearchCommand
There is some experience to be shared here:
For Request Class:
- For all bool, int, double, datetime fields, no matter with the Entity Class, I always to use bool?, int?, double?, datetime? instead. It is because when using the model as data update, the frontend may only transfer the modified fields only in the JSon. Therefore, we should avoiding overwriting the existing values with Null parameters.
- My habit is using the same class of Update Model and Create Model in single model, I will use the mapper service for data copying from model to entity or from entity to model.
- ‘Id’ key will absent from the Update Model because it avoid the mapper class to try to modify the ‘Id’ value when using UpdateAsync.
- The request class should write as simple as possible that easy to get the request data from Frontend.
For Response Class:
- Can create multiple response class in different controller response, reduce the unnecessary data fields to minimize the network usage to enhance the system performance.
- Add some calculated fields and prepare the value in factory to reduce the frontend to call API to retrieve more information. For example, if the table contains PictureId, we always as a PictureModel class into the model that include the URL of the Picture. Then we return the data model include the related PictureModel, so that the frontend do not needed to call another API to obtain the picture Url.
- May different when using in Administrator backend panel or in user portal. Just data should not directly pass to frontend browse due with the system security and data piracy.
For Command Class:
- Always consider to return data with paging, to avoid accidentally huge data transfer
- All Search parameters should be nullable, so we can easy to skip handling empty string and null data input options
- Include ActiveOnly and IncludeDeleted options if applicable.
- Always inherit basePageableModel
public partial record AddressSearchCommand : BasePageableModel
{
/// <summary> Address Type Enums </summary>
public string CRecType { get; set; }
/// <summary> Address Type Enums </summary>
public string CRecCode { get; set; }
/// <summary> Address Type Enums </summary>
public Guid? CRecGuid { get; set; }
/// <summary>收貨公司名稱</summary>
public string CompanyName { get; set; }
/// <summary>收貨人姓名</summary>
public string FullName { get; set; }
/// <summary>收貨人電郵地址</summary>
public string Email { get; set; }
/// <summary>國家碼</summary>
public string CountryCode { get; set; }
/// <summary>州/省碼</summary>
public string StateProvinceCode { get; set; }
/// <summary>市/縣</summary>
public string CountyCode { get; set; }
/// <summary>地/鎮</summary>
public string City { get; set; }
/// <summary>地址</summary>
public string Address { get; set; }
/// <summary> Phone, Tel, Fax </summary>
public string Phone { get; set; }
/// <summary> Is Approved Address </summary>
public bool? IsApproved { get; set; }
/// <summary> Is Billing Address </summary>
public bool? IsBilling { get; set; }
/// <summary> Show Active Only </summary>
public bool? ActiveOnly { get; set; }
/// <summary> Include Deleted </summary>
public bool? IncludeDeleted { get; set; }
/// <summary>
/// Order By : 0 - Order By Id
/// 1 - Company Name
/// 2 - Full Name
/// </summary>
public int? OrderBy { get; set; }
}
Create the Factory Class
Why we use factory? In my experience, factory is a part of service but handle different data. From my design, services always deal with data provider or repository to retrieve or post. Factories will mainly to handle to the build correct model result and return to controller. Therefore, in the example of this Framework, we always to call service to retrieve a set of data and call factory to build the response model. For more information, please take a look with the sample code of the Framework.
As the same way as Service, to create a Factory Class we needed to create a Factory Class and an Interface Class. The example like this:
namespace BlueSky.Service.Factories
{
public class AddressFactory : IAddressFactory
{
#region Properties
private readonly DataContext _context;
private readonly IMapper _mapper;
#endregion
#region Ctor
public AddressFactory(DataContext context,
IMapper mapper)
{
_context = context;
_mapper = mapper;
}
#endregion
#region Utilities
#endregion
#region Methods
public Address PrepareAddressFromUpdateModel(Address response, AddressUpdateRequest model)
{
response = _mapper.Map<Address>(model);
return response;
}
public AddressModel PrepareAddressResponseModel(AddressModel response, Address address)
{
response = _mapper.Map<AddressModel>(address);
return response;
}
public AddressListModel PrepareAddressListResponseModel(AddressListModel response, IPagedList<Address> list)
{
if (response == null)
response = new AddressListModel();
response.Addresses = _mapper.Map<List<AddressModel>>(list);
response.PageNumber = list.PageIndex + 1;
response.PageSize = list.PageSize;
response.HasNextPage = list.HasNextPage;
response.HasPreviousPage = list.HasPreviousPage;
response.TotalItems = list.TotalCount;
response.TotalPages = list.TotalPages;
return response;
}
public Address PrepareAddressApproveModel(Address response, AddressApproveRequest model)
{
if (response == null)
return response;
response = _mapper.Map<Address>(model);
response.IsApproved = true;
response.IsRejected = false;
response.ApproveDateUtc = DateTime.UtcNow;
return response;
}
public Address PrepareAddressRejectModel(Address address)
{
if (address == null)
return address;
address.IsApproved = false;
address.IsRejected = true;
address.ApproveDateUtc = DateTime.UtcNow;
return address;
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BlueSky.Core;
using BlueSky.Data;
using BlueSky.Core.Domain.Common;
using BlueSky.Core.Domain.Clients;
using BlueSky.Service.Models.CommonData;
using BlueSky.Service.Models.Client;
namespace BlueSky.Service.Factories
{
public interface IAddressFactory
{
Address PrepareAddressFromUpdateModel(Address response, AddressUpdateRequest model);
AddressModel PrepareAddressResponseModel(AddressModel response, Address address);
AddressListModel PrepareAddressListResponseModel(AddressListModel response, IPagedList<Address> list);
Address PrepareAddressApproveModel(Address response, AddressApproveRequest model);
Address PrepareAddressRejectModel(Address address);
}
}
Register the Factory Class
The same as service, create a registration in ‘Infrastructure/DependencyRegistrar.cs’ as below
namespace BlueSky.Service.Infrastructure
{
public class DependencyRegistrar
{
public void Register(IServiceCollection services)
{
services.AddScoped(typeof(IRepository<>), typeof(EntityRepository<>));
services.AddScoped(typeof(IGuidRepository<>), typeof(GuidEntityRepository<>));
services.AddScoped(typeof(ICustomKeyRepository<>), typeof(CustomKeyEntityRepository<>));
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<ISiteService, SiteService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<ICommonDataService, CommonDataService>();
services.AddScoped<ILanguageService, LanguageService>();
services.AddScoped<IAddressService, AddressService>();
services.AddScoped<IClientService, ClientService>();
services.AddScoped<ISiteFactory, SiteFactory>();
services.AddScoped<ICommonDataFactory, CommonDataFactory>();
services.AddScoped<ILanguageFactory, LanguageFactory>();
services.AddScoped<IUserFactory, UserFactory>();
services.AddScoped<IAddressFactory, AddressFactory>();
services.AddScoped<IClientFactory, ClientFactory>();
}
}
}
For using of Localization Service, it is best to work in the Factory so that we can easy response the correct locale result to client.
Using of AutoMapper
AutoMapper is a service which used to clone/fill data from Entity class to Model class or Model class to Entity class. It let you can copy hundreds of field by just few lines of code.
// To overwrite Client Record from ClientUpdateRequest
// Syntax : _mapper.Map<SourceModel, DestModel>(Source, Destination)
public Client PrepareClientFromUpdateModel(Client response, ClientUpdateRequest model)
{
_mapper.Map<ClientUpdateRequest, Client>(model, response);
return response;
}
// To Create a new set of record without old data
// Syntax : destination = _mapper.Map<DestModel>(SourceModel)
public ClientModel PrepareClientResponseModel(ClientModel response, Client client)
{
response = _mapper.Map<ClientModel>(client);
return response;
}
In the example, you can see the it is very easy to copy all the fields value from ClientUpdateRequest –> Client (Entity) and from Client (Entity) –> ClientModel
To complete this feature, we must have correct declaration between the class and model mapping. Open the “/Helpers/AutoMapperProfile.cs”, add the entry
CreateMap<Client, ClientModel>().ForMember(dest => dest.ClientGuid, opt => opt.MapFrom(src => src.guid));
For this Example, it declared a entity class (Client) to a response model (ClientModel). Seen we defined we have change the field name “guid” to “ClientGuid” in the response model so that we add a option to change the mapping.
CreateMap<ClientUpdateRequest, Client>()
.ForAllMembers(x => x.Condition(
(src, dest, prop) =>
{
// ignore null & empty string properties
if (prop == null) return false;
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
if (prop.GetType() == typeof(Guid) && (Guid)prop == Guid.Empty) return false;
return true;
}
));
For this example, it declared an update model (ClientUpdateRequest) to map to the entity class (Client). For this case, to avoid unexpected empty data overwrite the existing data, we needed to add some condition for handling. In this example, we have skip null value, empty string and empty Guid to me update to destinated class.
After adding this declaration, you can enjoy the service of AutoMapper, which is very code friendly for Model building. In later chapter, we will discuss how to use Localization service within the Factory Service and Models.
Overview : Developer Guide for BlueSky .NETCORE API Framework
Previous : Creating and Registering of Service Classes (Ch.6)
Next : Creating Controller Classes with API endpoint (Ch.8)
(c) 2022, BlueSky Information Technology (Int’l) Co. Ltd, All Rights Reserved.