【问题标题】:Ninject Convention Based binding to be resolved at runtime要在运行时解决的基于 Ninject 约定的绑定
【发布时间】:2014-12-03 15:07:17
【问题描述】:

我正在使用命令处理程序模式并与 ninject.extensions.Conventions 绑定,当我的实际 IQueryHandler 接口实现与单个具体类型匹配时,这非常有效。这是我正在使用的:

kernel.Bind(x => x
    .FromThisAssembly()
    .SelectAllClasses()
    .InheritedFrom(typeof(IQueryHandler<,>))
    .BindSingleInterface()
    .Configure(b => b.WhenInjectedInto(typeof(ValidationHandlerDecorator<,>)).InRequestScope()));
kernel.Bind(typeof(IQueryHandler<,>)).To(typeof(PerformanceHandlerDecorator<,>)).InRequestScope();

但是我遇到了一个场景,我需要在运行时根据自定义路由值覆盖默认的具体类型。以下工作没有问题:

    kernel.Bind<IQueryHandler<query1, result1>>().ToMethod(
    context => HttpContext.Current.Request.RequestContext.RouteData.Values["type"].ToString().ToLower() == "api"
        ? (IQueryHandler<query1, result1>)new apiHandler()
        : (IQueryHandler<query1, result1>)new defaultHandler()
)

上面的问题是我需要为我的每一个 IQueryHandler 泛型类型编写这段代码。此外,对于我想全局应用的每个装饰器(如顶部示例),我必须修改每个绑定并添加它,将代码加倍或加倍。

我希望完成的是使用以下内容。我已经实现了一个类/接口来返回自定义路由数据值。这会运行,但会引发异常,因为在运行时 HttpContext.Current 为空。我在想,因为它没有在运行时解决每个请求。

kernel.Bind<IMyContext>().To<MyContext>().InRequestScope();
kernel.Bind(x => x
.FromThisAssembly()
.SelectAllClasses()
.InheritedFrom(typeof(IQueryHandler<,>))
.StartingWith(kernel.Get<IMyContext>().customRouteValue)    // this isn't valid...
.BindSingleInterface()
.Configure(b => b.InRequestScope())
);

有没有办法使用“ToMethod”或工厂/提供者机制来移动匹配运行时特定值的逻辑并根据命名约定返回具体类型?或者有什么其他想法可以做到这一点?

更新:我使用以下模式进行数据库访问:https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92

所以我对我的数据库的每种类型的查询都有一个 IQueryHandler 的实现。

IQueryHandler<GetDocInfo, DocInfo>
IQueryHandler<GetFileInfo, FileInfo>
IQueryHandler<GetOrderInfo, OrderInfo>
IQueryHandler<GetMessageInfo, MessageInfo>

我的确切问题是跨客户端的某些表有不同的架构,因此我必须根据 url 中的路由配置覆盖某些客户端的实现。

public class defaultschemaGetMessageQueryHandler : IQueryHandler<GetMessageInfo, MessageInfo>
public class client1schemaGetMessageQueryHandler : IQueryHandler<GetMessageInfo, MessageInfo>
public class client2schemaGetMessageQueryHandler : IQueryHandler<GetMessageInfo, MessageInfo>

我有兴趣使用它的另一个地方是覆盖特定的查询实现以从不同的数据存储中提取:API 或 NoSQL。

更新 2 最后更新。所以我采用了下面的代码并进行了修改,以从命名方案转移到基于属性,因为我不希望每个 IQueryable 为每个不同的默认类型命名为“QueryHandler”。

改变了这个:

string route = serviceType.Name.Substring(0, indexOfSuffix);

到这里:

string route = System.ComponentModel.TypeDescriptor
  .GetAttributes(serviceType)
  .OfType<QueryImplementation>()
  .Single()
  .Id;

并添加了我用来装饰我的 IQueryHandlers 的以下属性

[System.AttributeUsage(System.AttributeTargets.Class |
    System.AttributeTargets.Struct)
]
public class QueryImplementation : System.Attribute
{
    public string Id { get { return id; } }

    private string id;

    public QueryImplementation(string id)
    {
        this.id = id;
    }
}

这样使用:

[QueryImplementation("Custom")]
public class CustomDocQueryHandler : IQueryHandler<GetDocInfo, DocInfo>

然后只需要为我的“默认”做同样的事情,以通过属性而不是名称来获取。

【问题讨论】:

  • 所有 IQueryHandler 都有多个实现吗?能有多少?总是有默认值吗?仅考虑静态信息,有没有办法区分默认处理程序和特定处理程序? (命名约定?命名空间约定?属性?,...)
  • 并非所有人都有多个,但很多人都会。大多数只有 2-3 种不同的实现(SQL DB、NoSQL DB、API)。总会有一个默认值。默认实现的命名约定将与接口的名称匹配。自定义将有一个标准化的前缀。但如果需要,我可以将其移至属性或命名空间约定。但主要问题是尝试根据在运行时解析的每个请求值来解析具体类型。
  • 我将为您提供一些示例代码,说明如何做到这一点。但是,您应该注意,它在性能方面“相当”昂贵。如果你没有任何性能问题,它可能会没问题。如果每一点都疼,你可能无论如何都必须切换容器。

标签: c# .net dependency-injection ninject


【解决方案1】:

所以让我为您提供一种实现它的方法。关键字是contextual binding

(但是请注意,性能方面的上下文绑定相当昂贵,因为条件会经常被评估。对于大型 Web 应用程序,这可能是一个问题......)

你已经了解了约定的第一部分,让我用上下文绑定魔法来修改它:

kernel.Bind(x => x
    .FromThisAssembly()
    .SelectAllClasses()
    .InheritedFrom(typeof(IQueryHandler<,>))
    .BindSingleInterface()
    .Configure(QueryHandlerBindingConfigurator.Configure));

public class QueryHandlerBindingConfigurator
{
    private static readonly string DefaultImplementationName =
        RetrieveDefaultImplementationName();

    public static void Configure(
        IBindingWhenInNamedWithOrOnSyntax<object> syntax,
        Type serviceType)
    {
        if (!IsDefaultImplementation(serviceType))
        {
            int indexOfSuffix = serviceType.Name.IndexOf(
                                  DefaultImplementationName,
                                  StringComparison.InvariantCultureIgnoreCase);
            if (indexOfSuffix > 0)
            {
                // specific handler
                string route = serviceType.Name.Substring(0, indexOfSuffix);

                syntax.When(x => route == 
                          syntax.Kernel.Get<IMyContext>().CustomRouteValue);
            }
            else
            {
                // invalid name!
                throw CreateExceptioForNamingConventionViolation(serviceType);
            }
        }

        syntax.InRequestScope();
    }

    private static bool IsDefaultImplementation(Type serviceType)
    {
        return serviceType.Name.StartsWith(
                   DefaultImplementationName,
                   StringComparison.InvariantCultureIgnoreCase);
    }

    private static Exception CreateExceptioForNamingConventionViolation(
        Type type)
    {
        string message = String.Format(
            CultureInfo.InvariantCulture,
            "The type {0} does implement the {1} interface, " +
                "but does not adhere to the naming convention: " +
            Environment.NewLine + "-if it is the default handler, " +
                 "it should  be named {2}" +
            Environment.NewLine + "-if it is an alternate handler, " +
                 "it should be named FooBar{2}, " +
                 "where 'FooBar' is the route key",
            type.Name,
            typeof(IQueryHandler<,>).Name,
            DefaultImplementationName);
        return new ArgumentOutOfRangeException("type", message);
    }

    private static string RetrieveDefaultImplementationName()
    {
        // the name is something like "IQueryHandler`2",
        // we only want "QueryHandler"
        string interfaceName = typeof(IQueryHandler<,>).Name;
        int indexOfApostrophe = interfaceName.IndexOf(
               "`",
               StringComparison.InvariantCulture);
        return interfaceName.Substring(1, indexOfApostrophe - 1);
    }
}

我已经通过以下方式对其进行了测试: (使用 XUnit 和 FluentAssertions)

public class Test
{
    [Fact]
    public void Whoop()
    {
        var kernel = new StandardKernel();
        var contextMock = new Mock<IMyContext>();

        kernel.Bind<IMyContext>().ToConstant(contextMock.Object);
        kernel.Bind(x => x
            .FromThisAssembly()
            .SelectAllClasses()
            .InheritedFrom(typeof(IQueryHandler<,>))
            .BindSingleInterface()
            .Configure(QueryHandlerBindingConfigurator.Configure));

        contextMock.Setup(x => x.CustomRouteValue).Returns(string.Empty);
        kernel.Get<IQueryHandler<int, int>>()
              .Should().BeOfType<QueryHandler>();

        contextMock.Setup(x => x.CustomRouteValue).Returns("AlternativeOne");
        kernel.Get<IQueryHandler<int, int>>()
              .Should().BeOfType<AlternativeOneQueryHandler>();

        contextMock.Setup(x => x.CustomRouteValue).Returns("AlternativeTwo");
        kernel.Get<IQueryHandler<int, int>>()
              .Should().BeOfType<AlternativeTwoQueryHandler>();
    }
}

【讨论】:

  • 这非常接近,但我明白你对性能的意思,特别是当它循环遍历每一个 IQueryHandler 我拥有的,而不仅仅是与 IQueryHandler.
  • 是吗?这似乎有点奇怪,因为在解析期间它应该找到IQueryHandler&lt;T1,T2&gt; 的所有绑定,并且只有少数它应该检查When 约束(但对于所有这些)。
  • 在玩了一些之后,我已经让它与您的确切代码一起工作。问题是每个 IQueryHandler 对于默认实现都需要具有完全相同的名称。我修改为使用属性来声明实现而不是名称模式,这很好用。感谢您的帮助!
  • 哦,好的,关于接口,这是一个误解,因为我认为这就是你想要的。是的属性给你更多的灵活性。你是把它们放在每个实现上还是只放在非默认值上?不客气。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多