AutoMapper的原始碼分析

NeilHu發表於2020-12-26

最近有一個小專案需要提供介面給第三方使用,介面會得到一個大的XML的字串大約有8個物件100多個欄位,在對映到Entity只能通過反射來賦值避免重複的賦值,但是明顯感覺到效能下降嚴重,因為以前接觸過AutoMapper所以寫了一篇部落格記錄其中的實現原理。

github 上可以下載AutoMapper 原始碼,直接開啟sln 檔案 就可以看到專案結構。

專案結構非常清晰,一個AutoMapper的實現類,兩個測試庫一個單元測試一個整合測試還有一個效能測試。下面就是AutoMapper的測試程式碼。

            var config = new MapperConfiguration(cfg => 
                cfg.CreateMap<TestSource, TestDestination>()
            );
            var mapper1 = config.CreateMapper();
            var fooDto = mapper1.Map<TestDestination>(new TestSource{
                MyProperty = 1
            });

專案不大,除錯的時候你就可以明白AutoMapper的實現原理,下面就是一個大致的分析過程。

程式碼建立了一個MapperConfiguration物件,裡面傳遞了一個Action到構造方法,而這個Action裡建立了Map 的資訊,這是最簡單的demo所以使用預設的欄位Map方式。

 1   public MapperConfiguration(Action<IMapperConfigurationExpression> configure)
 2             : this(Build(configure))
 3         {
 4         }
 5 
 6         private static MapperConfigurationExpression Build(Action<IMapperConfigurationExpression> configure)
 7         {
 8             var expr = new MapperConfigurationExpression();
 9             configure(expr);
10             return expr;
11         }

上面就是構造方法處理的邏輯,實際上就是build方法創造一個MapperConfigurationExpression物件,然後把這個物件傳給Action生成Mapper,這個Mapper的意思就是source Type和destination Type的欄位對映物件。我們繼續檢視這個第8行的程式碼在new這個物件的時候做了什麼初始化操作。這個MapperConfigurationExpression繼承了抽象類Profile,而在這個父類的構造方法裡初始化了IMemberConfiguration類,這個是為了制定map規則物件,預設是DefaultMember這個物件。下面是這個建立完MapperConfigurationExpression後呼叫構造方法。

 1         public MapperConfiguration(MapperConfigurationExpression configurationExpression)
 2         {
 3             _mappers = configurationExpression.Mappers.ToArray();
 4             _resolvedMaps = new LockingConcurrentDictionary<TypePair, TypeMap>(GetTypeMap);
 5             _executionPlans = new LockingConcurrentDictionary<MapRequest, Delegate>(CompileExecutionPlan);
 6             _validator = new ConfigurationValidator(this, configurationExpression);
 7             ExpressionBuilder = new ExpressionBuilder(this);
 8 
 9             ServiceCtor = configurationExpression.ServiceCtor;
10             EnableNullPropagationForQueryMapping = configurationExpression.EnableNullPropagationForQueryMapping ?? false;
11             MaxExecutionPlanDepth = configurationExpression.Advanced.MaxExecutionPlanDepth + 1;
12             ResultConverters = configurationExpression.Advanced.QueryableResultConverters.ToArray();
13             Binders = configurationExpression.Advanced.QueryableBinders.ToArray();
14             RecursiveQueriesMaxDepth = configurationExpression.Advanced.RecursiveQueriesMaxDepth;
15 
16             Configuration = new ProfileMap(configurationExpression);
17             Profiles = new[] { Configuration }.Concat(configurationExpression.Profiles.Select(p => new ProfileMap(p, configurationExpression))).ToArray();
18 
19             configurationExpression.Features.Configure(this);
20 
21             foreach (var beforeSealAction in configurationExpression.Advanced.BeforeSealActions)
22                 beforeSealAction?.Invoke(this);
23             Seal();
24         }

 在這個構造方法裡會將之前建立的MapperConfigurationExpression轉化成當前物件的各個欄位,其中LockingConcurrentDictionary是自己實現的執行緒安全的字典物件,構造方法傳遞取值的規則,重要的是Profiles欄位,這個儲存之前的action 傳遞的MapperConfigurationExpression物件,這個物件也就是source type和destination type 物件的相關資訊。最後重要的是seal 方法,根據相關資訊生產lambda程式碼。

        public void Seal(IConfigurationProvider configurationProvider)
        {
            if(_sealed)
            {
                return;
            }
            _sealed = true;

            _inheritedTypeMaps.ForAll(tm => _includedMembersTypeMaps.UnionWith(tm._includedMembersTypeMaps));
            foreach (var includedMemberTypeMap in _includedMembersTypeMaps)
            {
                includedMemberTypeMap.TypeMap.Seal(configurationProvider);
                ApplyIncludedMemberTypeMap(includedMemberTypeMap);
            }
            _inheritedTypeMaps.ForAll(tm => ApplyInheritedTypeMap(tm));

            _orderedPropertyMaps = PropertyMaps.OrderBy(map => map.MappingOrder).ToArray();
            _propertyMaps.Clear();

            MapExpression = CreateMapperLambda(configurationProvider, null);

            Features.Seal(configurationProvider);
        }

上面就是核心程式碼,根據之前註冊的相關資訊生成一個lambda程式碼。CreateDestinationFunc建立一個lambda表示式,內容是new 一個destination物件,在CreateAssignmentFunc繼續擴充套件欄位賦值的lambda內容,其中欄位map規則就是之前新建MapperConfiguration

物件的時候建立的,如果沒有注入就是預設的匹配規則,CreateMapperFunc會產生一些規則,比如預設值賦值等等。這些生成的lambda物件會存在Typemap 物件的MapExpression中。

 1 public LambdaExpression CreateMapperLambda(HashSet<TypeMap> typeMapsPath)
 2         {
 3             var customExpression = TypeConverterMapper() ?? _typeMap.CustomMapFunction ?? _typeMap.CustomMapExpression;
 4             if(customExpression != null)
 5             {
 6                 return Lambda(customExpression.ReplaceParameters(Source, _initialDestination, Context), Source, _initialDestination, Context);
 7             }
 8 
 9             CheckForCycles(typeMapsPath);
10 
11             if(typeMapsPath != null)
12             {
13                 return null;
14             }
15 
16             var destinationFunc = CreateDestinationFunc();
17             
18             var assignmentFunc = CreateAssignmentFunc(destinationFunc);
19 
20             var mapperFunc = CreateMapperFunc(assignmentFunc);
21 
22             var checkContext = CheckContext(_typeMap, Context);
23             var lambaBody = checkContext != null ? new[] {checkContext, mapperFunc} : new[] {mapperFunc};
24 
25             return Lambda(Block(new[] {_destination}, lambaBody), Source, _initialDestination, Context);
26         }

後來的呼叫map的時候就會這些lambda方法,在之前說過,生成的lambda表示式會在記憶體中動態生成IL程式碼,這些花費的時間只有一次就是seal呼叫的時候,後面的時候去執行程式碼速度會快得多,因為這些動態生成的il程式碼在執行的時候速度和手寫程式碼執行的一樣的,所以程式碼執行的時候是非常快的,遠遠高於反射的速度,所以這就是AutoMapper執行速度快的原因。這個專案挺小的並且程式碼工整可讀性挺強的,希望大家在閱讀原始碼能夠學的更多。最後謝謝大家。

相關文章