在分析Orchard的模块加载之前,先简要说一下因为Orchard中的模块并不是都被根(启动)项目所引用的,所以当Orchard需要加载一个模块时首先需要保证该模块所依赖的其它程序集能够被找到,那么才能正确的加载一个模块。在上一篇文章中对Orchard如何通过Module.txt以及Theme.txt完成对相应模块、引用依赖的分析和探测工作,并最后生成一个ExtensionLoadingContext,其主要包含了模块程序集路径以及其依赖路径等等详细信息。
在Orchard官方文档中提到"~/App_Data/Dependencies"目录,说它是一个在传统的bin目录之外ASP.NET用来查找附加程序集的目录,而Orchard就是将拓展模块及其依赖复制到这个目录,然后完成模块程序集加载工作的,"~/App_Data/Dependencies"目录是通过在根项目的web.config中加入以下节点实现的:
1 <runtime>
2 ...
3 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
4 <probing privatePath="App_Data/Dependencies" />
5 ...
6 </assemblyBinding>
7 </runtime>
所以简单的来说Orchard后续的处理工作就是,将它们拓展及其引用经过一系列的处理后(如版本问题),通过它们的路径直接复制到App_Data/Dependencies目录下,然后进行加载工作。接下来就对这一过程进行详细分析。
以下代码位于ExtensionLoaderCoordinator的SetupExtensions方法中,它的作用就是根据已有的ExtensionLoadingContext去处理可用的拓展。
1 // For all existing extensions in the site, ask each loader if they can
2 // load that extension.
3 foreach (var extension in context.AvailableExtensions) {
4 ProcessExtension(context, extension);
5 }
它的处理过程如下:
先放上全量代码:
1 private void ProcessExtension(ExtensionLoadingContext context, ExtensionDescriptor extension) {
2 var extensionProbes = context.AvailableExtensionsProbes.ContainsKey(extension.Id) ?
3 context.AvailableExtensionsProbes[extension.Id] :
4 Enumerable.Empty<ExtensionProbeEntry>();
5 // materializes the list
6 extensionProbes = extensionProbes.ToArray();
7
8 if (Logger.IsEnabled(LogLevel.Debug)) {
9 Logger.Debug("Loaders for extension \"{0}\": ", extension.Id);
10 foreach (var probe in extensionProbes) {
11 Logger.Debug(" Loader: {0}", probe.Loader.Name);
12 Logger.Debug(" VirtualPath: {0}", probe.VirtualPath);
13 Logger.Debug(" VirtualPathDependencies: {0}", string.Join(", ", probe.VirtualPathDependencies));
14 }
15 }
16
17 var moduleReferences =
18 context.AvailableExtensions
19 .Where(e =>
20 context.ReferencesByModule.ContainsKey(extension.Id) &&
21 context.ReferencesByModule[extension.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(e.Id, r.Name)))
22 .ToList();
23
24 var processedModuleReferences =
25 moduleReferences
26 .Where(e => context.ProcessedExtensions.ContainsKey(e.Id))
27 .Select(e => context.ProcessedExtensions[e.Id])
28 .ToList();
29
30 var activatedExtension = extensionProbes.FirstOrDefault(
31 e => e.Loader.IsCompatibleWithModuleReferences(extension, processedModuleReferences)
32 );
33
34 var previousDependency = context.PreviousDependencies.FirstOrDefault(
35 d => StringComparer.OrdinalIgnoreCase.Equals(d.Name, extension.Id)
36 );
37
38 if (activatedExtension == null) {
39 Logger.Warning("No loader found for extension \"{0}\"!", extension.Id);
40 }
41
42 var references = ProcessExtensionReferences(context, activatedExtension);
43
44 foreach (var loader in _loaders) {
45 if (activatedExtension != null && activatedExtension.Loader.Name == loader.Name) {
46 Logger.Information("Activating extension \"{0}\" with loader \"{1}\"", activatedExtension.Descriptor.Id, loader.Name);
47 loader.ExtensionActivated(context, extension);
48 }
49 else if (previousDependency != null && previousDependency.LoaderName == loader.Name) {
50 Logger.Information("Deactivating extension \"{0}\" from loader \"{1}\"", previousDependency.Name, loader.Name);
51 loader.ExtensionDeactivated(context, extension);
52 }
53 }
54
55 if (activatedExtension != null) {
56 context.NewDependencies.Add(new DependencyDescriptor {
57 Name = extension.Id,
58 LoaderName = activatedExtension.Loader.Name,
59 VirtualPath = activatedExtension.VirtualPath,
60 References = references
61 });
62 }
63
64 // Keep track of which loader we use for every extension
65 // This will be needed for processing references from other dependent extensions
66 context.ProcessedExtensions.Add(extension.Id, activatedExtension);
67 }
View Code
根据拓展信息获取相应的探测信息:
1 var extensionProbes = context.AvailableExtensionsProbes.ContainsKey(extension.Id) ?
2 context.AvailableExtensionsProbes[extension.Id] :
3 Enumerable.Empty<ExtensionProbeEntry>();
4
5 // materializes the list
6 extensionProbes = extensionProbes.ToArray();
一般来说只有~\Modules和自定义的拓展模块目录下的拓展模块会存在多个探测信息,因为它们被预编译加载器和动态编译加载器同时探测到(bin目录和csproj文件)。
处理模块引用(模块引用模块):
遍历所有的拓展模块,查找是否依赖于其它模块,如果存在依赖,那么查看所存在的依赖是否已经被处理。
1 var moduleReferences =
2 context.AvailableExtensions
3 .Where(e =>
4 context.ReferencesByModule.ContainsKey(extension.Id) &&
5 context.ReferencesByModule[extension.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(e.Id, r.Name)))
6 .ToList();
7
8 var processedModuleReferences =
9 moduleReferences
10 .Where(e => context.ProcessedExtensions.ContainsKey(e.Id))
11 .Select(e => context.ProcessedExtensions[e.Id])
12 .ToList();
如果对应依赖的模块已经被处理那么就根据被处理的模块来决定使用哪一个探测实体,因为探测实体的Loader不一样,从而可能与已经处理的模块不兼容,所以ExtensionProbes之前已经通过优先级和最新修改时间进行过排序,而判断是否兼容就是根据这个顺序依次选择优先级最高且兼容的探测信息 (注:所有的Loader中,只有预编译加载器不兼容动态编译加载器(看Loader的IsCompatibleWithModuleReferences方法),其余都兼容。所以换句话来说只有一些模块被确定使用动态编译加载器时,后续依赖它的模块的使用预编译加载器的探测信息不会被使用,会转向优先级较低的其它加载器) 。
1 var activatedExtension = extensionProbes.FirstOrDefault(
2 e => e.Loader.IsCompatibleWithModuleReferences(extension, processedModuleReferences)
3 );
找出是否被上一次运行的依赖信息:
1 var previousDependency = context.PreviousDependencies.FirstOrDefault(
2 d => StringComparer.OrdinalIgnoreCase.Equals(d.Name, extension.Id)
3 );
处理引用:
step 1. 获取所有依赖的名称:
1 var referenceNames = (context.ReferencesByModule.ContainsKey(activatedExtension.Descriptor.Id) ?
2 context.ReferencesByModule[activatedExtension.Descriptor.Id] :
3 Enumerable.Empty<ExtensionReferenceProbeEntry>())
4 .Select(r => r.Name)
5 .Distinct(StringComparer.OrdinalIgnoreCase);
step 2. 遍历并根据名称处理所有依赖(选取最适合的引用):
1. 判断依赖是否是已处理模块,如果是添加DependencyReferenceDescriptor并返回。
2. 如果不是已处理模块,则继续判断依赖是否存在于根项目的bin目录下,如果存在直接返回。
3. 如果以上条件均不符合,则在context.ReferencesByName中根据名称查找,然后根据最新修改时间排序,选择最新的引用文件,如果context.ProcessedReferences不存在该引用,那么将其添加到context.ProcessedReferences中,并调用ReferenceActivated方法。最后添加DependencyReferenceDescriptor并返回。
1 public override void ReferenceActivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry) {
2 if (string.IsNullOrEmpty(referenceEntry.VirtualPath))
3 return;
4
5 string sourceFileName = _virtualPathProvider.MapPath(referenceEntry.VirtualPath);
6
7 // Copy the assembly if it doesn't exist or if it is older than the source file.
8 bool copyAssembly =
9 !_assemblyProbingFolder.AssemblyExists(referenceEntry.Name) ||
10 File.GetLastWriteTimeUtc(sourceFileName) > _assemblyProbingFolder.GetAssemblyDateTimeUtc(referenceEntry.Name);
11
12 if (copyAssembly) {
13 context.CopyActions.Add(() => _assemblyProbingFolder.StoreAssembly(referenceEntry.Name, sourceFileName));
14
15 // We need to restart the appDomain if the assembly is loaded
16 if (_hostEnvironment.IsAssemblyLoaded(referenceEntry.Name)) {
17 Logger.Information("ReferenceActivated: Reference \"{0}\" is activated with newer file and its assembly is loaded, forcing AppDomain restart", referenceEntry.Name);
18 context.RestartAppDomain = true;
19 }
20 }
21 }
注:引用的处理实际上是对预编译和动态编译加载器探测到的模块进行处理,因为其它模块都是被根项目引用的,一旦编译根项目,所有被引用的项目均会被自动编译并将结果放置到bin目录下,而预编译和动态编译加载器则需要通过ReferenceActivated方法,将一个把程序集复制到App_Data/Dependencies目录下的方法添加到context.CopyActions中。
处理拓展模块:
1 foreach (var loader in _loaders) {
2 if (activatedExtension != null && activatedExtension.Loader.Name == loader.Name) {
3 Logger.Information("Activating extension \"{0}\" with loader \"{1}\"", activatedExtension.Descriptor.Id, loader.Name);
4 loader.ExtensionActivated(context, extension);
5 }
6 else if (previousDependency != null && previousDependency.LoaderName == loader.Name) {
7 Logger.Information("Deactivating extension \"{0}\" from loader \"{1}\"", previousDependency.Name, loader.Name);
8 loader.ExtensionDeactivated(context, extension);
9 }
10 }
11
12 if (activatedExtension != null) {
13 context.NewDependencies.Add(new DependencyDescriptor {
14 Name = extension.Id,
15 LoaderName = activatedExtension.Loader.Name,
16 VirtualPath = activatedExtension.VirtualPath,
17 References = references
18 });
19 }
20
21 // Keep track of which loader we use for every extension
22 // This will be needed for processing references from other dependent extensions
23 context.ProcessedExtensions.Add(extension.Id, activatedExtension);
根据上面代码可以看到处理模块的步骤主要是遍历所有的Loader,然后通过activatedExtension和previousDependency进行匹配,然后对相应的模块进行激活和禁用操作(这里要注意的是如果它们Loader一致,那么只需要调用ExtensionActivated方法即可,否则activatedExtension对应的loader调用activatedExtension,previousDependency调用ExtensionDeactivated禁用之前的同名模块)。最后创建DependencyDescriptor,并将对应的拓展放到ProcessedExtensions中,以供后续模块判断是否引用了已经被处理的模块。
ExtensionActivated:拓展的激活仅仅是针对预编译和动态编译加载器,其中预编译的激活是将一个复制程序集的方法添加到ctx.CopyActions中和处理引用的方法一致,而动态编译的则仅仅是添加了一个是否需要重启应用域的判断。
1 public override void ExtensionActivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension) {
2 if (_reloadWorkaround.AppDomainRestartNeeded) {
3 Logger.Information("ExtensionActivated: Module \"{0}\" has changed, forcing AppDomain restart", extension.Id);
4 ctx.RestartAppDomain = _reloadWorkaround.AppDomainRestartNeeded;
5 }
6 }
ExtensionDeactivated:是针对预编译和引用加载器,将一个把原有拓展程序集删除的方法添加到ctx.DeleteActions中。
预编译:
1 public override void ExtensionDeactivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension) {
2 if (_assemblyProbingFolder.AssemblyExists(extension.Id)) {
3 ctx.DeleteActions.Add(
4 () => {
5 Logger.Information("ExtensionDeactivated: Deleting assembly \"{0}\" from probing directory", extension.Id);
6 _assemblyProbingFolder.DeleteAssembly(extension.Id);
7 });
8
9 // We need to restart the appDomain if the assembly is loaded
10 if (_hostEnvironment.IsAssemblyLoaded(extension.Id)) {
11 Logger.Information("ExtensionDeactivated: Module \"{0}\" is deactivated and its assembly is loaded, forcing AppDomain restart", extension.Id);
12 ctx.RestartAppDomain = true;
13 }
14 }
15 }
View Code
引用:
1 private void DeleteAssembly(ExtensionLoadingContext ctx, string moduleName) {
2 var assemblyPath = _virtualPathProvider.Combine("~/bin", moduleName + ".dll");
3 if (_virtualPathProvider.FileExists(assemblyPath)) {
4 ctx.DeleteActions.Add(
5 () => {
6 Logger.Information("ExtensionRemoved: Deleting assembly \"{0}\" from bin directory (AppDomain will restart)", moduleName);
7 File.Delete(_virtualPathProvider.MapPath(assemblyPath));
8 });
9 ctx.RestartAppDomain = true;
10 }
11 }
View Code
执行复制和删除命令:
上面的代码分析可以看到,当激活或禁用一个引用或者模块时,特殊的模块加载器会将一个复制程序集或删除程序集的方法添加到ExtensionLoadingContext中对应的DeleteActions和CopyActions中,而ProcessContextCommands方法就是去处理这些Action。
1 private void ProcessContextCommands(ExtensionLoadingContext ctx) {
2 Logger.Information("Executing list of operations needed for loading extensions...");
3 foreach (var action in ctx.DeleteActions) {
4 action();
5 }
6
7 foreach (var action in ctx.CopyActions) {
8 action();
9 }
10 }
保存依赖信息:将解析后的DependencyDescriptor存放至对应的xml文件中:
1 // And finally save the new entries in the dependencies folder
2 _dependenciesFolder.StoreDescriptors(context.NewDependencies);
3 _extensionDependenciesManager.StoreDependencies(context.NewDependencies, desc => GetExtensionHash(context, desc));
应用域的重启:如果最后需要重启应用域,那么通过DefaultHostEnvironment的RestartAppDomain方法,通过在bin目录修改marker.txt文件或修改web.config的方法实现应用自动重启的功能。(一般在引用被覆盖和动态编译加载器监控到文件发生变化时需要重启)。
上面的分析完成了Orchard整个拓展和依赖的处理过程。
小结:
Orchard拓展和依赖的处理过程是用于保证所有的拓展模块程序集及其依赖程序集文件都存在于App_Data/Dependencies目录下(其中Core和reference类型的拓展模块已经在根项目的bin目录中无需进行额外处理),如果出现引用程序集被覆盖的情况则需要重启应用域。
整个处理过程主要是针对没有被依赖的,放置在Modules目录下的拓展模块。因为其余模块都被根项目引用,所以在根项目编译时它们的依赖及模块程序集已经会被自动处理了。