Reputation: 1709
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
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 causescontainer.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