17

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

所以,假设我定义了以下合同:

public interface IModule
{

}

这存在于 say 中Contracts.dll

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

public IEnumerable<IModule> 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实例用作元编程工具,因此您可以将实现捆绑ICompileModuleCompiler\Preprocess项目的文件夹中,并让它做一些时髦的事情。

我的目标是使用一个ICompileModule实现来完成我们在运行时所做的工作,而不是在编译时。

  • 在我的参考资料(编译和程序集)和我当前的编译中,发现所有可实例化的实例IModule
  • 创建一个类,让我们ModuleList用一个产生每个模块实例的实现来调用它。
public static class ModuleList
{
    public static IEnumerable<IModule>() GetModules()
    {
        yield return new Module1();
        yield return new Module2();
    }
}

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

鉴于我们可以通过该References属性访问编译的所有引用,我看不到如何获得任何有用的信息,例如可能访问字节码,可能加载程序集以进行反射,或类似的东西.

想法?

4

2 回答 2

6

想法?

是的。

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

就在昨天,我发布了动态加载工厂的代码。属性,用于加载 DLL 的更新等:GoF Factory 的命名约定?. 据我了解,这与您要实现的目标非常相似。这种方法的好处是您可以在运行时动态加载新的 DLL。如果你尝试一下,你会发现它相当快。

您还可以进一步限制您处理的程序集。例如,如果您不处理mscorlibSystem.*(甚至可能是所有 GAC 程序集),它当然会工作得更快。不过,正如我所说,这不应该是一个问题。仅扫描类型和属性是一个相当快的过程。


好的,更多信息和上下文。

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

如果您想平衡编译时代码生成与运行时解决方案的优缺点,这里有更多来自我的经验的信息。

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

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

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

不过,关于性能的说明是有序的。我在代码中使用反射不仅仅是工厂模式。我基本上在这里有一个广泛的“工具”库,其中包括所有设计模式(以及大量其他东西)。举几个例子:我在运行时自动为工厂、责任链、装饰器、模拟、缓存/代理(等等)生成代码。其中一些已经要求我扫描程序集。

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

激活类型实际上是您将获得的唯一“真正的”性能损失。可以通过发出 IL 代码来优化该惩罚;这真的没那么难。最终结果是它不会在这里产生任何影响。

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

  • 性能:差别不大。
  • 灵活性:运行时获胜。
  • 代码量:差别不大。

根据我的经验,虽然很多框架都希望支持即插即用的架构,这些架构可以从组件中受益,但现实情况是,并没有大量的用例可以真正适用。

如果它不适用,您可能首先要考虑不使用工厂模式。此外,如果它适用,我已经证明它没有真正的缺点,那就是:如果你正确地实施它。不幸的是,我必须在这里承认我已经看到了很多糟糕的实现。

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

如果论点是它会降低性能,那么您基本上希望删除整个类型扫描过程。就个人而言,我将它用于很多不同的事情,最值得注意的是缓存、统计、日志记录和配置。此外,我认为性能下降可以忽略不计。

只是我的 2 美分;HTH。

于 2015-07-08T06:28:33.363 回答
6

因此,我应对这一挑战的方法意味着深入研究大量参考资源,以了解 Roslyn 可用的不同类型。

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

public interface IModule
{
    public int Order { get; }

    public string Name { get; }

    public Version Version { get; }

    IEnumerable<ServiceDescriptor> GetServices();
}

public interface IModuleProvider
{
    IEnumerable<IModule> GetModules();
}

让我们也定义出基础提供者:

public abstract class ModuleProviderBase
{
    private readonly List<IModule> _modules = new List<IModule>();

    protected ModuleProviderBase()
    {
        Setup();
    }

    public IEnumerable<IModule> GetModules()
    {
        return _modules.OrderBy(m => m.Order);
    }

    protected void AddModule<T>() 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<ServiceDescriptor> GetServices()
    {
        yield return ServiceDescriptor.Instance<ILoggerFactory>(new DefaultLoggerFactory());
    }
}

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

现在,在我的 Web 项目中,我添加了对Contracts.dlland的引用DefaultLogger.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<MetadataReference, string[]> _cache
        = new ConcurrentDictionary<MetadataReference, string[]>();

    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 - 解析ModuleProviderWeb 项目中实例的命名空间,例如SampleWeb.
2 - 通过引用发现所有可用模块,这些模块作为字符串集合返回,例如 new[] { "SampleLogger.DefaultLoggerModule" }
3 - 将它们转换为类型的语句AddModule<SampleLogger.DefaultLoggerModule>();
4 - 创建我们要添加到的partial实现ModuleProvider我们的编译:

namespace SampleWeb
{
    partial class ModuleProvider
    {
        protected override void Setup()
        {
            AddModule<SampleLogger.DefaultLoggerModule>();
        }
    }
}

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

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

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

private IEnumerable<string> GetAvailableModules(Compilation compilation)
{
    var list = new List<string>();
    string[] modules = null;

    // Get the available references.
    var refs = compilation.References.ToList();

    // Get the assembly references.
    var assemblies = refs.OfType<PortableExecutableReference>().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<CompilationReference>().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<string> GetAssemblyModules(PortableExecutableReference reference)
{
    var metadata = GetMetadataMethodInfo.Invoke(reference, nul) as AssemblyMetadata;
    if (metadata != null)
    {
        var assemblySymbol = ((IEnumerable<IAssemblySymbol>)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<string>();
}

我们这里需要做一点反射,因为GetMetadata方法是不公开的,后来当我们抓取元数据时,CachedSymbols字段也是非公开的,所以更多的反射在那里。在确定什么是可用的方面,我们需要IEnumerable<IAssemblySymbol>CachedSymbols属性中获取。这为我们提供了参考程序集中的所有缓存符号。Roslyn 为我们这样做,所以我们可以滥用它:

private IEnumerable<ITypeSymbol> 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<string> 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<ClassDeclarationSyntax>().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.
    }
}

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

于 2015-07-08T08:32:06.387 回答