Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace activator for middleware in ASP.NET Core

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

like image 957
Joshua Barron Avatar asked Oct 07 '15 21:10

Joshua Barron


People also ask

Which package provides middleware in ASP.NET Core?

AspNetCore. Builder namespace. UseExceptionHandler is the first middleware component added to the pipeline.

Where do I register middleware in .NET Core?

Middlewares are registered in the Startup. cs file in a . NET Core application. Configure method handles all HTTP requests.

What is IApplicationBuilder in .NET Core?

IApplicationBuilder An object that provides the mechanisms to configure an application's request pipeline.


1 Answers

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.

like image 158
Steven Avatar answered Oct 23 '22 00:10

Steven