范围依赖解析到声明程序集

时间:2019-03-15 09:17:35

标签: c# plugins dependency-injection autofac

早上好。

短版;实现我共同定义的IMenuItem接口的所有程序集中的所有具体实现都注入到所有需要IEnumerable<IMenuItem>的构造函数中。

我正在构建一个允许插件的Windows TrayIcon小应用程序核心。核心会在bin目录中发现所有插件,并创建我的Autofac容器。

我有一个Core.Interfaces项目,它声明了一个IMenuItem接口

每个插件都在自己的程序集中定义,并且在该插件内可以有许多功能;每个功能都将声明其菜单项。

在运行时,每个插件都会发现所有功能并查询其菜单项。我遇到的问题是,插件A从插件B接收菜单项,因为所有菜单项都实现了IMenuItem接口。

我想要实现的是拥有一个通用的IMenuItem接口,但是当插件A中的构造函数要求IEnumerable<IMenuItem>时,只能将我们在自己的程序集中发现的具体内容传递给它。

只需说,如果我在每个程序集中都声明了IMenuItem接口,那么这一切都很好,大概是因为然后将注册命名为该接口的名称空间。

我正在努力寻找适用于Google的正确术语,但我认为这是一个注册问题;也许我只能在解决时解决?

1 个答案:

答案 0 :(得分:1)

开箱即用,没有什么可以做的。您必须编写自定义代码。

许多自定义代码将取决于您如何解析插件集。这里有一些解决方案,尽管如果它们都不适合您当前的设计,也许您可​​以使用它们激发更多想法的想象力。

免责声明:我没有通过编译器运行所有这些。它们只是部分示例,我可能会打错字。有些可能是伪代码。 YMMV。

选项1:在单独的生命周期范围内注册每个插件

the multitenant support works就是这样。基本上,根容器中仅包含共享组件,并且每个插件(以及相应的菜单项)都将在子作用域中注册。

var builder = new ContainerBuilder();

// register common stuff that all plugins use
builder.Register<SomethingCommon>().As<ICommonService>();
var container = builder.Build();

// iterate over the assemblies and create scopes per plugin
var pluginScopes = new List<ILifetimeScope>();
foreach(var assembly in GetThePluginAssemblies())
{
  var scope = container.BeginLifetimeScope(b =>
  {
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .AsImplementedInterfaces();
  });
  pluginScopes.Add(scope);
}

这时,您有一个单独的作用域列表,可用于解析每个插件,例如是否需要所有插件:

var plugins = pluginScopes.SelectMany(s => s.Resolve<IEnumerable<IPlugin>>());

(我认为是SelectMany的工作方式,我总是感到困惑。关键是您会得到所有作用域中所有插件的扁平列表。)

为简化生活,从技术上讲,您可以使用Autofac.Multitenant软件包,并“假装”每个插件都是单独的租户。它已经建立了每个租户的所有作用域跟踪和配置。

var builder = new ContainerBuilder();

// register common stuff that all plugins use
builder.Register<SomethingCommon>().As<ICommonService>();
var container = builder.Build();

// You probably won't want to resolve things "as a tenant" from the container,
// so you'd only use the Multitenant.ApplicationContainer (for global/common stuff)
// or individual tenant scopes directly. The tenant ID strategy, ostensibly, won't
// be used, so just make a dummy one that always returns false or something.
var multiPluginContainer = new MultitenantContainer(container, SomeTenantIdentificationStrategy);

// iterate over the assemblies and create tenants per plugin
// where the tenant ID is something like the assembly name
foreach(var assembly in GetThePluginAssemblies())
{
  multiPluginContainer.ConfigureTenant(assembly.FullName, b =>
  {
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
    b.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .AsImplementedInterfaces();
  });
}

然后,您可以获得插件列表(“租户”)并解决。

var plugins = multiPluginContainer
  .GetTenants()
  .SelectMany(k =>
    multiPluginContainer.GetTenantScope(k).Resolve<IEnumerable<IPlugin>>());

选项2:使用元数据标记项目

Autofac支持parameters during registration,而ResolvedParameter非常强大。与此相关的一些巧妙的工作可能会大有帮助。

首先,您可以注册所有菜单项并用元数据标记它们。

var builder = new ContainerBuilder();
// register a bunch of stuff and...
foreach(var assembly in GetThePluginAssemblies())
{
  builder.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IPlugin))
     .AsImplementedInterfaces();
  builder.RegisterAssemblyTypes(assembly)
     .Where(t => t.GetInterfaces().Any(i => i == typeof(IMenuItem))
     .WithMetadata("assembly", assembly.FullName)
     .AsImplementedInterfaces();
}

好的,现在您已经用程序集名称标记了所有IMenuItem条目。创建一个模块,该模块自动将解析的参数附加到每个IPlugin上,以便您的参数可以满足任何IEnumerable<IMenuItem>的要求。这主要基于log4net module example from the docs

public class MenuItemModule : Autofac.Module
{
  private static void OnComponentPreparing(object sender, PreparingEventArgs e)
  {
    e.Parameters = e.Parameters.Union(
      new[]
      {
        new ResolvedParameter(
            // Only provide values for IEnumerable<IMenuItem> requested
            // by IPlugin implementations
            (pi, ctx) =>
               pi.ParameterType == typeof(IEnumerable<IMenuItem>) &&
               pi.Member.DeclaringType.GetInterfaces().Any(i => i == typeof(IPlugin)),
            // Resolve the appropriately tagged menu items
            // IEnumerable<T> - get all the menu items
            // Meta<T> - you want to look at the metadata
            // Lazy<T> - don't actually construct them until you want them
            // meta.Value = Lazy<T>
            // meta.Value.Value resolves the IMenuItem
            (pi, ctx) => {
              var asmName = pi.Member.DeclaringType.Assembly.FullName;
              return ctx.Resolve<IEnumerable<Meta<Lazy<IMenuItem>>>>()
                 .Where(meta => meta.Metadata["assembly"] == asmName)
                 .Select(meta => meta.Value.Value);
            }
        ),
      });
  }

  protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
  {
    registration.Preparing += OnComponentPreparing;
  }
}

然后您将注册该模块以确保所有分辨率都可以获取该参数。

builder.RegisterModule<MenuItemModule>();

还有其他选项。

您可以想象它上的其他排列。每个插件使用单独的容器(这不是一个坏主意-很好地隔离插件)。每个插件分别使用 AppDomain (即使隔离度更高,也可以封送数据)。基本IPlugin实现中具有过滤逻辑,而不是ResolvedParameter中的过滤逻辑。插件实现中的元数据过滤器属性可以进行过滤。

希望这可以帮助您解除封锁。