C# Lambda Mapper

LazYu發表於2024-03-28

場景

在業務開發中,從倉儲至應用,中間一般還有一層模型對映服務,其中的核心主鍵俺管他叫對映器(Mapper)。現在業界已經有很多Mapper的實現方案了,多為自實現如反射同名對映,或者使用大名鼎鼎的AutoMapper

AutoMapper底層是有一些最佳化的,比很多自實現的反射同名對映要快,反射會佔用大量時間。但是不管AutoMapper再快,肯定是沒有直接賦值快( 直接new Data(){ Name = dataDto.Name }

一般來說,除非動態動編譯層,否則除了Lambda之外,很難有比原生賦值還快的方法了。

Lambda Mapper

我的LambdaMapper的實現是基於我領域模型與檢視模型的基本定義來編寫的,讀者可能需要按照各自的業務進行修改。

如我的領域模型:

    public class ApplicationMenuItem : EntityBase<Guid>
    {
        /// <summary>
        /// 訪問時的預設引數min
        /// </summary>
        public string Parameter { get; set; }
        /// <summary>
        /// 導航地址
        /// </summary>
        [StringLength(150)]
        public string Path { get; set; }
        /// <summary>
        /// 圖示
        /// </summary>
        public string Icon { get; set; }
        /// <summary>
        /// 上級選單
        /// </summary>
        public ApplicationMenuItem Parent { get; set; }
        /// <summary>
        /// 是否不顯示在導航欄
        /// </summary>
        public bool IsHidden { get; set; }
        /// <summary>
        /// 歸屬的客戶端
        /// </summary>
        public CddApplication CddApplication { get; set; }
    }

Dto:

    public class ApplicationMenuItemDto : DtoBase<Guid>
    {
        /// <summary>
        /// 訪問時的預設引數
        /// </summary>
        public string Parameter { get; set; }
        /// <summary>
        /// 導航地址
        /// </summary>
        public string Path { get; set; }
        /// <summary>
        /// 圖示
        /// </summary>
        public string Icon { get; set; }
        /// <summary>
        /// 上級選單
        /// </summary>
        public string ParentId { get; set; }
        public string ParentName { get; set; }
        /// <summary>
        /// 歸屬的客戶端
        /// </summary>
        public string CddApplicationId { get; set; }
        public string CddApplicationName { get; set; }

兩種模型相互的對映的規則

  1. 領域模型 => 檢視模型
    1.1 同名同型別屬性對映,同時提供不同型別但是同名型別的屬性型別轉換後賦值
    1.2 導航屬性對映,如上面ApplicationMenuItemParent.IdParent.Name會對映到ApplicationMenuItemDtoParentIdParentName(當然這裡面還有一些空值判斷,如果Parent為空則不對映)
    1.3 層級對映,類似於使用AutoMapper的時候,會將Parent.Parent.Name對映至ParentParentName

  2. 檢視模型 => 領域模型
    1.1 同名同型別屬性對映,同時提供不同型別但是同名型別的屬性型別轉換後賦值
    1.2 從檢視模型的屬性中,拼接回導航屬性,如上面ApplicationMenuItemDtoParentIdParentName會變成語句Parent = new ApplicationMenuItem(){ Id = applicationMenuItemDto.ParentId, Name = applicationMenuItemDto.ParentName}

程式碼實現

public class Mapper<TEntity, TEntityVM>
    where TEntity : class, new()
    where TEntityVM : class, new()
{
    /// <summary>
    /// 對於名稱空間的約束
    /// </summary>
    private const string configNamespace = "您的名稱空間";

    /// <summary>
    /// 轉檢視模型的lambda表示式快取
    /// </summary>
    private static Func<TEntity, TEntityVM> _mapToEntityVMCache;
    /// <summary>
    /// 轉實體模型的Lambda表示式快取
    /// </summary>
    private static Func<TEntityVM, TEntity> _mapToEntityCache;

    static Mapper()
    {
        _mapToEntityVMCache = GetMapToEntityVMFunc();
        _mapToEntityCache = GetMapToEntityFunc();
    }

    /// <summary>
    /// 判斷型別是否為可操作的列表型別
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    private static bool _isList(Type type)
    {
        if (typeof(System.Collections.IList).IsAssignableFrom(type))
        {
            return true;
        }
        foreach (var it in type.GetInterfaces())
        {
            if (it.IsGenericType && typeof(IList<>) == it.GetGenericTypeDefinition())
                return true;
        }
        return false;
    }

    /// <summary>
    /// 生成對映成檢視模型的Lambda表示式
    /// </summary>
    /// <returns></returns>
    private static Func<TEntity, TEntityVM> GetMapToEntityVMFunc()
    {
        ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "x");
        // 一組用於對映的模型繫結表示式
        List<MemberBinding> memberBindingList = new List<MemberBinding>();
        foreach (var entityProperty in typeof(TEntity).GetProperties())
        {
            // 這裡的item是檢視模型對應的屬性
            var item = typeof(TEntityVM).GetProperty(entityProperty.Name);
            if (item == null)
            {
                // 如果是自定義型別
                if (entityProperty.PropertyType.FullName.Contains(configNamespace) && entityProperty.PropertyType.IsClass
                    && _isList(entityProperty.PropertyType) == false)
                {
                    var rootName = "";
                    var nextLevelMemberExpression = Expression.Property(parameterExpression, entityProperty);
                    var res = GetSubPropertyMapConfig<TEntityVM>(memberBindingList, rootName, nextLevelMemberExpression, entityProperty,
                        new List<MemberExpression>() { nextLevelMemberExpression });
                    memberBindingList = res.Item1;
                }
                // 否則就是真的沒有
                else
                {
                    continue;
                }
            }
            else if (item.PropertyType == entityProperty.PropertyType)
            {
                // 如果是不能寫的屬性
                if (!item.CanWrite)
                    continue;
                MemberExpression eProperty = Expression.Property(parameterExpression, entityProperty.Name);
                MemberBinding memberBindingResult = Expression.Bind(item, eProperty);
                memberBindingList.Add(memberBindingResult);

                if (entityProperty.PropertyType.IsEnum)
                {
                    var enumNameProp = typeof(TEntityVM).GetProperty(entityProperty.Name + "Name");
                    // 搜尋一下是否有字尾加Name的字串值
                    if (enumNameProp != null)
                    {
                        MethodInfo methodInfo = entityProperty.PropertyType.GetMethod("ToString", new Type[] { });
                        var resultExpression = Expression.Call(eProperty, methodInfo);
                        MemberBinding enumNameMemberBindingResult = Expression.Bind(enumNameProp, resultExpression);
                        memberBindingList.Add(enumNameMemberBindingResult);
                    }
                }

            }
            // 其他型別轉string
            else if (item.PropertyType != entityProperty.PropertyType && item.PropertyType == typeof(string))
            {
                // 如果是不能寫的屬性
                if (!item.CanWrite)
                    continue;
                MemberExpression memberExp = Expression.Property(parameterExpression, entityProperty.Name);
                // 獲取ToString方法,一般來說,都應該是String型別
                MethodInfo methodInfo = memberExp.Type.GetMethod("ToString", new Type[] { });
                var resultExpression = Expression.Call(memberExp, methodInfo);
                // ToString()之後再傳輸過去
                MemberBinding memberBindingResult = Expression.Bind(item, resultExpression);
                memberBindingList.Add(memberBindingResult);
            }
            // string轉其他型別
            else if (item.PropertyType != entityProperty.PropertyType && entityProperty.PropertyType == typeof(string))
            {
                // 如果是不能寫的屬性
                if (!item.CanWrite)
                    continue;
                MemberExpression property = Expression.Property(parameterExpression, typeof(TEntity).GetProperty(entityProperty.Name));
                MethodInfo parseMethod = item.PropertyType.GetMethod("Parse", new Type[] { typeof(string) });
                MethodCallExpression resultExpression = Expression.Call(parseMethod, new List<Expression>() { property });
                // p=> p.item.Name = p.item.Name
                MemberBinding memberBindingResult = Expression.Bind(item, resultExpression);
                memberBindingList.Add(memberBindingResult);
            }
        }
        // p => new TOut(){item.Name = p.item.Name}
        MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TEntityVM)), memberBindingList.ToArray());
        Expression<Func<TEntity, TEntityVM>> lambda = Expression.Lambda<Func<TEntity, TEntityVM>>(memberInitExpression, new ParameterExpression[] { parameterExpression });

        return lambda.Compile();
    }


    /// <summary>
    /// 二級型別對映處理
    /// </summary>
    /// <typeparam name="Target"></typeparam>
    /// <param name="memberBindings"></param>
    /// <param name="sourceType"></param>
    /// <returns></returns>
    private static Tuple<List<MemberBinding>, string, MemberExpression, List<MemberExpression>> GetSubPropertyMapConfig<Target>(List<MemberBinding> memberBindings, string rootName,
        MemberExpression nowLevelMemberExpression,
        PropertyInfo sourceSubTypeInfo,
        List<MemberExpression> historyNullValueValidateMembers)
    {
        var targetModelProps = typeof(Target).GetProperties();
        var sourceSubTypeProps = sourceSubTypeInfo.PropertyType.GetProperties();

        // 根名稱,根據層級不斷疊加
        rootName += sourceSubTypeInfo.Name;

        if (historyNullValueValidateMembers == null)
        {
            historyNullValueValidateMembers = new List<MemberExpression>();
        }

        var subClassProps = sourceSubTypeProps.Where(x => x.PropertyType.FullName.Contains(configNamespace) && x.PropertyType.IsClass).ToList();
        var baseClassProps = sourceSubTypeProps.Where(x => !x.PropertyType.FullName.Contains(configNamespace)).ToList();

        // 二級的二級屬性對映
        foreach (var baseClassProp in baseClassProps)
        {
            var item = targetModelProps.FirstOrDefault(x => x.Name == rootName + baseClassProp.Name);
            if (item == null)
            {
                continue;
            }

            Expression nullValueValidateMemberExpression = null;
            if (historyNullValueValidateMembers == null)
            {
                return new Tuple<List<MemberBinding>, string, MemberExpression, List<MemberExpression>>(memberBindings, rootName, nowLevelMemberExpression, new List<MemberExpression>());
            }
            // 空值判斷表示式拼接
            else
            {
                for (var i = 0; i < historyNullValueValidateMembers.Count; i++)
                {
                    if (i == 0)
                    {
                        nullValueValidateMemberExpression = Expression.Equal(historyNullValueValidateMembers[i], Expression.Constant(null));
                    }
                    else
                    {
                        nullValueValidateMemberExpression = Expression.OrElse(nullValueValidateMemberExpression, Expression.Equal(historyNullValueValidateMembers[i], Expression.Constant(null)));
                    }
                }
            }

            // 型別一致才進行對映
            if (item.PropertyType == baseClassProp.PropertyType)
            {
                MemberExpression memberExp = Expression.Property(nowLevelMemberExpression, baseClassProp);

                Expression conditionExpr = Expression.Condition(
                      nullValueValidateMemberExpression,
                      Expression.Default(memberExp.Type),
                      memberExp
                    );
                MemberBinding memberBindingResult = Expression.Bind(item, conditionExpr);
                memberBindings.Add(memberBindingResult);
            }
            else if (item.PropertyType == typeof(string))
            {
                MemberExpression memberExp = Expression.Property(nowLevelMemberExpression, baseClassProp);
                // 獲取ToString方法,一般來說,都應該是String型別
                MethodInfo methodInfo = memberExp.Type.GetMethod("ToString", new Type[] { });
                var resultExpression = Expression.Call(memberExp, methodInfo);

                Expression conditionExpr = Expression.Condition(
                  nullValueValidateMemberExpression,
                  Expression.Default(typeof(string)),
                  resultExpression
                );
                MemberBinding memberBindingResult = Expression.Bind(item, conditionExpr);
                memberBindings.Add(memberBindingResult);
            }
            else if (baseClassProp.PropertyType == typeof(string))
            {
                MemberExpression memberExp = Expression.Property(nowLevelMemberExpression, baseClassProp);
                // 獲取ToString方法,一般來說,都應該是String型別
                MethodInfo methodInfo = item.PropertyType.GetMethod("Parse", new Type[] { typeof(string) });
                var resultExpression = Expression.Call(methodInfo, new List<Expression>() { memberExp });

                Expression conditionExpr = Expression.Condition(
                  nullValueValidateMemberExpression,
                  Expression.Default(typeof(string)),
                  resultExpression
                );
                MemberBinding memberBindingResult = Expression.Bind(item, conditionExpr);
                memberBindings.Add(memberBindingResult);
            }
        }

        foreach (var subClassProp in subClassProps)
        {
            var nextLevelMemberExpression = Expression.Property(nowLevelMemberExpression, subClassProp);
            if (!historyNullValueValidateMembers.Any(x => x.Type == nextLevelMemberExpression.Type))
            {
                historyNullValueValidateMembers.Add(nextLevelMemberExpression);
            }
            // 如果二級仍是當前型別,不繼續做處理,只對映一層
            if(subClassProp.PropertyType == sourceSubTypeInfo.PropertyType)
            {
                continue;
            }
            var res = GetSubPropertyMapConfig<Target>(memberBindings, rootName, nextLevelMemberExpression, subClassProp, historyNullValueValidateMembers);
        }

        return new Tuple<List<MemberBinding>, string, MemberExpression, List<MemberExpression>>(memberBindings, rootName, nowLevelMemberExpression, historyNullValueValidateMembers);
    }

    /// <summary>
    /// 生成對映成實體模型的Lambda表示式
    /// </summary>
    /// <returns></returns>
    private static Func<TEntityVM, TEntity> GetMapToEntityFunc()
    {
        ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntityVM), "p");
        List<MemberBinding> memberBindingList = new List<MemberBinding>();
        // 遍歷實體模型
        foreach (var item in typeof(TEntity).GetProperties())
        {
            // 同名的情況下
            // 如果是不能寫的屬性
            if (!item.CanWrite)
                continue;
            // 如果輸出型別沒有該屬性
            if (typeof(TEntityVM).GetProperty(item.Name) == null)
            {
                // 如果是自定義型別
                if (item.PropertyType.FullName.Contains(configNamespace) && !item.PropertyType.IsEnum && _isList(item.PropertyType) == false)
                {

                    List<MemberBinding> tempMemberBindingList = new List<MemberBinding>();

                    // 獲取實體模型二級

                    // p=>p.item.Name,拿到實體模型巢狀屬性Id和Name
                    var theIdProp = typeof(TEntityVM).GetProperty(item.Name + "Id");
                    if (theIdProp != null)
                    {
                        MemberExpression idProperty = Expression.Property(parameterExpression, theIdProp);
                        if (idProperty == null) continue;
                        // TEntity.Id
                        var id = item.PropertyType.GetProperty("Id");
                        if (id == null) continue;
                        // 檢視模型string轉其他型別
                        if (id.PropertyType != theIdProp.PropertyType && theIdProp.PropertyType == typeof(string))
                        {
                            MethodInfo toGuid = id.PropertyType.GetMethod("Parse", new Type[] { typeof(string) });
                            MethodCallExpression expression = Expression.Call(toGuid, new List<Expression>() { idProperty });
                            Expression idConditionExpr = Expression.Condition(
                              Expression.Equal(Expression.Property(parameterExpression, item.Name + "Id"), Expression.Constant(null)),
                              Expression.Default(id.PropertyType),
                              expression
                            );
                            MemberBinding idMemberBinding = Expression.Bind(id, idConditionExpr);
                            tempMemberBindingList.Add(idMemberBinding);
                        }
                        else
                        {
                            MemberBinding idMemberBinding = Expression.Bind(item.PropertyType.GetProperty("Id"), idProperty);
                            tempMemberBindingList.Add(idMemberBinding);
                        }
                    }

                    var theNameProp = typeof(TEntityVM).GetProperty(item.Name + "Name");
                    if (theNameProp != null)
                    {
                        MemberExpression NameProperty = Expression.Property(parameterExpression, theNameProp);
                        MemberBinding NameMemberBinding = Expression.Bind(item.PropertyType.GetProperty("Name"), NameProperty);
                        tempMemberBindingList.Add(NameMemberBinding);
                    }


                    MemberInitExpression includeBoInitExpression = Expression.MemberInit(Expression.New(item.PropertyType), tempMemberBindingList.ToArray());
                    if (typeof(TEntityVM).GetProperty(item.Name + "Id").PropertyType == typeof(string))
                    {
                        Expression includeBoInitExpressionC = Expression.Condition(
                          Expression.Equal(Expression.Property(parameterExpression, item.Name + "Id"), Expression.Constant(null)),
                          Expression.Default(item.PropertyType),
                          includeBoInitExpression
                        );
                        MemberBinding memberString = Expression.Bind(item, includeBoInitExpressionC);
                        memberBindingList.Add(memberString);
                    }
                    else if (typeof(TEntityVM).GetProperty(item.Name + "Id").PropertyType == typeof(Guid)
                        || typeof(TEntityVM).GetProperty(item.Name + "Id").PropertyType == typeof(int))
                    {
                        Expression includeBoInitExpressionC = Expression.Condition(
                          Expression.Equal(Expression.Property(parameterExpression, item.Name + "Id"), Expression.Default(typeof(TEntityVM).GetProperty(item.Name + "Id").PropertyType)),
                          Expression.Default(item.PropertyType),
                          includeBoInitExpression
                        );
                        MemberBinding memberString = Expression.Bind(item, includeBoInitExpressionC);
                        memberBindingList.Add(memberString);
                    }
                    continue;
                }
                // 如果是字串就繫結為""
                else if (item.PropertyType == typeof(string))
                {
                    // 將這個屬性繫結到""
                    MemberBinding memberString = Expression.Bind(item, Expression.Constant(""));
                    memberBindingList.Add(memberString);
                }

                continue;
            }
            else if (item.PropertyType == typeof(TEntityVM).GetProperty(item.Name).PropertyType)
            {
                MemberExpression property = Expression.Property(parameterExpression, typeof(TEntityVM).GetProperty(item.Name));
                // p=> p.item.Name = p.item.Name
                MemberBinding memberBinding = Expression.Bind(item, property);
                memberBindingList.Add(memberBinding);
            }
            // 基本型別 轉string
            else if (item.PropertyType != typeof(TEntityVM).GetProperty(item.Name).PropertyType
                && typeof(TEntityVM).GetProperty(item.Name).PropertyType != typeof(string))
            {
                MemberExpression property = Expression.Property(parameterExpression, typeof(TEntityVM).GetProperty(item.Name));
                MethodInfo toString = property.Type.GetMethod("ToString", new Type[] { });
                MethodCallExpression resultExpression = Expression.Call(property, toString);
                // p=> p.item.Name = p.item.Name
                MemberBinding memberBinding = Expression.Bind(item, resultExpression);
                memberBindingList.Add(memberBinding);

            }
            // string轉 基本型別
            else if (item.PropertyType != typeof(TEntityVM).GetProperty(item.Name).PropertyType
                && typeof(TEntityVM).GetProperty(item.Name).PropertyType == typeof(string))
            {
                MemberExpression property = Expression.Property(parameterExpression, typeof(TEntityVM).GetProperty(item.Name));
                MethodInfo toGuid = item.PropertyType.GetMethod("Parse", new Type[] { typeof(string) });
                MethodCallExpression resultExpression = Expression.Call(toGuid, new List<Expression>() { property });
                Expression guidValueExp = Expression.Condition(
                  Expression.Equal(property, Expression.Constant(null)),
                  Expression.Default(item.PropertyType),
                  resultExpression
                );
                // p=> p.item.Name = p.item.Name
                MemberBinding memberBinding = Expression.Bind(item, guidValueExp);
                memberBindingList.Add(memberBinding);
            }

        }
        // p => new TOut(){item.Name = p.item.Name}
        MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TEntity)), memberBindingList.ToArray());
        Expression<Func<TEntityVM, TEntity>> lambda = Expression.Lambda<Func<TEntityVM, TEntity>>(memberInitExpression, new ParameterExpression[] { parameterExpression });

        return lambda.Compile();
    }

    public static TEntityVM MapToApiEntity(TEntity tIn, Action<TEntity, TEntityVM> mapperExtensionAction = null)
    {
        if (tIn == null)
        {
            return null;
        }
        if (mapperExtensionAction == null)
        {
            return _mapToEntityVMCache(tIn);
        }
        else
        {
            var res = _mapToEntityVMCache(tIn);
            mapperExtensionAction(tIn, res);
            return res;
        }

    }

    public static TEntity MapToEntity(TEntityVM tout, Action<TEntityVM, TEntity> mapperExtensionAction = null)
    {
        if (tout == null)
        {
            return null;
        }
        if (mapperExtensionAction == null)
        {
            return _mapToEntityCache(tout);
        }
        else
        {
            var res = _mapToEntityCache(tout);
            mapperExtensionAction(tout, res);
            return res;
        }
    }

    public static List<TEntityVM> MapToApiEntity(IEnumerable<TEntity> tIns, Action<TEntity, TEntityVM> mapperExtensionAction = null)
    {
        if (tIns == null)
        {
            return null;
        }
        if (mapperExtensionAction == null)
        {
            return tIns.ToList().Select(x => _mapToEntityVMCache(x)).ToList();
        }
        else
        {
            return tIns.ToList().Select(x =>
            {
                var temp = _mapToEntityVMCache(x);
                mapperExtensionAction(x, temp);
                return temp;
            }).ToList();
        }
    }

    public static List<TEntity> MapToEntity(IEnumerable<TEntityVM> touts, Action<TEntityVM, TEntity> mapperExtensionAction = null)
    {
        if (touts == null)
        {
            return null;
        }
        if (mapperExtensionAction == null)
        {
            return touts.ToList().Select(x => _mapToEntityCache(x)).ToList();
        }
        else
        {
            return touts.ToList().Select(x =>
            {
                var temp = _mapToEntityCache(x);
                mapperExtensionAction(x, temp);
                return temp;
            }).ToList();
        }
    }
}

單句解釋我就不做了,本質上就是表示式樹的拼接,多看就理解了,實際上最核心的就是:

MemberBinding memberBinding = Expression.Bind(item, resultExpression);

以及一些情況下,需要先做不同型別的同名屬性的對映後再進行Binding

MemberExpression property = Expression.Property(parameterExpression, typeof(TEntityVM).GetProperty(item.Name));
MethodInfo toString = property.Type.GetMethod("ToString", new Type[] { });
MethodCallExpression resultExpression = Expression.Call(property, toString);
// p=> p.item.Name = p.item.Name
MemberBinding memberBinding = Expression.Bind(item, resultExpression);
memberBindingList.Add(memberBinding);

或者導航屬性拆解對映時,需要外層做一個判斷導航屬性是否為空的判斷表示式

Expression includeBoInitExpressionC = Expression.Condition(Expression.Equal(Expression.Property(parameterExpression, item.Name + "Id"), 
    Expression.Default(typeof(TEntityVM).GetProperty(item.Name + "Id").PropertyType)),
    Expression.Default(item.PropertyType),
    includeBoInitExpression
);
MemberBinding memberString = Expression.Bind(item, includeBoInitExpressionC);
memberBindingList.Add(memberString);

最終生成Lambda Func,並且快取在靜態變數中,這樣在一個服務的執行週期中,每兩種實體的對映只需要編譯一次Lambda表示式,往後的對映速度都會是最接近原生的,因為本質上已經編譯成Func來執行了。

MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TEntity)), memberBindingList.ToArray());
Expression<Func<TEntityVM, TEntity>> lambda = Expression.Lambda<Func<TEntityVM, TEntity>>(memberInitExpression, new ParameterExpression[] { parameterExpression });
return lambda.Compile();

好久前測過一次速度,找不到資料了,現在懶得搞了,有讀者自己測出來的話可以發一下出來,

相關文章