xander
xander

Reputation: 1709

Injecting IUrlHelper with Simple Injector

I'm working on an ASP.NET Core app using Simple Injector for dependency injection duties. I'm looking for a way to inject IUrlHelper into controllers. I know about IUrlHelperFactory, but would prefer to directly inject IUrlHelper to keep things a little tidier and simpler to mock.

The following questions have useful answers for injecting IUrlHelper through the standard ASP.Net Dependency Injection:

Based on those answers, I came up with a comparable SimpleInjector registration:

container.Register<IUrlHelper>(
    () => container.GetInstance<IUrlHelperFactory>().GetUrlHelper(
        container.GetInstance<IActionContextAccessor>().ActionContext));

It does work, but because IActionContextAccessor.ActionContext returns null when there's no active HTTP request, this binding causes container.Verify() to fail when called during app startup.

(It's worth noting that the ASP.Net DI registrations from the linked questions also work through cross-wiring, but suffer the same problem.)

As a workaround, I've designed a proxy class...

class UrlHelperProxy : IUrlHelper
{
    // Lazy-load because an IUrlHelper can only be created within an HTTP request scope,
    // and will fail during container validation.
    private readonly Lazy<IUrlHelper> realUrlHelper;

    public UrlHelperProxy(IActionContextAccessor accessor, IUrlHelperFactory factory)
    {
        realUrlHelper = new Lazy<IUrlHelper>(
            () => factory.GetUrlHelper(accessor.ActionContext));
    }

    public ActionContext ActionContext => UrlHelper.ActionContext;
    public string Action(UrlActionContext context) => UrlHelper.Action(context);
    public string Content(string contentPath) => UrlHelper.Content(contentPath);
    public bool IsLocalUrl(string url) => UrlHelper.IsLocalUrl(url);
    public string Link(string name, object values) => UrlHelper.Link(name, values);
    public string RouteUrl(UrlRouteContext context) => UrlHelper.RouteUrl(context);
    private IUrlHelper UrlHelper => realUrlHelper.Value;
}

Which then has a standard registration...

container.Register<IUrlHelper, UrlHelperProxy>(Lifestyle.Scoped);

This works, but leaves me with the following questions:

To the second point: The MVC architects clearly wanted us to inject IUrlHelperFactory, and not IUrlHelper. That's because of the need for an HTTP request when creating a URL Helper (see here and here). The registrations I've come up with do obscure that dependency, but don't fundamentally change it--if a helper can't be created, we're probably just going to throw an exception either way. Am I missing something that makes this more risky than I realize?

Upvotes: 4

Views: 2089

Answers (1)

Steven
Steven

Reputation: 172826

Based on those answers, I came up with a comparable SimpleInjector registration:

container.Register<IUrlHelper>(
    () => container.GetInstance<IUrlHelperFactory>().GetUrlHelper(
        container.GetInstance<IActionContextAccessor>().ActionContext));

It does work, but because IActionContextAccessor.ActionContext returns null when there's no active HTTP request, this binding causes container.Verify() to fail when called during app startup.

The underlying problem here is that the construction of the IUrlHelper requires runtime data, and runtime data should not be used while constructing object graphs. This is very similar to the code smell of Injecting runtime data into components.

As a workaround, I've designed a proxy class...

I don't consider the proxy to be a workaround at all. As I see it, you pretty much nailed it. A proxy (or adapter) is the way to defer the creation of runtime data. I would typically go with an adapter and define an application-specific abstraction, but that would be counter-productive in this case, as ASP.NET Core defines many extension methods on IUrlHelper. Defining your own abstraction would probably mean having to create many new methods, as you are likely to require several of them.

Is there a better/simpler way?

I don't think there is, although your proxy implementation can work as well without the use of a Lazy<T>:

class UrlHelperProxy : IUrlHelper
{
    private readonly IActionContextAccessor accessor;
    private readonly IUrlHelperFactory factory;

    public UrlHelperProxy(IActionContextAccessor accessor, IUrlHelperFactory factory)
    {
        this.accessor = accessor;
        this.factory = factory;
    }

    public ActionContext ActionContext => UrlHelper.ActionContext;
    public string Action(UrlActionContext context) => UrlHelper.Action(context);
    public string Content(string contentPath) => UrlHelper.Content(contentPath);
    public bool IsLocalUrl(string url) => UrlHelper.IsLocalUrl(url);
    public string Link(string name, object values) => UrlHelper.Link(name, values);
    public string RouteUrl(UrlRouteContext context) => UrlHelper.RouteUrl(context);
    private IUrlHelper UrlHelper => factory.GetUrlHelper(accessor.ActionContext);
}

Both IActionContextAccessor and IUrlHelperFactory are singletons (or at least, their implementations can be registered as singleton), so you can register the UrlHelperProxy as singleton as well:

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddSingleton<IUrlHelper, UrlHelperProxy>();

ActionContextAccessor uses AsyncLocal storage to store the ActionContext for the duration of the request. And IUrlHelperFactory uses the ActionContext's HttpContext to cache a created IUrlHelper for the duration of that request. Calling factory.GetUrlHelper multiple times for the same request will, therefore, result in the same IUrlHelper to be returned for the duration of that request. That's why you don't need to cache the IUrlHelper inside the proxy.

Also notice that I now registered both IActionContextAccessor and IUrlHelper in MS.DI instead of Simple Injector. This is to show that this solution works just as well when using the built-in container or any other DI Container--not just with Simple Injector.

Is this just a bad idea?

Absolutely not. I'm even wondering why such proxy implementation is not defined out-of-the-box by the ASP.NET Core team.

The MVC architects clearly wanted us to inject IUrlHelperFactory, and not IUrlHelper.

This is because those architects realized that object graphs should not be built using runtime data. This is actually something I discussed with them in the past as well.

The only risk I see here is that you can inject an IUrlHelper in a context where there is no web request, which will cause the use of the proxy to throw an exception. That problem, however, exists as well when you inject an IActionContextAccessor, so I don't find this a big deal.

Upvotes: 2

Related Questions