使用Castle Windsor解析HttpControllerContext

28
ASP.NET Web API中,HttpControllerContext实例提供了关于当前环境的大量信息,包括当前请求的URI。
如果服务依赖于这些信息(例如请求URI),则应该可以将该信息注入到服务中。
使用Poor Man's DI很容易做到这一点:只需实现自定义的IHttpControllerActivator即可。
然而,使用Castle Windsor就变得非常困难。以前,我描述了一种非常复杂的方法来解决这个问题,但它依赖于PerWebRequest生命周期,并且结果发现,这个生命周期在自托管场景中无法工作,因为HttpContext.Current为空。
到目前为止,我已经通过将所需的信息作为内联参数传递给来自自定义IHttpControllerActivator的Resolve方法,使其能够正常工作:
public IHttpController Create(
    HttpControllerContext controllerContext,
    Type controllerType)
{
    var baseUri = new Uri(
        controllerContext
            .Request
            .RequestUri
            .GetLeftPart(UriPartial.Authority));

    return (IHttpController)this.container.Resolve(
        controllerType,
        new { baseUri = baseUri });
}

然而,默认情况下,只有在立即请求的类型依赖于参数时(即如果请求的控制器本身依赖于baseUri),此方法才起作用。如果对baseUri的依赖在依赖层次结构中更深层次,则默认情况下不起作用,因为内联参数不会传播到更深层次。
可以通过自定义IDependencyResolver(Castle Windsor IDependencyResolver,而不是ASP.NET Web API IDependencyResolver)来更改此行为:
public class InlineDependenciesPropagatingDependencyResolver :
    DefaultDependencyResolver
{
    protected override CreationContext RebuildContextForParameter(
        CreationContext current, Type parameterType)
    {
        if (parameterType.ContainsGenericParameters)
        {
            return current;
        }

        return new CreationContext(parameterType, current, true);
    }
}

请注意,构造函数参数propagateInlineDependencies传递的是true,而不是默认实现false
要将容器实例与InlineDependenciesPropagatingDependencyResolver类连接起来,必须以此方式构建它:
this.container = 
    new WindsorContainer(
        new DefaultKernel(
            new InlineDependenciesPropagatingDependencyResolver(),
            new DefaultProxyFactory()),
        new DefaultComponentInstaller());

我在想这是否是解决这个问题的最佳方案,或者是否有更好/更简单的方法?


为什么你想要这样做呢?如果是为了单元测试,我曾经花了很长时间一直碰壁在同一个问题上,最后只能转向依赖于上下文中的信息进行集成测试的控制器。 - Maess
问题中的链接提供了这样做的理由。http://blog.ploeh.dk/2012/04/17/InjectingHttpControllerContextWithTheASPNETWebAPI.aspx - Mark Seemann
@MarkSeemann 你好 Mark,除了你在这里发布的博客文章 http://blog.ploeh.dk/2012/04/19/WiringHttpControllerContextWithCastleWindsor/ ,你有没有想到其他更好的解决方案来解决这个问题? - Farhad-Taran
1
@Xerxes 不是的,但现在我专门使用Pure DI,所以Pure DI方法是我现在使用的方法。这是使用Pure DI正确连接Web API的方法:http://blog.ploeh.dk/2012/09/28/DependencyInjectionandLifetimeManagementwithASP.NETWebAPI/ - Mark Seemann
@MarkSeemann 感谢你提供的链接和博客,真是个宝藏 :) - Farhad-Taran
2个回答

3

为了完整起见,我从Krzysztof Koźmic(Castle Windsor的当前维护者)在Twitter上得到的答案表明,在实现这个特定目标的方式上,问题中概述的方法确实是正确的。

(然而,由于Krzysztof的Twitter账户受保护(推文不可公开查看),我无法链接到该推文。)


2
我认为你的InlineDependenciesPropagatingDependencyResolver实际上掩盖了您的应用程序架构中相当关键的一些内容:即一个或多个组件具有无法从容器或动态上下文静态可靠地解析的依赖项。
它违反了大多数开发人员在将内联依赖项传递给Resolve()时会做出的假设(它们仅在依赖关系解析的一级下传递),并且在某些情况下可能会导致依赖关系错误地覆盖其他配置的服务。 (例如,如果您有另一个许多层次下具有相同类型和名称的依赖项的组件)。它可能是非常难以识别的错误的潜在原因。
这个问题的核心是DI的一个困难问题,并且实际上表明IoC并不真正可行(即我们的依赖关系需要被“推送”,而不能由容器“拉”来为我们解决)。我认为有两个选择:
1)纠正阻止“反转”的问题。即包装HttpControllerContext / HttpContext,增强该包装器以在自托管场景中按要求进行操作,并使您的组件依赖于该包装器,而不是直接依赖于HttpControllerContext / HttpContext。
2)反映您正在使用的环境的缺陷(它不完全支持“反转”),并使处理这些缺陷的解决方案非常明确。在您的情况下,这可能涉及利用一个类型工厂(接口)来实例化需要baseUri的组件,并在IHttpControllerActivator.Create()中进行显式构建依赖关系直到您拥有控制器。
我会选择第二种选项,因为当约定不够时,我更喜欢尽可能明确。
// Typed factories for components that have dependencies, which cannot be resolved statically
IBFactory bFactory; 
IAFactory aFactory;

public IHttpController Create(HttpControllerContext controllerContext, Type controllerType)
{
    if (controllerType == typeof(A))
    {
        // Special handling for controller where one or more dependencies
        // are only available via controllerContext.
        var baseUri = new Uri(controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Authority));
        B b = this.bFactory.Create(baseUri);
        return this.aFactory.Create(b);
    }
    // Default for all other controllers 
    return (IHttpController)this.container.Resolve(controllerType);
}

重点在于,它明确地解决了我们环境存在的缺陷,将受影响的类型与我们提供的依赖项重载具体绑定,并确保我们不会意外地覆盖任何其他依赖项。

1
如果我们做出(相当合理的)假设,即 ASP.NET Web API 公开的可扩展点(例如 IHttpControllerActivator)是固定的且无法更改,那么您如何实现您的两个选项之一? - Mark Seemann
1
据我所知,这个解决方案建立在你确切地知道基础URI在依赖图中的位置和深度的假设上。这似乎是一个非常脆弱的解决方案——例如,如果我重构API以使基础URI向下推一层,这将开始失败,我将不得不在我的自定义IHttpControllerActivator中手动修复它。由于我目前正在使用完全基于约定的依赖关系连接,这似乎不是一个好的权衡。 - Mark Seemann
该建议基于Krzysztof Koźmic在此答案中提出的相同原则:https://dev59.com/PlHTa4cB1Zd3GeqPOQ4a,他解释说将内联依赖项传递到解析管道中会破坏抽象。对我来说,如果您重构API时它会崩溃这一事实是可取的。这种依赖关系只能在特定情况下(即通过一个执行路径)可靠地提供,这一现实不应被掩盖。 - Phil Degenhardt
1
哪个合同会被违反?这是纯基础设施。这里发生的是,IHttpControllerActivator.Create 是我们唯一可以从当前请求上下文中提取数据的地方。我们将该数据传递给解析上下文。如果任何类依赖于此数据,则它就在那里。如果没有,也没关系。在两种情况下,都不会出现任何问题。 - Mark Seemann
是的,现在它肯定会工作。但是你正在覆盖Uri baseUri构造函数参数上的每一个依赖项,即使这样的对象已经在容器中正确配置了。这创造了一种奇怪的情况,如果我创建一个新组件,在它的构造函数中需要一个Uri baseUri参数,并将容器配置为静态提供它,那么你的Resolve()调用将覆盖静态配置的对象。然而,如果我将参数命名为'caseUri',你的Resolve()调用就不会覆盖它。 - Phil Degenhardt
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接