在 ASP.NET Core 项目中使用 AutoMapper 进行实体映射
?一、前言
在實(shí)際項(xiàng)目開發(fā)過程中,我們使用到的各種 ORM 組件都可以很便捷的將我們獲取到的數(shù)據(jù)綁定到對(duì)應(yīng)的?List<T>?集合中,因?yàn)槲覀冏罱K想要在頁面上展示的數(shù)據(jù)與數(shù)據(jù)庫(kù)實(shí)體類之間可能存在很大的差異,所以這里更常見的方法是去創(chuàng)建一些對(duì)應(yīng)于頁面數(shù)據(jù)展示的?`視圖模型`?類,通過對(duì)獲取到的數(shù)據(jù)進(jìn)行二次加工,從而滿足實(shí)際頁面顯示的需要。
因此,如何更便捷的去實(shí)現(xiàn)?數(shù)據(jù)庫(kù)持久化對(duì)象?與?視圖對(duì)象?間的實(shí)體映射,避免我們?cè)诖a中去一次次的手工實(shí)現(xiàn)這一過程,就可以降低開發(fā)的工作量,而 AutoMapper 則是可以幫助我們便捷的實(shí)現(xiàn)實(shí)體轉(zhuǎn)換這一過程的利器。所以,本章我們就來學(xué)習(xí)如何在 ASP.NET Core 項(xiàng)目中通過使用 AutoMapper 去完成實(shí)體間的映射。
當(dāng)然,如果你習(xí)慣于從視圖展現(xiàn)到持久化到數(shù)據(jù)庫(kù)都采用數(shù)據(jù)庫(kù)實(shí)體,那么本篇文章對(duì)你可能不會(huì)有任何的幫助。
代碼倉(cāng)儲(chǔ):https://github.com/Lanesra712/grapefruit-common/tree/master/sample/aspnetcore/aspnetcore-automapper-tutorial
?二、Step by Step
AutoMapper 是一個(gè) OOM(Object-Object-Mapping) 組件,從名字上就可以看出來,這一系列的組件主要是為了幫助我們實(shí)現(xiàn)實(shí)體間的相互轉(zhuǎn)換,從而避免我們每次都采用手工編寫代碼的方式進(jìn)行轉(zhuǎn)換。在沒有采用 OOM 組件之前,如果我們需要實(shí)現(xiàn)類似于一份數(shù)據(jù)在不同客戶端顯示不同的字段,我們只能以手工的、逐個(gè)屬性賦值的方式實(shí)現(xiàn)數(shù)據(jù)在各個(gè)客戶端數(shù)據(jù)類型間的數(shù)據(jù)傳遞,而 OOM 組件則可以很方便的幫我們實(shí)現(xiàn)這一需求。
1、幾個(gè)概念
在上面我們有提到?數(shù)據(jù)庫(kù)持久化對(duì)象?和?視圖對(duì)象?這兩個(gè)概念,其實(shí)除了這兩個(gè)對(duì)象的概念之外,還存在一個(gè)?數(shù)據(jù)傳輸對(duì)象?的概念,這里我們來簡(jiǎn)單闡述下這三種對(duì)象的概念。
數(shù)據(jù)庫(kù)持久化對(duì)象(Persistent Object):顧名思義,這個(gè)對(duì)象是用來將我們的數(shù)據(jù)持久化到數(shù)據(jù)庫(kù),一般來說,持久化對(duì)象中的字段會(huì)與數(shù)據(jù)庫(kù)中對(duì)應(yīng)的 table 保持一致。
這里,如果你采用了 DDD 的思想去指導(dǎo)設(shè)計(jì)系統(tǒng)架構(gòu),其實(shí)最終落地到我們代碼中的其實(shí)是?領(lǐng)域?qū)ο?#xff08;Domain Object),它與?數(shù)據(jù)庫(kù)持久化對(duì)象?最顯著的差異在于?領(lǐng)域?qū)ο?會(huì)包含當(dāng)前業(yè)務(wù)領(lǐng)域的各種事件,而?數(shù)據(jù)庫(kù)持久化對(duì)象?僅是包含了數(shù)據(jù)庫(kù)中對(duì)應(yīng) table 的數(shù)據(jù)字段信息。
視圖對(duì)象(View Object):視圖對(duì)象 VO 是面向前端用戶頁面的,一般會(huì)包含呈現(xiàn)給用戶的某個(gè)頁面/組件中所包含的所有數(shù)據(jù)字段信息。
數(shù)據(jù)傳輸對(duì)象(Data Transfer Object):數(shù)據(jù)傳輸對(duì)象 DTO 一般用于前端展示層與后臺(tái)服務(wù)層之間的數(shù)據(jù)傳遞,以一種媒介的形式完成 數(shù)據(jù)庫(kù)持久化對(duì)象 與 視圖對(duì)象 之間的數(shù)據(jù)傳遞。
這里通過一個(gè)簡(jiǎn)單的示意圖去解釋下這三種對(duì)象的具體使用場(chǎng)景,在這個(gè)示例的項(xiàng)目中,我省略了數(shù)據(jù)傳輸對(duì)象,將數(shù)據(jù)庫(kù)持久化對(duì)象直接轉(zhuǎn)換成頁面顯示的視圖對(duì)象。
2、組件加載
首先我們需要通過 Nuget 將 AutoMapper 加載到項(xiàng)目中,因?yàn)檫@個(gè)示例項(xiàng)目只包含一個(gè) MVC 的項(xiàng)目,并沒有多余的分層,所以這里需要將兩個(gè)使用到的 dll 都添加到這個(gè) MVC 項(xiàng)目中。
Install-Package AutoMapperInstall-Package AutoMapper.Extensions.Microsoft.DependencyInjection
這里我添加了?AutoMapper.Extensions.Microsoft.DependencyInjection?這個(gè)程序集,從這個(gè)程序集的名字就可以看出來,這個(gè)程序集主要是為了我們可以通過依賴注入的方式在項(xiàng)目中去使用 AutoMapper。
在 .NET Fx 的時(shí)代,我們使用 AutoMapper 時(shí),可能就像下面的代碼一樣,更多的是通過 Mapper 的幾個(gè)靜態(tài)方法來實(shí)現(xiàn)實(shí)體間的映射,不過在 .NET Core 程序中,我們首選還是采用依賴注入的方式去完成實(shí)體間的映射。
// 構(gòu)建實(shí)體映射規(guī)則Mapper.Initialize(cfg => cfg.CreateMap<OrderModel, OrderDto>());
// 實(shí)體映射
var order = new OrderModel{};
OrderDto dto = Mapper.Map<OrderModel,OrderDto>(order);
3、使用案例
因?yàn)樵鞠胍褂玫氖纠?xiàng)目是之前的 ingos-server 這個(gè)項(xiàng)目,由于目前自己有在學(xué)習(xí) DDD 的知識(shí),并且有在按照微軟的 eShopOnContainers 這個(gè)項(xiàng)目中基于 DDD 思想設(shè)計(jì)的框架,對(duì)自己的這個(gè) ingos-server 項(xiàng)目進(jìn)行 DDD 化的調(diào)整,嗯,其實(shí)就是照葫蘆畫瓢,所以目前整個(gè)項(xiàng)目被我改的亂七八糟的,不太適合作為示例項(xiàng)目了,所以這里新創(chuàng)建了一個(gè)比較單純的 ASP.NET Core MVC 項(xiàng)目來作為這篇文章的演示項(xiàng)目。
因?yàn)檫@個(gè)示例項(xiàng)目只是為了演示如何在 ASP.NET Core 項(xiàng)目中去使用 AutoMapper,所以這里并沒有進(jìn)行分層,整個(gè)示例頁面的運(yùn)行流程就是,PostController 中的 List Action 調(diào)用 PostAppService 類中的 GetPostLists 方法去獲取所有的文章數(shù)據(jù),同時(shí)在這個(gè)方法中會(huì)進(jìn)行實(shí)體映射,將我們從 PostDomain 中獲取到的 PO 對(duì)象轉(zhuǎn)換成頁面展示的 VO 對(duì)象,項(xiàng)目中每個(gè)文件夾的作用見下圖所示。
這里的示例項(xiàng)目是演示當(dāng)我們從數(shù)據(jù)庫(kù)獲取到需要的數(shù)據(jù)后,如何完成從 PO 到 VO 的實(shí)體映射,PostModel(PO)和 PostViewModel(VO)的類定義如下所示。
{
public Guid Id { get; set; }
public long SerialNo { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Image { get; set; }
public short CategoryCode { get; set; }
public bool IsDraft { get; set; }
public string Content { get; set; }
public DateTime ReleaseDate { get; set; }
public virtual IList<CommentModel> Comments { get; set; }
}
public class PostViewModel
{
public Guid Id { get; set; }
public long SerialNo { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public short CategoryCode { get; set; }
public string Category => CategoryCode == 1001 ? ".NET" : "雜談";
public string ReleaseDate { get; set; }
public short CommentCounts { get; set; }
public virtual int Count { get; set; }
}
首先我們需要?jiǎng)?chuàng)建一個(gè)實(shí)體映射的配置類,需要繼承于 AutoMapper 的 Profile 類,在無參構(gòu)造函數(shù)中,我們就可以通過 CreateMap 方法去創(chuàng)建兩個(gè)實(shí)體間的映射關(guān)系。
{
/// <summary>
/// ctor
/// </summary>
public PostProfile()
{
// 配置 mapping 規(guī)則
//
CreateMap<PostModel, PostViewModel>();
}
}
通過泛型的 CreateMap 方法就可以完成我們從 PostModel(PO) 到 PostViewModel(VO) 的實(shí)體映射。當(dāng)然,因?yàn)?AutoMapper 默認(rèn)是通過匹配字段名稱和類型進(jìn)行自動(dòng)匹配,所以如果你進(jìn)行轉(zhuǎn)換的兩個(gè)類的中的某些字段名稱不一樣,這里我們就需要進(jìn)行手動(dòng)的編寫轉(zhuǎn)換規(guī)則。
就像在這個(gè)需要進(jìn)行實(shí)體映射的示例代碼中,PostViewModel 中的 CommentCounts 字段是根據(jù) PostModel 中 CommentModel 集合的數(shù)據(jù)個(gè)數(shù)進(jìn)行賦值的,所以這里我們就需要對(duì)這個(gè)字段的轉(zhuǎn)換規(guī)則進(jìn)行修改。
在 AutoMapper 中,我們可以通過 ForMember 方法對(duì)映射規(guī)則做進(jìn)一步的加工。這里我們需要指明 PostViewModel 的 CommentCounts 字段的值是通過對(duì) PostModel 中的 Comments 信息進(jìn)行求和從而獲取到的,最終實(shí)現(xiàn)的轉(zhuǎn)換代碼如下所示。
{
/// <summary>
/// ctor
/// </summary>
public PostProfile()
{
// 配置 mapping 規(guī)則
//
CreateMap<PostModel, PostViewModel>()
.ForMember(destination => destination.CommentCounts, source => source.MapFrom(i => i.Comments.Count()));
}
}
ForMember 方法不僅可以進(jìn)行指定不同名稱的字段進(jìn)行轉(zhuǎn)換,也可以通過編寫規(guī)則實(shí)現(xiàn)字段類型的轉(zhuǎn)換。例如這里 PO 中的 ReleaseDate 字段其實(shí)是 DateTime 類型的,我們需要通過編寫規(guī)則將該字段對(duì)應(yīng)到 VO 中 string 類型的 ReleaseDate 字段上,最終的實(shí)現(xiàn)代碼如下所示。
{
/// <summary>
/// ctor
/// </summary>
public PostProfile()
{
// Config mapping rules
//
CreateMap<PostModel, PostViewModel>()
.ForMember(destination => destination.CommentCounts, source => source.MapFrom(i => i.Comments.Count()))
.ForMember(destination => destination.ReleaseDate, source => source.ConvertUsing(new DateTimeConverter()));
}
}
public class DateTimeConverter : IValueConverter<DateTime, string>
{
public string Convert(DateTime source, ResolutionContext context)
=> source.ToString("yyyy-MM-dd HH:mm:ss");
}
這里很多人可能習(xí)慣將所有的實(shí)體映射規(guī)則都放到同一個(gè) Profile 文件里面,因?yàn)檫@里采用是單體架構(gòu)的項(xiàng)目,所以整個(gè)項(xiàng)目中會(huì)存在不同的模塊,所以這里我是按照每個(gè)模塊去創(chuàng)建對(duì)應(yīng)的 Profile 文件。實(shí)際在 ingos-server 這個(gè)項(xiàng)目中的使用方式見下圖所示。
?當(dāng)我們創(chuàng)建好對(duì)應(yīng)的映射規(guī)則后,因?yàn)槲覀兪遣捎靡蕾囎⑷氲姆绞竭M(jìn)行使用,所以這里我們就需要將我們的匹配規(guī)則注入到 IServiceCollection 中。從之前加載的程序集的 github readme 描述中可以看到,我們需要將配置好的 Profile 類通過 AddAutoMapper 這個(gè)擴(kuò)展方法進(jìn)行注入。
因?yàn)槲覀冊(cè)趯?shí)際項(xiàng)目中可能存在多個(gè)自定義的 Profile 文件,而我們肯定是需要將這些自定義規(guī)則都注入到 IServiceCollection 中。所以我在 AddAutoMapper 這個(gè)方法的基礎(chǔ)上創(chuàng)建了一個(gè) AddAutoMapperProfiles 方法去注入我們的實(shí)體映射規(guī)則。
通過 AutoMapper 的說明我們可以看出來,所有的自定義的 Profile 類都是需要繼承于 AutoMapper 的 Profile 基類,所以這里我是采用反射的方式,通過獲取到程序集中所有繼承于 Profile 類的類文件進(jìn)行批量的注入到 IServiceCollection 中,具體的實(shí)現(xiàn)代碼如下所示。
/// Automapper 映射規(guī)則配置擴(kuò)展方法
/// </summary>
public static class AutoMapperExtension
{
public static IServiceCollection AddAutoMapperProfiles(this IServiceCollection services)
{
// 從 appsettings.json 中獲取包含配置規(guī)則的程序集信息
string assemblies = ConfigurationManager.GetConfig("Assembly:Mapper");
if (!string.IsNullOrEmpty(assemblies))
{
var profiles = new List<Type>();
// 獲取繼承的 Profile 類型信息
var parentType = typeof(Profile);
foreach (var item in assemblies.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
{
// 獲取所有繼承于 Profile 的類
//
var types = Assembly.Load(item).GetTypes()
.Where(i => i.BaseType != null && i.BaseType.Name == parentType.Name);
if (types.Count() != 0 || types.Any())
profiles.AddRange(types);
}
// 添加映射規(guī)則
if (profiles.Count() != 0 || profiles.Any())
services.AddAutoMapper(profiles.ToArray());
}
return services;
}
}
因?yàn)槲沂菍⑿枰虞d的程序集信息放到配置文件中的,所以這里我們只需要將包含 Profile 規(guī)則的程序集添加到對(duì)應(yīng)的配置項(xiàng)下面就可以了,此時(shí)如果包含多個(gè)程序集,則需要使用?`|`?進(jìn)行分隔。
"Assembly": {
"Mapper": "aspnetcore-automapper-tutorial"
}
}
當(dāng)我們將所有的實(shí)體映射規(guī)則注入到 IServiceCollection 中,就可以在代碼中使用這些實(shí)體映射規(guī)則。和其它通過依賴注入的接口使用方式相同,我們只需要在使用到的地方注入 IMapper 接口,然后通過 Map 方法就可以完成實(shí)體間的映射,使用的代碼如下。
public class PostAppService : IPostAppService
{#region Initialize
/// <summary>
///
/// </summary>
private readonly IPostDomain _post;
/// <summary>
///
/// </summary>
private readonly IMapper _mapper;
/// <summary>
/// ctor
/// </summary>
/// <param name="post"></param>
/// <param name="mapper"></param>
public PostAppService(IPostDomain post, IMapper mapper)
{
_post = post ?? throw new ArgumentNullException(nameof(post));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
#endregion Initialize
/// <summary>
/// 獲取所有的文章信息
/// </summary>
/// <returns></returns>
public IList<PostViewModel> GetPostLists()
{
var datas = _post.GetPostLists();
return _mapper.Map<IList<PostModel>, IList<PostViewModel>>(datas);
}
}
至此我們就實(shí)現(xiàn)了在 ASP.NET Core 項(xiàng)目中使用 AutoMapper,實(shí)現(xiàn)后的結(jié)果如下圖所示。
?三、總結(jié)
本篇文章主要是演示下如何在 ASP.NET Core 項(xiàng)目中去使用 AutoMapper 來實(shí)現(xiàn)實(shí)體間的映射,因?yàn)橹爸皇窃?.NET Fx 項(xiàng)目中有使用過這個(gè)組件,并沒有在 .NET Core 項(xiàng)目中使用,所以這次趁著國(guó)慶節(jié)假期就來嘗試如何在 .NET Core 項(xiàng)目中使用,整個(gè)組件使用起來其實(shí)是很簡(jiǎn)單的,但是使用后卻可以給我們?cè)趯?shí)際的項(xiàng)目開發(fā)中省很多的事,所以就把自己的使用方法分享出來,如果對(duì)你有些許的幫助的話,不勝榮幸~~~
原文鏈接:https://www.cnblogs.com/danvic712/p/11628523.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號(hào)文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的在 ASP.NET Core 项目中使用 AutoMapper 进行实体映射的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EF Core 实现读写分离的最佳方案
- 下一篇: .NET开发者必须学习.NET Core