简体   繁体   English

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

[英]Getting interface implementations in referenced assemblies with Roslyn

I'd like to bypass some classical assembly scanning techniques in a framework I am developing. 我想在我正在开发的框架中绕过一些经典的汇编扫描技术。

So, say I've defined the following contract: 所以,说我已经定义了以下合同:

public interface IModule
{

}

This exists in say Contracts.dll . 这存在于Contracts.dll

Now, if I want to discover all implementations of this interface, we would probably do something similar to the following: 现在,如果我想发现这个接口的所有实现,我们可能会做类似以下的事情:

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));
}

Not a great example, but it will do. 不是一个很好的例子,但它会做。

Now, these sorts of assembly scanning techniques can be quite under-performaning, and its all done at runtime, typically impacting startup performance. 现在,这些类型的汇编扫描技术可能完全不足,而且它们都在运行时完成,通常会影响启动性能。

In the new DNX environment, we can use ICompileModule instances as metaprogramming tools, so you could bundle an implementation of ICompileModule into your Compiler\\Preprocess folder in your project and get it to do something funky. 在新的DNX环境中,我们可以使用ICompileModule实例作为元编程工具,因此您可以将ICompileModule的实现ICompileModule到项目的Compiler\\Preprocess文件夹中,并让它做一些时髦的事情。

What my target would be, is to use an ICompileModule implementation, to do the work that we would do at runtime, at compile time instead. 我的目标是使用ICompileModule实现,以便在编译时执行我们将在运行时执行的工作。

  • In my references (both compilations and assemblies), and my current compilation, discover all instaniatable instances of IModule 在我的引用(包括编译和程序集)和我当前的编译中,发现所有可以实现的IModule实例
  • Create a class, lets call it ModuleList with an implementation which yields instances of each module. 创建一个类,让我们称之为ModuleList ,其实现产生每个模块的实例。
public static class ModuleList
{
    public static IEnumerable<IModule>() GetModules()
    {
        yield return new Module1();
        yield return new Module2();
    }
}

With that class added to the compilation unit, we could invoke it and get a static list of modules at runtime, instead of having to search through all the attached assemblies. 将该类添加到编译单元后,我们可以调用它并在运行时获取静态模块列表,而不必搜索所有附加的程序集。 We're essentially offloading the work on the compiler instead of the runtime. 我们实际上卸载了编译器而不是运行时的工作。

Given we can get access to all references for a compilation via the References property, I can't see how I can get any useful information, such as maybe access to the byte code, to perhaps to load an assembly for reflection, or something like that. 鉴于我们可以通过References属性访问编译的所有引用,我无法看到如何获取任何有用的信息,例如可能访问字节代码,也许加载用于反射的程序集,或类似的东西那。

Thoughts? 思考?

Thoughts? 思考?

Yes. 是。

Typically in a module environment you want to dynamically load a module based on the context, or - if applicable - from a third party. 通常,在模块环境中,您希望根据上下文动态加载模块,或者 - 如果适用 - 来自第三方。 In contrast, using the Roslyn compiler framework, you basically get this information compile-time, thereby restricting the modules to static references. 相比之下,使用Roslyn编译器框架,您基本上可以获得编译时的这些信息,从而将模块限制为静态引用。

Just yesterday I posted the code for dynamic loading of factories wth. 就在昨天,我发布了动态加载工厂的代码。 attributes, updates for loading DLL's etc here: Naming convention for GoF Factory? 这里有属性,加载DLL等的更新: GoF Factory的命名约定? . From what I understand, it's quite similar to what you're trying to achieve. 据我所知,它与你想要实现的非常相似。 The upside of that approach is that you can dynamically load new DLL's at runtime. 这种方法的好处是你可以在运行时动态加载新的DLL。 If you try it, you'll find that it's quite fast. 如果你试试,你会发现它很快。

You can also further restrict the assemblies you process. 您还可以进一步限制您处理的装配。 For example, if you don't process mscorlib and System.* (or perhaps even all GAC assemblies) it'll work a lot faster of course. 例如,如果您不处理mscorlibSystem.* (或者甚至可能是所有GAC程序集),它当然会更快地运行。 Still, as I said, it shouldn't be a problem; 不过,正如我所说,它应该不是问题; just scanning for types and attributes is quite a fast process. 只扫描类型和属性是一个非常快速的过程。


OK, a bit more information and context. 好的,更多的信息和背景。

Now, it might be possible that you're just looking for a fun puzzle. 现在,你可能正在寻找一个有趣的谜题。 I can understand that, toying around with technology is after all a lot of fun. 我可以理解,玩弄技术毕竟是很有趣的。 The answer below (by Matthew himself) will give you all the information that you need. 下面的答案(马修本人)将为您提供所需的所有信息。

If you want to balance the pro's and cons of compile-time code generation versus a runtime solution, here's more information from my experience. 如果您想平衡编译时代码生成与运行时解决方案的优缺点,请从我的经验中获取更多信息。

Some years back, I decided it was a good idea to have my own C# parser/generator framework to do AST transformations. 几年前,我认为让自己的C#解析器/生成器框架进行AST转换是个好主意。 It's quite similar to what you can do with Roslyn; 它与Roslyn的功能非常相似; basically it converts an entire project into an AST tree, which you can then normalize, generate code for, do extra checks on do aspect-oriented programming stuff and add new language constructs. 基本上它将整个项目转换为AST树,然后您可以对其进行规范化,生成代码,对面向方面的编程内容进行额外检查并添加新的语言结构。 My original goal here was to add support for aspect oriented programming into C#, for which I had some practical applications. 我最初的目标是在C#中添加对面向方面编程的支持,我有一些实际的应用程序。 I'll spare you the details, but for this context it's sufficient to say that a Module / Factory based on code generation was one of the things I've experimented with as well. 我将为您提供详细信息,但对于此上下文,可以说基于代码生成的模块/工厂也是我尝试过的事情之一。

Performance, flexibility and amount of code (in the non-library solution) are the key aspects for me for weighting the decision between a runtime and compile time decision. 性能,灵活性和代码量(在非库解决方案中)是我在运行时和编译时决策之间加权决策的关键方面。 Let's break them down: 让我们分解吧:

  • Performance . 表现 This is important because I cannot assume that the library code is not on the critical path. 这很重要,因为我不能假设库代码不在关键路径上。 Runtime will cost you a few milliseconds per appdomain instance. 运行时每个appdomain实例将花费几毫秒。 (See below for remarks on how/why). (有关如何/为何的说明,请参见下文)。
  • Flexibility . 灵活性 They're both about equally flexible in terms of attribute / scanning. 它们在属性/扫描方面同样具有灵活性。 However, at runtime you have more possibilities in terms of changing the rules (eg dynamically plugging modules etc). 但是,在运行时,您在更改规则方面有更多可能性(例如,动态插入模块等)。 I sometimes use this, particularly based on configuration, so that I don't have to develop everything in the same solution (because that's inefficient). 我有时会使用它,特别是基于配置,这样我就不必在同一个解决方案中开发所有东西(因为效率低下)。
  • Amount of code . 代码量 As a rule of thumb, less code is usually better code. 根据经验,较少的代码通常是更好的代码。 If you do it right, both will result in a single attribute that you need on a class. 如果你做得对,两者都会产生你在课堂上需要的单一属性。 In other words, both solutions give the same result here. 换句话说,这两种解决方案都给出了相同的结果。

A note on performance is in order though. 但是关于性能的说明是有序的。 I use reflection for more than just factory patterns in my code. 我在代码中使用反射不仅仅是工厂模式。 I basically have an extensive library here of 'tools' that include all design patterns (and a ton of other things). 我基本上有一个包含所有设计模式(以及其他许多东西)的“工具”库。 A few examples: I automatically generate code at runtime for things like factories, chain-of-responsibility, decorators, mocking, caching / proxies (and much more). 举几个例子:我在运行时自动生成代码,例如工厂,责任链,装饰器,模拟,缓存/代理(以及更多)。 Some of these already required me to scan the assemblies. 其中一些已经要求我扫描组件。

As a simple rule of thumb, I always use an attribute to denote that something has to be changed. 作为一个简单的经验法则,我总是使用一个属性来表示必须更改某些内容。 You can use this to your advantage: by simply storing every type with an attribute (of the correct assembly/namespace) in a singleton / dictionary somewhere, you can make the application a lot faster (because you only need to scan once). 您可以利用这个优势:通过简单地将每个类型(具有正确的程序集/命名空间的属性)存储在某个地方的单例/字典中,您可以使应用程序更快(因为您只需要扫描一次)。 It's also not very useful to scan assemblies from Microsoft. 从Microsoft扫描程序集也不是很有用。 I did a lot of tests on large projects, and found that in the worst case that I found, scanning added approximately 10 ms to the startup time of an application . 我对大型项目进行了大量测试,发现在我发现的最坏情况下, 扫描在应用程序启动时增加了大约10毫秒 Note that this is only once per instantiation of an appdomain, which means you won't even notice it, ever. 请注意,这只是每个appdomain实例化一次,这意味着你甚至都不会注意到它。

Activation of the types is really the only 'real' performance penalty you will get. 激活类型实际上是你将获得的唯一“真正的”性能惩罚。 That penalty can be optimized away by emitting the IL code; 可以通过发出IL代码来优化该惩罚; it's really not that difficult. 这真的不那么难。 The end result is that it won't make any difference here. 最终结果是它在这里没有任何区别。

To wrap it up, here are my conclusions: 总结一下,这是我的结论:

  • Performance : Insignificant difference. 表现 :微不足道的差异。
  • Flexibility : Runtime wins. 灵活性 :运行时获胜。
  • Amount of code : Insignificant difference. 代码量 :微不足道的差异。

From my experience, although a lot of frameworks hope to support plug and play architectures which could benefit from drop in assemblies, the reality is, there isn't a whole load of use-cases where this is actually applicable. 根据我的经验,尽管很多框架都希望支持即插即用架构,这些架构可以从组件的下降中受益,但实际情况是,实际上并没有完全适用的用例。

If it's not applicable, you might want to consider not using a factory pattern in the first place. 如果它不适用,您可能想要考虑不首先使用工厂模式。 Also, if it is applicable, I've shown that there isn't a real downside to it, that is: iff you implement it properly. 此外,如果它适用,我已经表明它没有真正的缺点,即:如果你正确实现它。 Unfortunately I have to acknowledge here that I've seen a lot of bad implementations. 不幸的是,我必须在此承认,我已经看到很多不好的实现。

As for the fact that it's not actually applicable, I think that's only partly true. 至于它实际上并不适用的事实,我认为这只是部分正确。 It's quite common to drop-in data providers (it logically follows from a 3-tier architecture). 嵌入式数据提供程序非常常见(逻辑上遵循3层架构)。 I also use factories to wire up things like communication/WCF API's, caching providers and decorators (that logically follows from an n-tier architecture). 我还使用工厂来连接诸如通信/ WCF API,缓存提供者和装饰器之类的东西(逻辑上遵循n层架构)。 Generally speaking it's used for any kind of provider you can think of. 一般来说,它可用于您能想到的任何类型的提供商。

If the argument is that it gives a performance penalty, you basically want to remove the entire type scanning process. 如果参数是性能损失,您基本上想要删除整个类型扫描过程。 Personally, I use that for a ton of different things, most notably caching, statistics, logging and configuration. 就个人而言,我将其用于大量不同的事情,最着名的是缓存,统计,日志记录和配置。 Also, I believe the performance downside is negliable. 此外,我认为业绩下滑可以忽略不计。

Just my 2 cents; 只需2美分; HTH. HTH。

So my approach with this challenge meant diving through a whole load of reference source to understand the different types available to Roslyn. 因此,我采用这一挑战的方法意味着潜入一大堆参考源,以了解Roslyn可用的不同类型。

To prefix the end solution, lets create the module interface, we'll put this in Contracts.dll : 要为最终解决方案添加前缀,让我们创建模块接口,我们将其放在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();
}

And let's also define out base provider: 我们还定义了基础提供者:

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() { }
}

Now, in this architecture, the module isn't really anything more than a descriptor, so shouldn't take dependencies, it merely expresses what services it offers. 现在,在这个体系结构中,模块实际上不仅仅是一个描述符,所以不应该依赖它,它只是表达它提供的服务。

Now an example module might look like, in DefaultLogger.dll : 现在,在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());
    }
}

I've left out the implementation of ModuleBase for brevity. 为简洁起见,我省略了ModuleBase的实现。

Now, in my web project, I add a reference to Contracts.dll and DefaultLogger.dll , and then add the following implementation of my module provider: 现在,在我的web项目中,我添加对Contracts.dllDefaultLogger.dll的引用,然后添加我的模块提供程序的以下实现:

public partial class ModuleProvider : ModuleProviderBase { }

And now, my ICompileModule : 现在,我的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
}

Essentially this module does a few steps; 基本上这个模块做了几个步骤;

1 - Resolves the namespace of the ModuleProvider instance in the web project, eg SampleWeb . 1 - 解析Web项目中ModuleProvider实例的名称空间,例如SampleWeb
2 - Discovers all the available modules through references, these are returned as a collection of strings, eg new[] { "SampleLogger.DefaultLoggerModule" } 2 - 通过引用发现所有可用模块,这些模块作为字符串集合返回,例如new [] {“SampleLogger.DefaultLoggerModule”}
3 - Convert those to statements of the kind AddModule<SampleLogger.DefaultLoggerModule>(); 3 - 将它们转换为AddModule<SampleLogger.DefaultLoggerModule>();
4 - Create a partial implementation of ModuleProvider that we are adding to our compilation: 4 - 创建我们要添加到编译中的ModuleProviderpartial实现:

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

So, how did I discover the available modules? 那么,我是如何发现可用模块的呢? There are three phases: 分为三个阶段:

1 - The referenced assemblies (eg, those provided through NuGet) 1 - 引用的程序集(例如,通过NuGet提供的程序集)
2 - The referenced compilations (eg, the referenced projects in the solution). 2 - 引用的编译(例如,解决方案中引用的项目)。
3 - The module declarations in the current compilation. 3 - 当前编译中的模块声明。

And for each referenced compilation, we repeat the above. 对于每个引用的编译,我们重复上面的内容。

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;
}

So, to get assembly referenced modules: 因此,要获得程序集引用的模块:

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>();
}

We need to do a little reflection here as the GetMetadata method is not public, and later, when we grab the metadata, the CachedSymbols field is also non-public, so more reflection there. 我们需要在这里做一点反思,因为GetMetadata方法不公开,后来,当我们获取元数据时, CachedSymbols字段也是非公开的,因此更多的反映。 In terms of identifying what is available, we need to grab the IEnumerable<IAssemblySymbol> from the CachedSymbols property. 在识别可用内容方面,我们需要从CachedSymbols属性中获取IEnumerable<IAssemblySymbol> This gives us all the cached symbols in the reference assembly. 这为我们提供了引用程序集中的所有缓存符号。 Roslyn does this for us, so we can then abuse it: 罗斯林为我们做了这个,所以我们可以滥用它:

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;
        }
    }
}

The GetTypeSymbols method walks through the namespaces and discovers all types. GetTypeSymbols方法遍历命名空间并发现所有类型。 We then chain the result to the filter method, which ensures it implements our required interface: 然后我们将结果链接到filter方法,这确保它实现了我们所需的接口:

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

With GetFullMetadataName being a utility method: 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;
}

Next up, module declarations in the current compilation: 接下来,当前编译中的模块声明:

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);
            }
        }
    }
}

And that's really it! 这就是它! So, now at compile time, my ICompileModule will: 所以,现在在编译时,我的ICompileModule将:

  • Discover all available modules 发现所有可用的模块
  • Implement an override of my ModuleProvider.Setup method with all known referenced modules. 使用所有已知引用的模块实现ModuleProvider.Setup方法的覆盖。

This means I can add my startup: 这意味着我可以添加我的启动:

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.
    }
}

Massively over-engineered, quite complex, but kinda awesome I think! 大量过度设计,相当复杂,但我觉得有点棒!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何找到与Roslyn接口的所有实现? - How to find all implementations of an interface with Roslyn? 如何通过多个程序集的接口有效地对所有实现进行分组? - How to efficiently group all implementations by interface from multiple assemblies? 当引用的程序集引用mscorlib 2.0.5.0和4.0.0.0时,如何让roslyn编译 - How to get roslyn to compile when referenced assemblies have references to both mscorlib 2.0.5.0 and 4.0.0.0 有没有办法使用Roslyn中的Compilation对象从引用的程序集中获取程序集级属性? - Is there a way to get assembly level attributes from referenced assemblies using the Compilation object in Roslyn? 获取项目中引用的所有程序集中的所有接口类型 - Get all interface types in all assemblies referenced in project .NET - 获取通用接口的所有实现? - .NET - Getting all implementations of a generic interface? Nuget 库获取在所有程序集和引用的程序集中实现接口的所有类型 - Nuget Library to get all types that implement an interface in all assemblies and referenced assemblies 不管实际的代码(执行)路径如何,引用的程序集都会被加载? - Referenced assemblies getting loaded regardless of the actual code (execution) path? 引用程序集中的ConfigurationManager - ConfigurationManager in Referenced Assemblies 确定所有引用的程序集 - Determine all referenced assemblies
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM