使用Roslyn在引用的程序集中获取接口实现

我想在我正在开发的框架中绕过一些经典的汇编扫描技术。

所以,说我已经定义了以下合同:

public interface IModule { } 

这存在于Contracts.dll

现在,如果我想发现这个接口的所有实现,我们可能会做类似于以下的事情:

 public IEnumerable DiscoverModules() { var contractType = typeof(IModule); var assemblies = AppDomain.Current.GetAssemblies() // Bad but will do var types = assemblies .SelectMany(a => a.GetExportedTypes) .Where(t => contractType.IsAssignableFrom(t)) .ToList(); return types.Select(t => Activator.CreateInstance(t)); } 

不是一个很好的例子,但它会做。

现在,这些类型的汇编扫描技术可能完全不足,而且它们都在运行时完成,通常会影响启动性能。

在新的DNX环境中,我们可以使用ICompileModule实例作为元编程工具,因此您可以将ICompileModule的实现ICompileModule到项目的Compiler\Preprocess文件夹中,并让它做一些时髦的事情。

我的目标是使用ICompileModule实现,以便在编译时执行我们在运行时所做的工作。

  • 在我的引用(包括编译和程序集)和我当前的编译中,发现所有可以实现的IModule实例
  • 创建一个类,让我们称之为ModuleList ,其实现产生每个模块的实例。
 public static class ModuleList { public static IEnumerable() GetModules() { yield return new Module1(); yield return new Module2(); } } 

将该类添加到编译单元后,我们可以调用它并在运行时获取静态模块列表,而不必搜索所有附加的程序集。 我们实际上卸载了编译器而不是运行时的工作。

鉴于我们可以通过References属性访问编译的所有引用,我无法看到如何获取任何有用的信息,例如可能访问字节代码,可能加载用于reflection的程序集,或类似的东西那。

思考?

思考?

是。

通常,在模块环境中,您希望根据上下文动态加载模块,或者 – 如果适用 – 来自第三方。 相比之下,使用Roslyn编译器框架,您基本上可以获得编译时的这些信息,从而将模块限制为静态引用。

就在昨天,我发布了动态加载工厂的代码。 属性,加载DLL的更新等在这里: GoF Factory的命名约定? 。 据我所知,它与你想要实现的非常相似。 这种方法的好处是你可以在运行时动态加载新的DLL。 如果你试试,你会发现它很快。

您还可以进一步限制您处理的assembly。 例如,如果您不处理mscorlibSystem.* (或者甚至可能是所有GAC程序集),它当然会更快地运行。 不过,正如我所说,它应该不是问题; 只扫描类型和属性是一个非常快速的过程。


好的,更多的信息和背景。

现在,你可能正在寻找一个有趣的谜题。 我可以理解,玩弄技术毕竟是很有趣的。 下面的答案(马修本人)将为您提供所需的所有信息。

如果您想平衡编译时代码生成与运行时解决方案的原理和缺点,请从我的经验中获取更多信息。

几年前,我认为让自己的C#解析器/生成器框架进行AST转换是个好主意。 它与Roslyn的function非常相似; 基本上它将整个项目转换为AST树,然后您可以对其进行规范化,生成代码,对面向方面的编程内容进行额外检查并添加新的语言结构。 我最初的目标是在C#中添加对面向方面编程的支持,我有一些实际的应用程序。 我将为您提供详细信息,但对于此上下文,可以说基于代码生成的模块/工厂也是我尝试过的事情之一。

性能,灵活性和代码量(在非库解决方案中)是我在运行时和编译时决策之间加权决策的关键方面。 让我们分解它们:

  • 表现 。 这很重要,因为我不能假设库代码不在关键路径上。 运行时每个appdomain实例将花费几毫秒。 (有关如何/为何的说明,请参阅下文)。
  • 灵活性 。 它们在属性/扫描方面同样具有灵活性。 但是,在运行时,您在更改规则方面有更多可能性(例如,动态插入模块等)。 我有时会使用它,尤其是基于配置,因此我不必在同一个解决方案中开发所有内容(因为效率低下)。
  • 代码量 。 根据经验,较少的代码通常是更好的代码。 如果你做得对,两者都会产生你在课堂上需要的单一属性。 换句话说,这两种解决方案都给出了相同的结果。

但是关于性能的说明是有序的。 我在代码中使用reflection不仅仅是工厂模式。 我基本上在这里有一个包含所有设计模式(以及大量其他东西)的“工具”的扩展库。 举几个例子:我在运行时为工厂,责任链,装饰器,模拟,缓存/代理(以及更多)之类的东西自动生成代码。 其中一些已经要求我扫描组件。

作为一个简单的经验法则,我总是使用一个属性来表示必须更改某些内容。 您可以利用这个优势:只需将具有属性(正确的程序集/命名空间)的每个类型存储在某个地方的单例/字典中,就可以使应用程序更快(因为您只需要扫描一次)。 从Microsoft扫描程序集也不是很有用。 我在大型项目上做了很多测试,发现在我发现的最坏情况下, 扫描在应用程序的启动时间增加了大约10毫秒 。 请注意,这只是每个appdomain实例化一次,这意味着你甚至都不会注意到它。

激活类型实际上是你将获得的唯一“真实”性能惩罚。 可以通过发出IL代码来优化该惩罚; 这真的不那么难。 最终结果是它在这里没有任何区别。

总结一下,这是我的结论:

  • 表现 :微不足道的差异。
  • 灵活性 :运行时获胜。
  • 代码量 :微不足道的差异。

根据我的经验,尽管许多框架希望支持即插即用架构,这些架构可以从组件的下降中受益,但实际情况是,实际上并没有一整套用例。

如果它不适用,您可能想要考虑不首先使用工厂模式。 此外,如果它适用,我已经表明它没有真正的缺点,即:如果你正确实施它。 不幸的是,我必须承认,我已经看到很多不好的实现。

至于它实际上并不适用,我认为这只是部分正确。 嵌入式数据提供程序非常常见(逻辑上遵循3层架构)。 我还使用工厂来连接诸如通信/ WCF API,缓存提供者和装饰器之类的东西(逻辑上遵循n层架构)。 一般来说,它可用于您能想到的任何类型的提供商。

如果参数是它会给性能带来损失,那么您基本上想要删除整个类型扫描过程。 就个人而言,我将其用于大量不同的事情,最着名的是缓存,统计,日志记录和配置。 此外,我认为业绩下滑可以忽略不计。

只需2美分; HTH。

因此,我采用这一挑战的方法意味着深入了解整个参考源,以了解Roslyn可用的不同类型。

要为最终解决方案添加前缀,让我们创建模块接口,我们将其放在Contracts.dll

 public interface IModule { public int Order { get; } public string Name { get; } public Version Version { get; } IEnumerable GetServices(); } public interface IModuleProvider { IEnumerable GetModules(); } 

我们还定义了基础提供者:

 public abstract class ModuleProviderBase { private readonly List _modules = new List(); protected ModuleProviderBase() { Setup(); } public IEnumerable GetModules() { return _modules.OrderBy(m => m.Order); } protected void AddModule() where T : IModule, new() { var module = new T(); _modules.Add(module); } protected virtual void Setup() { } } 

现在,在这个体系结构中,模块实际上不仅仅是一个描述符,所以不应该依赖它,它只是表达它提供的服务。

现在,在DefaultLogger.dll ,示例模块可能看起来像:

 public class DefaultLoggerModule : ModuleBase { public override int Order { get { return ModuleOrder.Level3; } } public override IEnumerable GetServices() { yield return ServiceDescriptor.Instance(new DefaultLoggerFactory()); } } 

为简洁起见,我省略了ModuleBase的实现。

现在,在我的Web项目中,我添加对Contracts.dllDefaultLogger.dll的引用,然后添加我的模块提供程序的以下实现:

 public partial class ModuleProvider : ModuleProviderBase { } 

现在,我的ICompileModule

 using T = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree; using F = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using K = Microsoft.CodeAnalysis.CSharp.SyntaxKind; public class DiscoverModulesCompileModule : ICompileModule { private static MethodInfo GetMetadataMethodInfo = typeof(PortableExecutableReference) .GetMethod("GetMetadata", BindingFlags.NonPublic | BindingFlags.Instance); private static FieldInfo CachedSymbolsFieldInfo = typeof(AssemblyMetadata) .GetField("CachedSymbols", BindingFlags.NonPublic | BindingFlags.Instance); private ConcurrentDictionary _cache = new ConcurrentDictionary(); public void AfterCompile(IAfterCompileContext context) { } public void BeforeCompile(IBeforeCompileContext context) { // Firstly, I need to resolve the namespace of the ModuleProvider instance in this current compilation. string ns = GetModuleProviderNamespace(context.Compilation.SyntaxTrees); // Next, get all the available modules in assembly and compilation references. var modules = GetAvailableModules(context.Compilation).ToList(); // Map them to a collection of statements var statements = modules.Select(m => F.ParseStatement("AddModule<" + module + ">();")).ToList(); // Now, I'll create the dynamic implementation as a private class. var cu = F.CompilationUnit() .AddMembers( F.NamespaceDeclaration(F.IdentifierName(ns)) .AddMembers( F.ClassDeclaration("ModuleProvider") .WithModifiers(F.TokenList(F.Token(K.PartialKeyword))) .AddMembers( F.MethodDeclaration(F.PredefinedType(F.Token(K.VoidKeyword)), "Setup") .WithModifiers( F.TokenList( F.Token(K.ProtectedKeyword), F.Token(K.OverrideKeyword))) .WithBody(F.Block(statements)) ) ) ) .NormalizeWhitespace(indentation("\t")); var tree = T.Create(cu); context.Compilation = context.Compilation.AddSyntaxTrees(tree); } // Rest of implementation, described below } 

基本上这个模块做了几个步骤;

1 – 解析Web项目中ModuleProvider实例的名称空间,例如SampleWeb
2 – 通过引用发现所有可用模块,这些模块作为字符串集合返回,例如new [] {“SampleLogger.DefaultLoggerModule”}
3 – 将它们转换为AddModule();
4 – 创建我们要添加到编译中的ModuleProviderpartial实现:

 namespace SampleWeb { partial class ModuleProvider { protected override void Setup() { AddModule(); } } } 

那么,我是如何发现可用模块的呢? 有三个阶段:

1 – 引用的程序集(例如,通过NuGet提供的程序集)
2 – 引用的编译(例如,解决方案中引用的项目)。
3 – 当前编译中的模块声明。

对于每个引用的编译,我们重复上面的内容。

 private IEnumerable GetAvailableModules(Compilation compilation) { var list = new List(); string[] modules = null; // Get the available references. var refs = compilation.References.ToList(); // Get the assembly references. var assemblies = refs.OfType().ToList(); foreach (var assemblyRef in assemblies) { if (!_cache.TryGetValue(assemblyRef, out modules)) { modules = GetAssemblyModules(assemblyRef); _cache.AddOrUpdate(assemblyRef, modules, (k, v) => modules); list.AddRange(modules); } else { // We've already included this assembly. } } // Get the compilation references var compilations = refs.OfType().ToList(); foreach (var compliationRef in compilations) { if (!_cache.TryGetValue(compilationRef, out modules)) { modules = GetAvailableModules(compilationRef.Compilation).ToArray(); _cache.AddOrUpdate(compilationRef, modules, (k, v) => modules); list.AddRange(modules); } else { // We've already included this compilation. } } // Finally, deal with modules in the current compilation. list.AddRange(GetModuleClassDeclarations(compilation)); return list; } 

因此,要获得程序集引用的模块:

 private IEnumerable GetAssemblyModules(PortableExecutableReference reference) { var metadata = GetMetadataMethodInfo.Invoke(reference, nul) as AssemblyMetadata; if (metadata != null) { var assemblySymbol = ((IEnumerable)CachedSymbolsFieldInfo.GetValue(metadata)).First(); // Only consider our assemblies? Sample*? if (assemblySymbol.Name.StartsWith("Sample")) { var types = GetTypeSymbols(assemblySymbol.GlobalNamespace).Where(t => Filter(t)); return types.Select(t => GetFullMetadataName(t)).ToArray(); } } return Enumerable.Empty(); } 

我们需要在这里做一点反思,因为GetMetadata方法不公开,后来,当我们抓取元数据时, CachedSymbols字段也是非公开的,因此更多的反映。 在识别可用内容方面,我们需要从CachedSymbols属性中获取IEnumerable 。 这为我们提供了引用程序集中的所有缓存符号。 Roslyn为我们做了这个,所以我们可以滥用它:

 private IEnumerable GetTypeSymbols(INamespaceSymbol ns) { foreach (var typeSymbols in ns.GetTypeMembers().Where(t => !t.Name.StartsWith("<"))) { yield return typeSymbol; } foreach (var namespaceSymbol in ns.GetNamespaceMembers()) { foreach (var typeSymbol in GetTypeSymbols(ns)) { yield return typeSymbol; } } } 

GetTypeSymbols方法遍历命名空间并发现所有类型。 然后我们将结果链接到filter方法,这确保它实现了我们所需的接口:

 private bool Filter(ITypeSymbol symbol) { return symbol.IsReferenceType && !symbol.IsAbstract && !symbol.IsAnonymousType && symbol.AllInterfaces.Any(i => i.GetFullMetadataName(i) == "Sample.IModule"); } 

GetFullMetadataName是一个实用工具方法:

 private static string GetFullMetadataName(INamespaceOrTypeSymbol symbol) { ISymbol s = symbol; var builder = new StringBuilder(s.MetadataName); var last = s; while (!!IsRootNamespace(s)) { builder.Insert(0, '.'); builder.Insert(0, s.MetadataName); s = s.ContainingSymbol; } return builder.ToString(); } private static bool IsRootNamespace(ISymbol symbol) { return symbol is INamespaceSymbol && ((INamespaceSymbol)symbol).IsGlobalNamespace; } 

接下来,当前编译中的模块声明:

 private IEnumerable GetModuleClassDeclarations(Compilation compilation) { var trees = compilation.SyntaxTrees.ToArray(); var models = trees.Select(compilation.GetSemanticModel(t)).ToArray(); for (var i = 0; i < trees.Length; i++) { var tree = trees[i]; var model = models[i]; var types = tree.GetRoot().DescendantNodes().OfType().ToList(); foreach (var type in types) { var symbol = model.GetDeclaredSymbol(type) as ITypeSymbol; if (symbol != null && Filter(symbol)) { yield return GetFullMetadataName(symbol); } } } } 

这就是它! 所以,现在在编译时,我的ICompileModule将:

  • 发现所有可用的模块
  • 使用所有已知引用的模块实现ModuleProvider.Setup方法的覆盖。

这意味着我可以添加我的启动:

 public class Startup { public ModuleProvider ModuleProvider = new ModuleProvider(); public void ConfigureServices(IServiceCollection services) { var descriptors = ModuleProvider.GetModules() // Ordered .SelectMany(m => m.GetServices()); // Apply descriptors to services. } public void Configure(IApplicationBuilder app) { var modules = ModuleProvider.GetModules(); // Ordered. // Startup code. } } 

大量过度设计,相当复杂,但我觉得有点棒!