如何通过依赖注入传递 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.Current
、CallContext.Get/SetData
或 Thread.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 应用程序中),您也有这样的选择(同样,使用自定义同步上下文):
【讨论】:
【参考方案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.Current
在await
之后访问时可能不正确。
@Jeff Walker Code Ranger:看起来这不是问题:***.com/questions/13557798/…
据我所知,如果 API 是自托管的,HttpContext.Current 也不存在。
@Michael Stum:这就是我第一点的意思。您的服务层是tightly coupled
与您的应用程序层。它不能与控制台、win 表单、...一起使用,因为没有 HttpContext。
@MichaelStum:我已经更新了答案,想法是一样的,但使用了不同的方法,请看一下并发表你的意见,谢谢。以上是关于如何通过依赖注入传递 Web API 请求?的主要内容,如果未能解决你的问题,请参考以下文章