Подтвердить что ты не робот

Получение реализаций интерфейса в ссылочных сборках с Roslyn

Я хотел бы обойти некоторые классические методы сканирования сборки в рамках, которые я разрабатываю.

Итак, скажем, я определил следующий контракт:

public interface IModule
{

}

Это существует в выражении 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 в качестве инструментов метапрограммирования, чтобы вы могли объединить реализацию ICompileModule в свою папку Compiler\Preprocess в своем проекте и заставить ее делать что-то напуганное.

То, что моя цель будет, заключается в использовании реализации ICompileModule для выполнения работы, которую мы будем делать во время выполнения, во время компиляции.

  • В моих ссылках (как компиляциях, так и сборках) и моей текущей компиляции обнаружайте все incaniatable экземпляры IModule
  • Создайте класс, позвоните ему ModuleList с реализацией, которая дает экземпляры каждого модуля.
public static class ModuleList
{
    public static IEnumerable<IModule>() GetModules()
    {
        yield return new Module1();
        yield return new Module2();
    }
}

Если этот класс добавлен в блок компиляции, мы можем вызвать его и получить статический список модулей во время выполнения, вместо того чтобы искать все прикрепленные сборки. Мы существенно разгружаем работу над компилятором вместо среды выполнения.

Учитывая, что мы можем получить доступ ко всем ссылкам для компиляции с помощью свойства References, я не вижу, как я могу получить любую полезную информацию, например, возможно, доступ к байтовому коду, чтобы, возможно, загрузить сборку для отражение или что-то в этом роде.

Мысли?

4b9b3361

Ответ 1

Мысли?

Да.

Обычно в среде модуля вы хотите динамически загружать модуль на основе контекста или, если применимо, от третьего лица. Напротив, используя инфраструктуру компилятора Roslyn, вы в основном получаете эту информацию за время компиляции, тем самым ограничивая модули статическими ссылками.

Вчера я опубликовал код для динамической загрузки заводов wth. атрибуты, обновления для загрузки DLL и т.д. здесь: Соглашение об именах для GoF Factory?. Насколько я понимаю, это очень похоже на то, что вы пытаетесь достичь. Поверхность этого подхода заключается в том, что вы можете динамически загружать новую DLL во время выполнения. Если вы попробуете, вы обнаружите, что это довольно быстро.

Вы также можете ограничить процессы сборки. Например, если вы не обрабатываете mscorlib и System.* (или, возможно, даже все сборки GAC), это будет работать намного быстрее, конечно. Тем не менее, как я уже сказал, это не должно быть проблемой; просто сканирование типов и атрибутов - довольно быстрый процесс.


ОК, немного больше информации и контекста.

Теперь, возможно, вы просто ищете забавную головоломку. Я могу понять, что заниматься спортом с технологией в конце концов очень весело. Ответ ниже (сам Матвей) даст вам всю необходимую информацию.

Если вы хотите сбалансировать про и минусы генерации кода компиляции по сравнению с решением во время выполнения, здесь больше информации из моего опыта.

Несколько лет назад я решил, что было бы неплохо иметь собственную платформу парсера/генератора С# для трансформации AST. Это очень похоже на то, что вы можете сделать с Рослином; в основном он преобразует весь проект в дерево AST, который вы затем можете нормализовать, сгенерировать код, выполнить дополнительные проверки на аспектно-ориентированное программирование и добавить новые языковые конструкции. Моя первоначальная цель заключалась в том, чтобы добавить поддержку аспектно-ориентированного программирования в С#, для которой у меня было несколько практических приложений. Я пощажу вам подробности, но для этого контекста достаточно сказать, что модуль / Factory, основанный на генерации кода, был одной из вещей, с которыми я тоже экспериментировал.

Производительность, гибкость и количество кода (в небиблиотечном решении) являются ключевыми аспектами для меня для взвешивания решения между временем выполнения и решением времени компиляции. Пусть они сломаются:

  • Производительность. Это важно, потому что я не могу предположить, что код библиотеки не находится на критическом пути. Runtime обойдется вам в несколько миллисекунд за экземпляр приложения. (См. Ниже замечания о том, как/почему).
  • Гибкость. Они одинаково гибки в плане атрибута/сканирования. Однако во время выполнения вы имеете больше возможностей с точки зрения изменения правил (например, динамического подключения модулей и т.д.). Я иногда использую это, особенно на основе конфигурации, так что мне не нужно разрабатывать все в одном решении (потому что это неэффективно).
  • Количество кода. Как правило, меньше кода обычно лучше кода. Если вы это сделаете правильно, обе будут приводить к одному атрибуту, который вам нужен в классе. Другими словами, оба решения дают здесь один и тот же результат.

Однако заметка о производительности в порядке. Я использую отражение не только шаблонов Factory в моем коде. У меня в основном есть обширная библиотека здесь "инструментов", которые включают все шаблоны проектирования (и тонну других вещей). Несколько примеров: я автоматически генерирую код во время выполнения для таких вещей, как фабрики, цепочка ответственности, декораторы, насмешка, кеширование/прокси (и многое другое). Некоторые из них уже потребовали от меня сканирования сборок.

Как простое правило, я всегда использую атрибут для обозначения того, что что-то нужно изменить. Вы можете использовать это в своих интересах: просто сохраняя каждый тип с атрибутом (правильной сборки/пространства имен) в каком-либо одноместном/словаре где-нибудь, вы можете сделать приложение намного быстрее (потому что вам нужно только сканировать один раз). Это также не очень полезно для сканирования сборок от Microsoft. Я провел много тестов по крупным проектам и обнаружил, что в худшем случае, которое я нашел, сканирование добавило примерно 10 мс к времени запуска приложения. Обратите внимание, что это только один раз за экземпляр appdomain, а это означает, что вы его даже не заметите.

Активация типов - это действительно единственное "реальное" исполнение, которое вы получите. Это наказание можно оптимизировать, испустив код IL; это действительно не так сложно. Конечным результатом является то, что здесь это не будет иметь никакого значения.

Чтобы обернуть это, вот мои выводы:

  • Производительность: незначительная разница.
  • Гибкость: побеждает Runtime.
  • Количество кода: незначительная разница.

По моему опыту, несмотря на то, что многие платформы надеются поддерживать архитектуры plug-and-play, которые могут извлечь выгоду из падения сборок, реальность такова, что на самом деле не существует целых случаев использования, где это действительно применимо.

Если это не применимо, вы можете рассмотреть возможность использования шаблона Factory в первую очередь. Кроме того, если это применимо, я показал, что для него нет реального недостатка, то есть: если вы его правильно реализуете. К сожалению, я должен признать здесь, что видел много плохих реализаций.

Что касается того, что это не применимо, я думаю, что это отчасти верно. Это довольно часто встречается у поставщиков данных (он логически следует из трехуровневой архитектуры). Я также использую заводы для подключения таких вещей, как API связи /WCF API, поставщики кэширования и декораторы (что логически следует из архитектуры n-уровня). Вообще говоря, он используется для любого провайдера, о котором вы можете думать.

Если аргумент состоит в том, что он дает ограничение производительности, вы в основном хотите удалить весь процесс сканирования по типу. Лично я использую это для множества разных вещей, в первую очередь кэширования, статистики, регистрации и настройки. Кроме того, я считаю, что снижение производительности является несущественным.

Только мои 2 цента; НТН.

Ответ 2

Итак, мой подход с этой проблемой означает плавание через весь груз эталонного источника, чтобы понять различные типы доступных для Рослин.

Чтобы префикс конечного решения, создадим интерфейс модуля, мы поместим его в 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 для краткости.

Теперь, в моем веб-проекте, я добавляю ссылку на Contracts.dll и 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 - разрешает пространство имен экземпляра ModuleProvider в веб-проекте, например. 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. Это дает нам все кешированные символы в контрольной сборке. Рослин делает это для нас, поэтому мы можем оскорбить его:

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 проходит через пространства имен и обнаруживает все типы. Затем мы связываем результат с методом фильтра, который гарантирует, что он реализует наш необходимый интерфейс:

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

Массивно чрезмерно спроектированный, довольно сложный, но любопытный, как мне кажется!