AutoMapper 初體驗

昨天在討論完 ViewModel 與 Entity 之後,我們發現當下完查詢指令,將 Entity 中的屬性對應到 ViewModel 中相對應的屬性,如果要對應的屬性一多,用手動的方式一個一個的 Coding 的話,確實是一項苦力的工作,我們寫程式的目的是要幫助企業減省人力,除去做些規則且又重複的工作。當然在寫程式的過程當中,也會希望有工具(如果有現成的,就用現成的,沒有現成的就自己開發)能幫我們做些規則而又重複的工作。為了讓我們的工作能夠輕鬆愉快,還是求問一下 Google 好了,看來這一篇有提到 AutoMapper 這項工具,看來不錯用,今天花點時間就來體驗看看吧!

安裝 NuGet 套件

AutoMapper 套件安裝進來,請留意,因為是要在 ASP.NET Core 中使用,所以請記得選擇 AutoMapper.Extensions.Microsoft.DependencyInjection 套件:

註冊 AutoMapper 

安裝完 AutoMapper 套件之後,接著在 Demae.Api 專案的 Startup.cs 檔案中加入 services.AddAutoMapper(typeof(Startup));  註冊 AutoMapper 服務,如下所述:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    ........
    ........        

    services.AddAutoMapper(typeof(Startup));

    ........
    ........
            
}

接著在 AddressController 的建構函式中加入 IMapper mapper 如下所示:

public class AddressesController : Controller
{
    private readonly DemaeContext _context;
    private IMapper _mapper;

    public AddressesController(DemaeContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }
    .....
    .....
    .....
    .....


}

接著修改程式如下所示,使用 _mapper.Map<IEnumerable<AddressModel>>(addresses)  或是 _mapper.Map<AddressModel>(address)  將查詢到的 Entity 對應到 ViewModel 中:

[HttpGet]
public IEnumerable<AddressModel> GetAddresses()
{
    var addresses = _context.Addresses;
    return _mapper.Map<IEnumerable<AddressModel>>(addresses);            
}

        
[HttpGet("{id}")]
public async Task<IActionResult> GetAddress([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var address = 
        await _context.Addresses.SingleOrDefaultAsync(m => m.Id == id);

    if (address == null)
    {
        return NotFound();
    }

    return Ok(_mapper.Map<AddressModel>(address));
}

接著以 Postman 試著執行,卻發生如下圖所示的錯誤訊息:「Missing type map configuration」!看來 AutoMapper 不知道哪兩個物件是要互相對應的。

AutoMapper Profile

有關 AutoMapper Profile 請先參考這一篇(往後會有相關文章介紹),底下先針對解決目前的問題,說明做法。請先在 Demae.Core 專案加入 AutoMapper 套件:

接著在 Demae.Core 專案的 Models 資料夾中加入 AddressMapProfile 類別(可隨意命名,建議取個有意義的名稱)並加入程式碼如下:

namespace Demae.Core.Models
{
    public class AddressMapProfile : Profile
    {
        public AddressMapProfile()
        {
            CreateMap<Address, AddressModel>();
        }
    }
}

類別繼承 AutoMapper.Profile 類別,並在建構函式中加入 CreateMap<Address, AddressModel>();  設定 Address 為對應來源,而 AddressModel 為目標。

接著試著以 Postman 查詢,雖然沒有發生錯誤也有結果產出,但是除了 id 有值之外其他的屬性都是 null 值,看來 Entity 與 ViewModel 的屬性間有一定的命名規則必需遵行,相同的名稱可以毫無疑問地對應上,名稱不同就不知如何對應了。

修改 ViewModel 的屬性名稱如下:

public class AddressModel
{
    public int Id { get; set; }
    public string AreaCityName{ get; set; }
    public string AreaName { get; set; }
    public string Address { get; set; }
}

因為 Area 與 City 資料需要的資訊,所以在查詢時也要一起包含進來:

[HttpGet]
public IEnumerable<AddressModel> GetAddresses()
{
    var addresses = _context.Addresses
                       .Include(e=>e.Area)
                       .ThenInclude(e=>e.City);
    return _mapper.Map<IEnumerable<AddressModel>>(addresses);
}
        
[HttpGet("{id}")]
public async Task<IActionResult> GetAddress([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var address = 
        await _context.Addresses
                 .Include(e => e.Area)
                 .ThenInclude(e => e.City)
                 .SingleOrDefaultAsync(m => m.Id == id);

    if (address == null)
    {
        return NotFound();
    }

    return Ok(_mapper.Map<AddressModel>(address));
}

接著再試著以 Postman 查詢看看,縣市與鄉鎮區的資料有了,但是 address 還是 null 值,因為 address 是由多個屬性組合而成,所以不是只有單純的用命名規則就可對應得上,必需再加以設定。

AutoMapper Custom Mapping

修改一下 AutoMapper Profile 明確地指定目標類別 Address 屬性是由來源類別的哪些屬性所組合成的:

namespace Demae.Core.Models
{
    public class AddressMapProfile : Profile
    {
        public AddressMapProfile()
        {
            CreateMap<Address,AddressModel>()
                .ForMember(c => c.Address, opt => opt.MapFrom(a => a.Area.City.Name + a.Area.Name + a.Line));
        }
    }
}

再試著執行,成功了:

好吧!今天就學到這裡,明天就繼續努了了。