Ninject基于约定的绑定在运行时解析

5

我正在使用命令处理程序模式,并与ninect.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<,>都有多个实现吗?可以有多少个?是否总是有一个默认值?仅考虑静态信息,有没有一种方法可以区分默认处理程序和特定处理程序?(命名约定?命名空间约定?属性?...) - BatteryBackupUnit
并非所有都有多个,但很多会有。主要只有2-3种不同的实现(SQL数据库,NoSQL数据库,API)。总会有一个默认值。默认实现的命名约定将与接口名称匹配。自定义实现将具有标准化前缀。但如果需要,我可以将其移动到属性或命名空间约定中。但主要问题是尝试根据在运行时解析的每个请求值来解决具体类型。 - Ashley Lee
我会为你提供一些示例代码,告诉你如何实现这个功能。但是需要注意的是,从性能角度来看,这种方法相对来说比较耗费资源。如果你没有性能问题,那么使用这种方法应该没问题。但是如果每一个小细节都很重要,那么你可能需要考虑切换容器了。 - BatteryBackupUnit
1个回答

3
所以,让我向您介绍一种实现它的方式。关键词是上下文绑定
(请注意,从性能角度来看,上下文绑定相当昂贵,因为条件经常被评估。对于大型 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<obj1,obj2>实际特定接口匹配的那些。 - Ashley Lee
这是不是有点奇怪呢?因为在解析时,它应该找到所有 IQueryHandler<T1,T2> 的绑定,只检查那些满足 When 约束的绑定(但对于所有这些绑定都要检查)。 - BatteryBackupUnit
经过更多的尝试,我已经成功地使用了您提供的代码。问题在于每个IQueryHandler<T1,T2>需要具有完全相同的名称以进行默认实现。我修改了使用属性来声明实现而不是名称模式,这非常有效。感谢您的帮助! - Ashley Lee
哦,好的,关于接口那个问题是误解了,因为我以为那就是你想要的。是的,属性会让你更加灵活。你把它们放在每个实现上了吗,还是只放在非默认的上面?不客气。 - BatteryBackupUnit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接