Добавление сборок / типов, которые должны быть доступны для Razor Page во время выполнения - PullRequest
2 голосов
/ 04 ноября 2019

Я пытаюсь создать динамический веб-интерфейс, в котором я могу динамически указывать на папку и обслуживать веб-контент из этой папки с помощью ASP.NET Core. Это довольно легко работает с использованием FileProviders в ASP.NET Core для перенаправления корневой веб-папки. Это работает как для StaticFiles, так и для RazorPages.

Однако для RazorPages проблема заключается в том, что после этого вы не можете динамически добавлять ссылки для дополнительных типов. Я хотел бы иметь возможность при желании добавить папку (PrivateBin), которую при запуске я могу просмотреть, загрузить сборки и затем увидеть эти сборки в Razor.

К сожалению, это не работает, как Razorне отображаются загруженные сборки даже при использовании компиляции во время выполнения.

Я использую следующее при запуске для загрузки сборок. Обратите внимание, что папка, из которой они загружены, находится не в ContentRoot или WebRoot по умолчанию, а в новом перенаправленном WebRoot.

// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
    var binPath = Path.Combine(WebRoot, "PrivateBin");
    if (Directory.Exists(binPath))
    {
        var files = Directory.GetFiles(binPath);
        foreach (var file in files)
        {
            if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
               !file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
                continue;

            try
            {
                var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                Console.WriteLine("Additional Assembly: " + file);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to load private assembly: " + file);
            }
        }
    }
}

Сборка загружается в AssemblyLoadContext (), и я могу - используя Reflection и Type.GetType("namespace.class,assembly")- получить доступ к типу.

Однако, когда я пытаюсь получить доступ к типу в RazorPages - даже с включенной компиляцией во время выполнения - типы недоступны. Я получаю следующую ошибку:

enter image description here

Чтобы убедиться, что тип действительно доступен, я проверил, что внутри Razor я могу сделать следующее:

@{
 var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
 var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
                    null, new object[] { "**asdasd**", false, false, false });
}
@mdText

и это прекрасно работает. Таким образом, сборка загружена, и тип доступен, но Razor, кажется, не знает об этом.

Поэтому вопрос:

Можно ли загрузить сборки вruntime и сделать их доступными для Razor с помощью Runtime Compilation и использовать его так, как вы обычно используете тип через прямой декларативный доступ?

Ответы [ 2 ]

0 голосов
/ 05 ноября 2019

Оказывается, решение этой проблемы заключается в параметрах компиляции Razor Runtime, которые позволяют добавлять дополнительные 'ReferencePaths', а затем явно загружать сборки.

В ConfigureServices ():

services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
    .AddRazorRuntimeCompilation(
        opt =>
        {

            opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
            LoadPrivateBinAssemblies(opt);
        });

затем:

private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
    var binPath = Path.Combine(WebRoot, "PrivateBin");
    if (Directory.Exists(binPath))
    {
        var files = Directory.GetFiles(binPath);
        foreach (var file in files)
        {
            if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
               !file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
                continue;

            try
            {
                var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                opt.AdditionalReferencePaths.Add(file);           
            }
            catch (Exception ex)
            {
                ...
            }

        }
    }

}

Ключ:

opt.AdditionalReferencePaths.Add(file);  

, который делает сборку видимой для Razor, но фактически не загружает ее. Чтобы загрузить его, вы должны явно загрузить его:

AssemblyLoadContext.Default.LoadFromAssemblyPath(file);

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

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

0 голосов
/ 05 ноября 2019

Быстрый просмотр исходного кода ASP.NET Core показывает:

Все представления Razor Компиляции начинаются с:

RuntimeViewCompiler.CreateCompilation (..)

, который использует: CSharpCompiler.Create (.., .., ссылки: ..)

, который использует: RazorReferenceManager.CompilationReferences

, который использует: см. Код на github

// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
    .OfType<ICompilationReferencesProvider>()
    .SelectMany(_ => _.GetReferencePaths())

, который использует: ApplicationPartManager.ApplicationParts

Так что нам нужно как-то зарегистрироватьсянаше собственное ICompilationReferencesProvider и вот как ..

ApplicationPartManager

В то время как поиск частей приложения делает ApplicationPartManager несколько вещей:

  1. он ищет скрытые сборки, считывая атрибуты, такие как:
[assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
[assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
// plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.

Затем он перебирает все найденные сборки и использует ApplicationPartFactory.GetApplicationPartFactory (сборка) (, как видно в строке 69 ), чтобы найти типы, которые расширяются ApplicationPartFactory.

Затем он вызывает метод GetApplicationParts(assembly) для всех найденных ApplicationPartFactory с.

Все сборки без ApplicationPartFactory получаютDefaultApplicationPartFactory, который возвращает new AssemblyPart(assembly) в GetApplicationParts.

public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);

GetApplicationPartFactory

GetApplicationPartFactory ищет [assembly: ProvideApplicationPartFactory(typeof(SomeType))], затем использует SomeType в качестве фабрики.

public abstract class ApplicationPartFactory {

    public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);

    public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
    {
        // ...

        var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
        if (provideAttribute == null)
        {
            return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
        }

        var type = provideAttribute.GetFactoryType();

        // ...

        return (ApplicationPartFactory)Activator.CreateInstance(type);
    }
}

One Solution

Это означает, что мы можем создать и зарегистрировать (используя ProvideApplicationPartFactoryAttribute) нашу собственную ApplicationPartFactory, которая возвращает пользовательскую реализацию ApplicationPart, которая реализует ICompilationReferencesProvider, а затем возвращает наши ссылкив GetReferencePaths.

[assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]

namespace WebApplication1 {
    public class MyApplicationPartFactory : ApplicationPartFactory {
        public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
        {
            yield return new CompilationReferencesProviderAssemblyPart(assembly);
        }
    }

    public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
        private readonly Assembly _assembly;

        public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
        {
            _assembly = assembly;
        }

        public IEnumerable<string> GetReferencePaths()
        {
            // your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
            // So you should load all private bin's before the first RazorPage gets requested.

            return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
                .Where(_ => !_.IsDynamic)
                .Select(_ => new Uri(_.CodeBase).LocalPath);
        }
    }
}

Настройка моего рабочего теста:

  • Веб-приложение ASP.NET Core 3
  • Класс ASP.NET Core 3 ClassLibrary
  • В обоих проектах нет ссылок друг на друга.
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Content Remove="Pages\**" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
  </ItemGroup>

</Project>
services
   .AddRazorPages()
   .AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.

~ / Pages / Index.cshtml

<code>@page

<pre>
    output: [
        @(
            new ClassLibrary1.Class1().Method1()
        )
    ]

И он показывает ожидаемый результат:

    output: [ 
        Hallo, World!
    ]

Хорошего дня.

...