如何通过依赖注入传递 Web API 请求?

Posted

技术标签:

【中文标题】如何通过依赖注入传递 Web API 请求?【英文标题】:How would I pass a Web API Request through Dependency Injection? 【发布时间】:2014-06-18 21:55:49 【问题描述】:

我有一个 Web API 应用程序,其中控制器通过依赖注入 (Unity) 将服务/存储库等注入其中。假设我有一个 IStuffService 需要当前请求的 IPrincipal(或围绕它的包装器)。

Web API 的问题似乎是当前请求/用户的唯一可靠来源是ApiController实例 上的Request 属性.由于 Web API 的同步特性,不能保证任何静态内容(无论是 HttpContext.CurrentCallContext.Get/SetDataThread.Get/SetData)都在同一个线程上。

我如何可靠地确保特定于请求的上下文通过依赖项传递,更重要的是,该操作在整个操作过程中始终保持正确的IPrincipal

两种选择:

    每个需要 IPrincipal 的方法都将它作为方法的参数 - 这是最可靠的方法,但它也要求我在每个方法签名中都有那个东西 将 IPrincipal 注入到服务的 ctor 中,在每个请求上启动对象图的新实例,使用 Unity 中的 DependencyOverride:container.Resolve(opType, new DependencyOverride(typeof(IPrincipal), principal))

选项 2 意味着我的方法签名是干净的,但这也意味着我需要确保所有依赖项都使用 TransientLifetimeManager,而不是单例甚至每线程。

有没有比我没有看到的更好的解决方案?

【问题讨论】:

理想情况下,您是否希望您的 IStuffService 实现在所有请求中都是单例? @Xenolightning 但是当我进行方法调用时,服务如何获取当前的IPrincipal 有趣的问题,但为什么你的控制器中的每个方法都需要IPrincipal @DavidG 我的服务中需要它(它在我的控制器中可用)。一个示例是记录哪个用户进行了更改,或者确定给定当前用户的服务调用是否合法。这是一个业务逻辑级别的决策,存在于服务中,需要知道谁在调用来做出该决策。 @MichaelStum 这种逻辑在AuthorizeAttribute 中不是更好吗? 【参考方案1】:

来自cmets:

@MichaelStum,我相信 HttpContext.User 应该正确流动 跨 async/await(在同一个 HTTP 请求中)。不适合你吗? – Noseratio 17 小时前

@Noseratio 查看其他答案 - 在 .net 4.0 中,它绑定到 当前线程并且没有得到适当的维护。似乎在4.5中, 这可能是固定的。也就是说, HttpContext.Current 仍然不是那个 适用于 Web API,因为在自托管的 API 上没有 HttpContext.Current。

AFAIK,无论如何,在 ASP.NET 4.0 中都没有对 async/await 的适当支持(您可能可以使用 Microsoft.Bcl.Async 获得语言支持,但没有 ASP.NET 运行时支持,因此您必须使用 @987654321 @ 来实现 TAP 模式)。

也就是说,我 99% 确定 Thread.CurrentPrincipal 仍会在 ASP.NET 4.0 中正确地流经 await 延续。那是因为它作为ExecutionContext 流的一部分进行流动,而不是通过同步上下文。至于HtttContext.Current.User,我不确定它是否会在 ASP.NET 4.0 中正确流动(尽管它在 ASP.NET 4.5 中确实如此)。

我已重新阅读您的问题,但可以找到关于 Thread.CurrentPrincipal 未正确处理的明确投诉。您是否在现有代码中遇到过这个问题(如果有,可能是 ASP.NET 中的一个错误)?

以下是相关问题的列表,由 Stephen Cleary 提供了一些深刻的见解:

Understanding context in C# 5 async/await

Why is an "await Task.Yield()" required for Thread.CurrentPrincipal to flow correctly?

Using ASP.NET Web API, my ExecutionContext isn't flowing in async actions

Scott Hanselman 的这篇博文也与此相关,尽管他谈到了 WebForms:

System.Threading.Thread.CurrentPrincipal vs. System.Web.HttpContext.Current.User or why FormsAuthentication can be subtle

如果您担心自托管场景,我相信Thread.CurrentPrincipal 仍会在那里正确流动(一旦设置为正确的身份)。如果您想流动任何其他属性(those which get automatically flowed with ExecutionContext 除外),您可以推出自己的同步上下文。另一种选择(不太好,IMO)是use custom awaiters。

最后,如果您遇到实际需要跨 await 延续的线程亲和性的情况(很像在客户端 UI 应用程序中),您也有这样的选择(同样,使用自定义同步上下文):

How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API?

【讨论】:

【参考方案2】:

最终的答案是我们的 IoC 容器需要更改以更好地支持 async/await。

背景:

在 .NET 4 和 .NET 4.5 之间,围绕此问题的 async/await 行为发生了变化。在 .NET 4.5 中引入了SynchronizationContext,它将正确恢复HttpContext.Current(请参阅http://msdn.microsoft.com/en-us/magazine/gg598924.aspx)。但是,使用.ConfigureAwait(false)(参见http://msdn.microsoft.com/en-us/magazine/jj991977.aspx 中的“配置上下文”)通常是最佳实践,并且特别要求不保留上下文。在这种情况下,您仍然会遇到您描述的问题。

答案:

我能够在自己的代码中提出的最佳答案是确保在 Web 请求的早期请求来自 HttpContext.Current(在您的情况下为 IPrincipal)的依赖项,以便加载放入容器中。

我对 Unity 没有任何经验,但在 Ninject 中这看起来像:

kernal.Bind<IPrincipal>().ToMethod(c => HttpContext.Current.User).InRequestScope();

然后我会确保在您丢失上下文之前在 Web 请求的早期加载 IPrincipal。在 BeginRequest 中或作为控制器的依赖项。这将导致 IPrincipal 被加载到该请求的容器中。

注意:在某些情况下这可能不起作用。我不知道 Unity 是否有这个问题,但我知道 Ninject 有。它实际上使用 HttpContext.Current 来确定哪个请求处于活动状态。因此,如果您稍后尝试从容器中解析某些内容,例如服务定位器或工厂,那么它可能无法解析。

【讨论】:

当我们在另一个线程(异步、等待或任何其他方法...)执行代码时,您确定这可以解决问题吗? 容器如何知道新线程是related,当前线程有上下文?【参考方案3】:

我知道这是一个老问题,我曾使用选项一(在问题中)并且它有效。

更新:我删除了我的答案,因为我意识到我发布了一些不起作用的东西。 给您带来不便,敬请见谅。

【讨论】:

调用构造函数时请求还不可用。这行不通【参考方案4】:

如何可靠地确保传递特定于请求的上下文 通过依赖关系,更重要的是,操作保留 整个操作过程中的 IPrincipal 是否正确?

我认为你不应该这样做。您的服务比您的 Api 控制器低层。你的服务不应该依赖于任何与高层相关的类,否则你的服务不能被重用,例如:当你需要在上面构建一个win forms应用程序时现有服务。

IPrincipal 不适合注入我们的服务,因为它与网络应用程序相关。当我们将此信息传递到较低层(服务)时,我们应该传递我们的 neutral-classes 或仅传递一个 userId 以将我们的服务与使用它的应用程序。

您应该为用户定义自己的类以及与请求相关的任何内容,以便在我们的服务层中使用,因为它更与域相关。这样一来,您的服务层就与应用层(Web、win 表单、控制台等)无关:

public class AppPrincipal : IAppPrincipal

   public int UserId  get; set; 
   public string Role  get; set; 
   //other properties
   public AppPrincipal() 

   
   public AppPrincipal(int userId, string role):this() 
       UserId  = userId;
       Role = role;
   

然后,您可以在 Web 应用程序中将 IAppPrincipal 注册为每个请求范围,并使用您的 IPrincipal 填充所有属性。这将在任何await/async 调用之前为您的整个对象图初始化您的IAppPrincipal。 Unity 示例代码:

public void RegisterTypes(IUnityContainer container)

    container.RegisterType<IAppPrincipal>(
                new PerRequestLifetimeManager(),
                new InjectionFactory(c => CreateAppPrincipal()));

public IAppPrincipal CreateAppPrincipal()

    var principal = new AppPrincipal();
    principal.UserId = //Retrieve userId from your IPrincipal (HttpContext.Current.User)
    principal.Role = //Retrieve role from your IPrincipal (HttpContext.Current.User)
    return principal;

这里的关键是我们已经将我们的服务层与网络解耦了。如果您需要重用服务层来构建 Windows 窗体或控制台应用程序,您可以将 IAppPrincipal 注册为单例并以不同方式填充它。

我们不需要处理像async/await这样的平台相关问题

【讨论】:

根本不回答这个问题。正如他所说,HttpContext.Currentawait 之后访问时可能不正确。 @Jeff Walker Code Ranger:看起来这不是问题:***.com/questions/13557798/… 据我所知,如果 API 是自托管的,HttpContext.Current 也不存在。 @Michael Stum:这就是我第一点的意思。您的服务层是tightly coupled 与您的应用程序层。它不能与控制台、win 表单、...一起使用,因为没有 HttpContext。 @MichaelStum:我已经更新了答案,想法是一样的,但使用了不同的方法,请看一下并发表你的意见,谢谢。

以上是关于如何通过依赖注入传递 Web API 请求?的主要内容,如果未能解决你的问题,请参考以下文章

在 .NET Web Api 的控制器中捕获依赖注入错误

Angular依赖注入小解

如何通过某些依赖注入来限制对象创建?

用于依赖注入的 MVC Web API 和 Unity

.NET Core Web API使用依赖注入(DI)进行服务配置一

Simple Injector 无法在 Web API 控制器中注入依赖项