Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ninject Convention Based binding to be resolved at runtime

I'm using a Command Handler pattern and binding with ninject.extensions.Conventions, which is working great when my actual IQueryHandler<,> interface implementation matches a single concrete type. Here is what I'm using:

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();

But I've come across a scenario where I need to override the default concrete type at runtime based on a custom route value. The following works without issues:

    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()
)

The problem with the above is that I would need to write this code for every single one of my IQueryHandler<,> generic types. In addition, for each decorator I'd like to apply globally (like the top sample), I would have to modify each binding and add it, doubling or tripling the code.

What I'm hoping to accomplish is use something like the following. I've implemented a class/interface to return the custom Route data value. This runs, but it throws an exception because at runtime the HttpContext.Current is null. I'm thinking because it's not resolving per request at runtime.

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())
);

Is there any way to use "ToMethod" or a Factory/Provider mechanism to move the logic for matching the runtime specific value and return the concrete type based on a naming convention? Or any other ideas to accomplish this?

UPDATE: I'm using the following pattern for DB access: https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92

So I have an implementation of IQueryHandler<,> for each type of query to my DB.

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

My exact issue is I have different schemas for certain tables across clients, so I have to override the implementation for certain clients based on the Route Config in the url.

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

The other place I'm interested in using it would be to override a particular query implementation to pull from a different datastore: API or NoSQL.

UPDATE 2 Final update. So I took the code below and modified to move from naming scheme to Attribute based, as I don't want each IQueryable to be named "QueryHandler" for each different default type.

Changed this:

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

To this:

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

And added the following attribute that I'm using to decorate my 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;
    }
}

Used like this:

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

Then just had to do the same thing for my "default" to get by Attribute instead of Name.

like image 990
Ashley Lee Avatar asked Oct 19 '22 21:10

Ashley Lee


1 Answers

So let me provide you with one way how you can achieve it. The keyword is contextual binding.

(Note however, that performance wise contextual bindings are rather costly since the conditions get evaluated quite often. For a massive web application it could be a problem...)

You've already got the first part of the convention, let me amend it with the contextual binding magic:

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);
    }
}

I've tested it with: (using XUnit and 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>();
    }
}
like image 92
BatteryBackupUnit Avatar answered Oct 22 '22 11:10

BatteryBackupUnit