[C#.NET] Use Reflection / AutoMapper achieve Object Mapping

[C#.NET] Use Reflection / AutoMapper achieve Object Mapping

在三層式架構中,中間插了 ViewModel ,ViewModel 是由 UI 所呈現的資料設計出來,ViewModel 與 Model 會不斷的轉換,達到資料交換、呈現,這樣的架構令不少開發人員疑惑:為什麼 ViewModel 的欄位跟 Model 差不多,還要多一個 ViewModel,直接把 Model 放在 UI 不行嗎?

 

image

 

 

 

Entity Model 包含了所有資料庫的欄位,UI 可能跟本不需要 Model 所有的欄位,直接綁定到 UI 則是造成了網路資源的浪費;為了要讓 UI 的欄位也能在使用 Entity Model,資料庫又會把 UI 的欄位加進去 ,UI 又是最常修改的,全部攪成一團,真的是很痛苦

ViewModel 解決了 UI 與 Model 的直接依賴,讓架構更有彈性、提高開發效率,UI、資料庫和商業邏輯並行開發、提高可測試性、避免SQL欄位訊息洩露,所以通常不會在 UI 直接使用 Entity Model,當然這不是絕對的,還是要看使用情境

 

 

本篇情境:

解決手動處理 Model to ViewModel、ViewModel to Model

 

本篇章節:

反射處理物件映射

使用 AutoMapper 處理物件映射

 

 

反射處理物件映射

ViewModel 與 Model 欄位名稱相同

ViewModel 很好,但手動處理對應關係,真的是件苦工,欄位越多,漏打的機會就越高,錯誤越高,開發人員越抗拒,明明就一樣,為什麼要寫兩次,如下程式碼:

 

{
    var account = new Account();
    account.UserId = accountViewModel.UserId;
    account.Password = accountViewModel.Password;
    //TODO:後面還有好多欄位
    return account;
}

 

上面的例子還好,只要懂的例用反射,上面的問題很容易解掉,如下程式碼

TSource 代表的是來源物件,迴圈是依照 TSource 的欄位數量去執行的

TTarget 代表目標物件

 

    where TSource : class,new()
{
    var sourceType = sourceInstance.GetType();
    var targetType = typeof(TTarget);
    var sourceProperties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
    var targetProperties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

    var targetInstance = Activator.CreateInstance<TTarget>();
    foreach (var sourceProperty in sourceProperties)
    {
        var sourcePropertyName = sourceProperty.Name;
        var sourceValue = sourceProperty.GetValue(sourceInstance);

        foreach (var targetProperty in targetProperties)
        {
            var targetPropertyName = targetProperty.Name;

            if (sourcePropertyName == targetPropertyName)
            {
                if (sourceProperty.PropertyType == targetProperty.PropertyType)
                {
                    targetProperty.SetValue(targetInstance, sourceValue);
                    break;
                }
            }
        }
    }
    return targetInstance;
}

 

 

ViewModel 與 Model 欄位名稱不同

ViewModel 跟 Entity Model 資料庫實體名稱相同, 就達不到資訊隱藏的效果,所以一般會建議將它們兩個的欄位名稱取不一樣,也就是上面的邏輯不能用了,要再改一下,我想要在 ViewModel 屬性上加一個 Map Attribute,用它來決定映射的欄位名稱

@ObjectMappingAttribute.cs

首先,先增加一個 ObjectMappingAttribute

 

public class ObjectMappingAttribute : Attribute
{
    public string MappingName { get; set; }
    public ObjectMappingAttribute(string mappingName)
    {
        this.MappingName = mappingName;
    }
}

 

@Account.cs

Account 是 Entity Model,它會對應到資料庫

 

{
    public Guid Id { get; set; }

    public string UserId { get; set; }

    public string Password { get; set; }
}

 

@AccountTwViewModel.cs

AccountTwViewModel 是提供給 UI 綁定的物件

 
TwViewModel
{
    [ObjectMapping("UserId")]
    public string 帳號 { get; set; }

    [ObjectMapping("Password")]
    public string 密碼 { get; set; }
}

 

@Utility.cs

Migration 方法用來處理資料對應,跟上面的 Migration 方法多了 Attribute 的處理

 

    where TSource : class,new()
{
    var sourceType = sourceInstance.GetType();
    var targetType = typeof(TTarget);
    var sourceProperties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
    var targetProperties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
    var targetInstance = Activator.CreateInstance<TTarget>();
    var mappingAttributeType = typeof(ObjectMappingAttribute);
    foreach (var sourceProperty in sourceProperties)
    {
        var sourcePropertyName = sourceProperty.Name;
        var sourceValue = sourceProperty.GetValue(sourceInstance);

        foreach (var targetProperty in targetProperties)
        {
            var targetPropertyName = targetProperty.Name;
            if (sourcePropertyName == targetPropertyName)
            {
                if (sourceProperty.PropertyType == targetProperty.PropertyType)
                {
                    targetProperty.SetValue(targetInstance, sourceValue);
                    break;
                }
            }
            var mappingAttributes = targetProperty.GetCustomAttributes(mappingAttributeType, false);
            if (mappingAttributes.Any())
            {
                var mappingAttributePropertyName = ((ObjectMappingAttribute)mappingAttributes[0]).PropertyName;
                if (mappingAttributePropertyName == sourcePropertyName)
                {
                    if (sourceProperty.PropertyType == targetProperty.PropertyType)
                    {
                        targetProperty.SetValue(targetInstance, sourceValue);
                        break;
                    }
                }

            }

        }
    }
    return targetInstance;
}

調用 Migration 方法
 

public void Migration_Account_AccountTwViewModel_Test()
{
    Account source = new Account();
    source.UserId = "yao123";
    source.Password = "1234";
    Utility utility = new Utility();
    var target = utility.Migration<Account, AccountTwViewModel>(source);
    Assert.AreEqual(source.UserId, target.帳號);
    Assert.AreEqual(source.Password, target.密碼);
}

 

完整測試程式碼:

https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.AutoMapViewModel/UnitTestProject1/UnitTest1.cs

 

使用 AutoMapper 處理物件映射

AutoMapper 使用起來非常簡單,網路上有很多的資源,從 Nuget Server 上可看到,這已經被很多人下載使用了

image

 

ViewModel 與 Model 欄位名稱相同

若兩個物件欄位名稱相同,它會幫我們處理兩個物件的內容

  • Mapper.CreateMap 方法,用來宣告要處理的兩個物件型別,它的方法簽章如下:
    public static IMappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>();

 

  • Mapper.Map 方法,用來處理兩個物件的內容對應,它的方法簽章如下:
    public static TDestination Map<TSource, TDestination>(TSource source);

測試程式碼如下:

 

public void Account_AccountViewModel_Test()
{
    Account account = new Account();
    account.UserId = "yao123";
    account.Password = "1234";
    Mapper.CreateMap<Account, AccountViewModel>();
    var actual = Mapper.Map<Account, AccountViewModel>(account);
    Assert.AreEqual(account.UserId, actual.UserId);
    Assert.AreEqual(account.Password, actual.Password);
}

 

完整程式碼如下:
https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.AutoMapViewModel/UnitTestProject1/UnitTest1.cs

 

ViewModel 與 Model 欄位名稱不同

若兩個物件欄位名稱不相同,要跟 AutoMapper 講對應關係或是不想要對應,ForMember 方法就是在做這件事,它的方法簽章如下:
IMappingExpression<TSource, TDestination> ForMember(Expression<Func<TDestination, object>> destinationMember, Action<IMemberConfigurationExpression<TSource>> memberOptions);

使用方式一樣跟上面一樣,只是多了 ForMember

 

public void Account_AccountTwViewModel_Test()
{
    Account account = new Account();
    account.UserId = "yao123";
    account.Password = "1234";
    Mapper.CreateMap<Account, AccountTwViewModel>()
          .ForMember(target => target.帳號, option => option.MapFrom(source => source.UserId))
          .ForMember(target => target.密碼, option => option.MapFrom(source => source.Password));
    var actual = Mapper.Map<Account, AccountTwViewModel>(account);
    Assert.AreEqual(account.UserId, actual.帳號);
    Assert.AreEqual(account.Password, actual.密碼);
}

 

完整程式碼如下:
https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.AutoMapViewModel/UnitTestProject1/UnitTest2.cs

 

文章出自:https://www.dotblogs.com.tw/yc421206/archive/2014/12/12/147617.aspx

範例專案:https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.AutoMapViewModel/

運行測試:Ctrl + R,Ctrl + A

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo