ASP.NET загрузить основные модули и их зависимости


Я бы один из моих RESTful-сервисов для поддержки плагинов. В настоящее время он использует ссылки жестко на этапе компиляции.

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

Он загружает Плагины, расположенном в ext поддиректории. Каждый плагин находится в его собственный подкаталог, названный после того, как сам плагин, и он может содержать собственные зависимости, например:

ext\PluginTest.HalloWord\PluginTest.HalloWorld.dll
ext\PluginTest.HalloWord\PluginTest.HalloWorldHelper.dll

Я реализовал это путем загрузки плагинов с ConfigureApplicationPartManager и позже, если плагин зависимостей просят, я стараюсь их разрешить в плагин каталога с AssemblyResolve обработчик событий. Дополнительно каждая сборка также может содержать встроенные видом. Бритвы вид двигателя способен найти их через EmbeddedFileProvider для каждого загружаемого плагина.

public class Startup
{
    private const string PluginsDirectoryName = "ext";

    public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
    {
        Configuration = configuration;
        HostingEnvironment = hostingEnvironment;
    }

    public IConfiguration Configuration { get; }

    public IHostingEnvironment HostingEnvironment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
        {
            var pluginName = e.RequestingAssembly.GetName().Name;

            // Extract dependency name from the full assembly name:
            // PluginTest.HalloWorldHelper, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null
            var pluginDependencyName = e.Name.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).First();

            var pluginDependencyFullName = 
                Path.Combine(
                    HostingEnvironment.ContentRootPath, 
                    PluginsDirectoryName, 
                    pluginName, 
                    $"{pluginDependencyName}.dll"
                );

            return
                File.Exists(pluginDependencyFullName)
                    ? Assembly.LoadFile(pluginDependencyFullName)
                    : null;
        };

        var pluginAssemblies = 
            GetPluginAssemblies(HostingEnvironment)
                .ToList();

        services
            .AddMvc()
            .ConfigureApplicationPartManager(apm =>
            {
                foreach (var pluginAssembly in pluginAssemblies)
                {
                    apm.ApplicationParts.Add(new AssemblyPart(pluginAssembly));
                }
            });

        // Views are embeded in plugins so add a resolver so that the Razor view engine can find them.
        services.Configure<RazorViewEngineOptions>(options =>
        {
            foreach (var pluginAssembly in pluginAssemblies)
            {
                options
                    .FileProviders
                    .Add(new EmbeddedFileProvider(pluginAssembly));
            }                
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseMvc();
    }

    private static IEnumerable<Assembly> GetPluginAssemblies(IHostingEnvironment hostingEnvironment)
    {
        var pluginDirectoryName = Path.Combine(hostingEnvironment.ContentRootPath, PluginsDirectoryName);

        if (!Directory.Exists(pluginDirectoryName))
        {
            yield break;
        }

        var pluginDirectories = Directory.GetDirectories(pluginDirectoryName);
        foreach (var pluginDirectory in pluginDirectories)
        {
            var pluginFullName =
                Path.Combine(
                    hostingEnvironment.ContentRootPath,
                    pluginDirectory,
                    $"{Path.GetFileName(pluginDirectory)}.dll"
                );

            if (File.Exists(pluginFullName))
            {
                yield return Assembly.LoadFile(pluginFullName);
            }
        }
    }
}

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

Мне было интересно, есть ли что-нибудь об этом простом решении, которое может быть сделано лучше?



1565
4
задан 17 марта 2018 в 08:03 Источник Поделиться
Комментарии
2 ответа

Помимо конвертации конфигурации сервиса для методов расширения, там не так много еще я хотел изменить в действующем кодексе.

Там повторяется код, который может быть вынесен на свои проблемы.

public static class PluginConfigurationExtensions {

public IServiceCollection AddMvcPlugins(IServiceCollection services, string pluginsRootPath) {
AppDomain.CurrentDomain.ConfigureAssemblyResolve(pluginsRootPath);

var pluginAssemblies = GetPluginAssemblies(pluginsRootPath).ToList();

// Setup options with DI
services.AddOptions();

services
.AddMvc()
.ConfigureApplicationPartManager(apm => {
foreach (var pluginAssembly in pluginAssemblies) {
apm.ApplicationParts.Add(new AssemblyPart(pluginAssembly));
}
});

// Views are embedded in plugins so add a resolver so that the Razor view engine can find them.
services.Configure<RazorViewEngineOptions>(_ => {
foreach (var pluginAssembly in pluginAssemblies) {
_
.FileProviders
.Add(new EmbeddedFileProvider(pluginAssembly));
}
});
return services;
}

private static void ConfigureAssemblyResolve(this AppDomain appDomain, string pluginsRootPath) {
appDomain.AssemblyResolve += (sender, e) => {
var pluginName = e.RequestingAssembly.GetName().Name;

// Extract dependency name from the full assembly name:
// PluginTest.HalloWorldHelper, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null
var pluginDependencyName = e.Name.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).First();

var pluginDependencyFullName = Path.Combine(
pluginsRootPath,
pluginName,
$"{pluginDependencyName}.dll"
);

return
File.Exists(pluginDependencyFullName)
? Assembly.LoadFile(pluginDependencyFullName)
: null;
};
}

private static IEnumerable<Assembly> GetPluginAssemblies(string pluginsRootPath) {
if (!Directory.Exists(pluginsRootPath)) {
yield break;
}

var pluginDirectories = Directory.GetDirectories(pluginsRootPath);
foreach (var pluginDirectory in pluginDirectories) {
var pluginFullName =
Path.Combine(
pluginsRootPath,
$"{Path.GetFileName(pluginDirectory)}.dll"
);

if (File.Exists(pluginFullName)) {
yield return Assembly.LoadFile(pluginFullName);
}
}
}
}

Это уменьшает ConfigureServices для

public void ConfigureServices(IServiceCollection services) {
var pluginsRootPath = Path.Combine(HostingEnvironment.ContentRootPath, PluginsDirectoryName);

services.AddMvcPlugins(pluginsRootPath);

//...
}

Вы могли бы рассмотреть возможность размещения PluginsDirectoryName в appsetting.json и вынув его через IOptions вместо жесткого кодирования его в Startup.

1
ответ дан 19 марта 2018 в 04:03 Источник Поделиться

Я последовал за большинством @Nkosiбыл предложения и реорганизовать код для регистрации плагинов.


Я изменил Основной расширение для работы на IMvcBuilder чтобы избежать вызова AddMvc внутри него.

Это дает мне доступ к IMvcBuilder.Services имущество, которое может использоваться для получения других услуг, которые этот exension требует. Таким образом не нужно проходить каких-либо других аргументов. Два каталога имен, теперь хранятся в appsettings.json файл.

Я тоже слегка изменился, как модули будут загружены, потому что мне не нравится встроенный вид. Я предпочитаю быть в состоянии изменить текст/HTML без развертывания нового *.dll. Это означает, что взгляды решаются теперь с src\Views каталог вместо.

Новая структура каталогов:

\ext
\PluginX
\bin
PluginX.dll
\src
\Views
Index.cshtml

(Бритвы вид двигателя знает о src папку, потому что я переопределить разрешение с таможни IViewLocationExpander.)

public static class MvcBuilderPluginExtensions
{
// Adds plugins located in \{Root}\Plugin\{Binary}\Plugin.dll
// Example: \ext\Plugin\bin\Plugin.dll
public static IMvcBuilder AddPlugins(this IMvcBuilder mvc)
{
var serviceProvider = mvc.Services.BuildServiceProvider();
var configuration = serviceProvider.GetService<IConfiguration>();
var hostingEnvironment = serviceProvider.GetService<IHostingEnvironment>();
var logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger<Startup>();

var pluginsRootPath = Path.Combine(hostingEnvironment.ContentRootPath, configuration["PluginDirectory:Root"]);
var pluginAssemblies = GetPluginAssemblies(pluginsRootPath, configuration["PluginDirectory:Binary"]).ToList();

logger.Log(Abstraction.Layer.Infrastructure().Data().Variable(new { pluginAssemblies = pluginAssemblies.Select(x => x.FullName) }));

mvc
.ConfigureApplicationPartManager(apm =>
{
foreach (var pluginAssembly in pluginAssemblies)
{
logger.Log(Abstraction.Layer.Infrastructure().Data().Object(new { pluginAssembly = new { pluginAssembly.FullName } }));
apm.ApplicationParts.Add(new AssemblyPart(pluginAssembly));
}
});

mvc
.Services
.ConfigureRazorViewEngine(hostingEnvironment, pluginAssemblies, pluginsRootPath);

ConfigureAssemblyResolve(logger, pluginsRootPath, configuration["PluginDirectory:Binary"]);

return mvc;
}

private static IEnumerable<Assembly> GetPluginAssemblies(string pluginsRootPath, string binDirectoryName)
{
if (!Directory.Exists(pluginsRootPath))
{
yield break;
}

var pluginDirectories = Directory.GetDirectories(pluginsRootPath);
foreach (var pluginDirectory in pluginDirectories)
{
// C:\..\ext\Plugin\bin\Plugin.dll
var pluginFullName =
Path.Combine(
pluginDirectory,
binDirectoryName,
$"{Path.GetFileName(pluginDirectory)}.dll"
);

if (File.Exists(pluginFullName))
{
yield return Assembly.LoadFile(pluginFullName);
}
}
}

private static void ConfigureAssemblyResolve(ILogger logger, string pluginsRootPath, string binDirectoryName)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
// Extract dependency name from the full assembly name:
// FooPlugin.FooClass, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null
var pluginDependencyName = e.Name.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).First();

// C:\..\ext\Plugin\bin\PluginDependency.dll
var pluginDependencyFullName =
Path.Combine(
pluginsRootPath,
pluginDependencyName,
binDirectoryName,
$"{pluginDependencyName}.dll"
);

logger.Log(Abstraction.Layer.Infrastructure().Data().Variable(new { pluginDependencyFullName }));

return
File.Exists(pluginDependencyFullName)
? Assembly.LoadFile(pluginDependencyFullName)
: null;
};
}

// Adds plugin directory to Razor view engine so that it can resolve plugin's views e.g. \ext\Plugin
private static void ConfigureRazorViewEngine(this IServiceCollection services, IHostingEnvironment hostingEnvironment, IEnumerable<Assembly> pluginAssemblies, string pluginsRootPath)
{
services.Configure<RazorViewEngineOptions>(options =>
{
foreach (var pluginAssembly in pluginAssemblies)
{
var pluginRootPath =
Path.Combine(
pluginsRootPath,
pluginAssembly.GetName().Name
);

options
.FileProviders
.Add(new PhysicalFileProvider(pluginRootPath));
}

// Extension development does not use plugins so we have to look for it in the current directory parent
// because the service is "installed" as a submodule which is a subdirectory.
if (hostingEnvironment.IsDevelopment("Extension"))
{
// ContentRootPath is the path of the *.csproj, we have to go back two levels to reach the extension directory.
var extensionDirectory = new DirectoryInfo(hostingEnvironment.ContentRootPath).Parent?.Parent;

if (extensionDirectory is null)
{
throw new DirectoryNotFoundException("Could not find extension directory.");
}

options
.FileProviders
.Add(new PhysicalFileProvider(Path.Combine(extensionDirectory.FullName, extensionDirectory.Name)));
}
});
}
}

Регистрацию плагина двигатель стал один-лайнер:

public void ConfigureServices(IServiceCollection services)
{

//..

services
.AddMvc()
.AddPlugins();

//..
}

1
ответ дан 19 марта 2018 в 06:03 Источник Поделиться