Replace activator for middleware in ASP.NET Core
Asked Answered
D

1

8

I am trying to instruct my ASP.NET Core MVC application to use a 3rd party DI container. Rather than writing an adapter I am trying to just plug in the the library following the advice in this post

This works pretty well - I can replace the built in IControllerActivator with my own that uses the DI container. However, I am running into a roadblock when trying to instantiate custom middleware that also relies on injected dependencies. ASP.NET cannot resolve these dependencies because it is not using my 3rd party DI container - is there an equivalent of IControllerActivator for middleware, or am I stuck using the built-in DI or writing an adapter?

** EDIT **

Here's some more of my code - I am actually trying to use Ninject using the pattern above.

internal sealed class NinjectControllerActivator : IControllerActivator
{
    private readonly IKernel _kernel;

    public NinjectControllerActivator(IKernel kernel)
    {
        _kernel = kernel;
    }

    [DebuggerStepThrough]
    public object Create(ActionContext context, Type controllerType)
    {
        return _kernel.Get(controllerType);
    }
}

I've discovered I have two problems:

  • I can't inject standard ASP.NET components into my controllers because Ninject is not aware of them
  • My middleware that uses application services can't be instantiated because ASP.NET isn't aware of Ninject.

For an example of the first problem, here's a controller that fails to instantiate because I'm using IUrlHelper (also note the ILogger, which also fails to instantiate):

public class SystemController : Controller 
{
    public SystemController(ILogger logger, IUrlHelper urlHelper) 
    {
         /*...*/
    }
}

Here's an example of the second problem with a custom middleware:

public class CustomMiddleware
{
    private RequestDelegate _next;
    // this is an application specific service registered via my Ninject kernel
    private IPersonService _personService; 

    public CustomMiddleware(RequestDelegate next, IPersonService personService)
    {
        _next = next;
        _personService = personService;
    }

    public async Task Invoke(HttpContext context)
    {
        /* ... */
    }
}

I realize that in theory ASP.NET components should be in their own pipeline and my application components should be in another, but in practice I often need to use components in a cross-cutting way (as in the examples above).

Diathesis answered 7/10, 2015 at 21:1 Comment(11)
What DI container are you trying to use?Flume
@opiants looks like Simple Injector.Bitner
Can you share more information about the middleware you are writing and the dependencies you are trying to inject. Please you what you tried.Leo
"or am I stuck using the built-in DI or writing an adapter". You will never need to build an adapter. Adapters are simply not needed and as I explained here, adapters will only be in the way.Leo
@Leo The only problem with not having an "adapter" I suppose is that all registration chaining that's already made available by the team (services.UseSqlServer, services.UseMvc) will become useless and you'll have to register them yourself manually.Flume
@opiants: That shouldn't be a problem, since you should typically not inject those framework types into your own components. You need to write application-specific abstraction and write an implementation that adapts to those framework pieces to keep your application clean anyway (that's what the DIP advices). Such specific implementation can get the specific framework type injected. And even if you make it less clean, you typically need a handful of types that you need to share. You can register a simple delegate that calls the 'other' container to request the required type.Leo
@Leo True, but would that now mean that you'll now have 2 DI containers in your project (not saying it's plain bad but not saying it's good either)? And what happens if you need to tap into the pipeline like what the author wants and still want to use a more matured/feature-rich container?Flume
@opiants: You shouldn't see it as "2 DI containers". You just have one DI container for your application, and you have the "configuration system of ASP.NET" which internally happens to look like a DI container. In a sense things aren't any different as before. MVC and Web API already had their own configuration system; we never wanted to replace their complete internal configuration system. That doesn't really make sense.Leo
Sorry Steven, I will try to post an update tonight.Diathesis
@Leo sorry for the long delay - posted an update after I messed around with it some more.Diathesis
Steven reading your comments, it sounds like I probably need to write an adapter around the components I need. This feels a little kludgey to me but I will give it a shot - thanks.Diathesis
L
12

The SOLID principles dictate that:

the abstracts are owned by the upper/policy layers (DIP)

Which means that our application code should not depend directly on framework code, even if they are abstractions. Instead we should define role interfaces that are tailored for the use of our application.

So instead of depending on a Microsoft.Framework.Logging.ILogger abstraction, that might or might not fit our application specific needs, the SOLID principles guide us towards abstractions (ports) that are owned by the application, and use adapter implementations that hook into framework code. Here's an example of how your own ILogger abstraction might look like.

When application code depends on your own abstraction, you need an adapter implementation that will be able to forward the call to the implementation supplied by the framework:

public sealed class MsLoggerAdapter : MyApp.ILogger
{
    private readonly Func<Microsoft.Framework.Logging.ILogger> factory;
    public MsLoggerAdapter(Func<Microsoft.Framework.Logging.ILogger> factory) {
        this.factory = factory;
    }

    public void Log(LogEntry entry) {
        var logger = this.factory();
        LogLevel level = ToLogLevel(entry.Severity);
        logger.Log(level, 0, entry.Message, entry.Exception,
            (msg, ex) => ex != null ? ex.Message : msg.ToString());
    }

    private static LogLevel ToLogLevel(LoggingEventType severity) { ... }
}

This adapter can be registered in your application container as follows:

container.RegisterSingleton<MyApp.ILogger>(new MsLoggerAdapter(
    app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>));

BIG WARNING: Do not make direct copies of the framework abstractions. That will almost never lead to good results. you should specify abstractions that are defined in terms of your application. This could even mean that an adapter becomes more complex and needs multiple framework components to fulfill its contract, but this results in cleaner and more maintainable application code.

But if applying SOLID is too much a hassle for you, and you just want to depend directly on external components, you can always cross-wire the required dependencies in your application container as follows:

container.Register<Microsoft.Framework.Logging.ILogger>(
    app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>);

It is as easy as this, but do note that in order to keep your application clean and maintainable, it's much better to define application specific abstractions that adhere to the SOLID principles. Also note that, even if you do this, you only need a few of those cross-wired dependencies anyway. So it's best to still keep your application container as separated as possible from the vNext configuration system.

With the middleware, there is a completely different issue at play here. In your middleware you are injecting runtime data (the next delegate) into a component (the CustomMiddleware class). This is giving you double grief, because this complicates registration and resolving the component and prevents it to be verified and diagnosed by the container. Instead, you should move the next delegate out of the constructor and into the Invoke delegate as follows:

public class CustomMiddleware
{
    private IPersonService _personService;

    public CustomMiddleware(IPersonService personService) {
        _personService = personService;
    }

    public async Task Invoke(HttpContext context, RequestDelegate next) { /* ... */ }
}

Now you can hook your middleware into the pipeline as follows:

app.Use(async (context, next) =>
{
    await container.GetInstance<CustomMiddleware>().Invoke(context, next);
});

But don't forget that you can always create your middleware by hand as follows:

var frameworkServices = app.ApplicationServices;

app.Use(async (context, next) =>
{
    var mw = new CustomMiddleware(
        container.GetInstance<IPersonService>(),
        container.GetInstance<IApplicationSomething>(),
        frameworkServices.GetRequiredService<ILogger>(),
        frameworkServices.GetRequiredService<AspNetSomething>());

    await mw.Invoke(context, next);
});

It's really unfortunate that ASP.NET calls its own services ApplicationServices, because that's where your own application container is for; not the built-in configuration system.

Leo answered 14/10, 2015 at 7:11 Comment(5)
I understand the principles that are being violated by the happy path, but the alternative looks pretty awful. Following the principles to the letter means wrapping all abstractions outside of your control and cross wiring the external dependencies into the application container. Do you really think that's better?Idiomorphic
@Idiomorphic I don't suggest cross-wiring external dependencies in the application container AT ALL, because this means that application components can still depend on external dependencies, while those should be wrapped by adapters. And don't forget you only need to wrap the abstractions that your application actually uses, normally just a few. Whether 'it is better' depends on the size and lifetime of the application at hand, but in general I would say that is actually is much, much better.Leo
Even worse then, it means you need to keep them out of the application container, get them somehow and manually pass them to the adapter. That's honestly pretty horrific IMO. The good news is, this pattern is doable but just not very nice from an aesthetic POV.Idiomorphic
@Idiomorphic I really have no idea what you are talking about. Passing stuff into an adapter is simple; that never caused any trouble to me. You don't need to abuse your container for that.Leo
Sure, we'll let the users decide.Idiomorphic

© 2022 - 2024 — McMap. All rights reserved.