使用简单注入器注入 IUrlHelper

Injecting IUrlHelper with Simple Injector

我正在开发一个 ASP.NET 核心应用程序,使用 Simple Injector 来执行依赖注入任务。我正在寻找一种将 IUrlHelper 注入控制器的方法。我知道 IUrlHelperFactory,但更愿意直接注入 IUrlHelper 以使事情更整洁,更易于模拟。

以下问题对于通过标准 ASP.Net 依赖注入注入 IUrlHelper 很有用:

基于这些答案,我想出了一个类似的 SimpleInjector 注册:

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

它确实有效,但是因为 IActionContextAccessor.ActionContext returns null 当没有活动的 HTTP 请求时,此绑定会导致 container.Verify() 在应用程序启动期间调用时失败。

(值得注意的是,链接问题中的 ASP.Net DI 注册也可以通过交叉连接工作​​,但遇到同样的问题。)

作为解决方法,我设计了一个代理 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;
}

然后有一个标准的注册...

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

这有效,但给我留下了以下问题:

第二点:MVC 架构师显然希望我们注入 IUrlHelperFactory,而不是 IUrlHelper。这是因为在创建 URL Helper 时需要 HTTP 请求(参见 here and here)。我提出的注册确实掩盖了这种依赖性,但并没有从根本上改变它——如果无法创建助手,我们可能会以任何一种方式抛出异常。我是否遗漏了一些让这比我意识到的更危险的东西?

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.

这里的根本问题是 IUrlHelper 的构造需要运行时数据,而构造对象图时不应使用运行时数据。这与 Injecting runtime data into components.

的代码味道非常相似

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

我根本不认为代理是解决方法。在我看来,你几乎做到了。代理(或适配器)是推迟创建运行时数据的方法。我通常会使用适配器并定义特定于应用程序的抽象,但在这种情况下会适得其反,因为 ASP.NET Core 在 IUrlHelper 上定义了 many 扩展方法。定义您自己的抽象可能意味着必须创建许多新方法,因为您可能需要其中的几个。

Is there a better/simpler way?

我认为没有,尽管您的代理实现在不使用 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);
}

IActionContextAccessorIUrlHelperFactory都是单例(或者至少,它们的实现可以注册为单例),所以你也可以将UrlHelperProxy注册为单例:

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

ActionContextAccessor 在请求期间使用 AsyncLocal 存储来存储 ActionContextIUrlHelperFactory 使用 ActionContextHttpContext 在请求期间缓存创建的 IUrlHelper。因此,为同一请求多次调用 factory.GetUrlHelper 将导致在该请求期间返回相同的 IUrlHelper。这就是为什么您不需要在代理中缓存 IUrlHelper

另请注意,我现在在 MS.DI 中同时注册了 IActionContextAccessorIUrlHelper 而不是 Simple Injector。这是为了表明此解决方案在使用内置容器或任何其他 DI 容器时同样有效——而不仅仅是使用 Simple Injector。

Is this just a bad idea?

绝对不是。我什至想知道为什么 ASP.NET 核心团队没有立即定义这种代理实现。

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

这是因为那些架构师意识到不应使用运行时数据构建对象图。这实际上也是我过去与他们一起 discussed 的事情。

我在这里看到的唯一风险是你可以在没有网络请求的上下文中注入一个IUrlHelper,这将导致使用代理抛出异常。然而,当你注入一个 IActionContextAccessor 时,这个问题也存在,所以我觉得这没什么大不了的。