问题
In the ASP.NET Web API, HttpControllerContext instances provide a lot of information about the current environment, including the URI of the current request.
If a service relies on such information (e.g. the request URI), it should be possible to inject that information into the service.
This is pretty easy to do using Poor Man's DI: just implement a custom IHttpControllerActivator.
However, with Castle Windsor this suddenly becomes very difficult. Previously, I've described a very convoluted way to resolve this issue, but it hinges on the PerWebRequest lifestyle, and it turns out that this lifestyle doesn't work in self-hosting scenarios, because HttpContext.Current is empty.
So far, I've been able to make this work by passing the desired information as an inline argument to the Resolve method from a custom IHttpControllerActivator:
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 });
}
However, by default, this only works if the immediately requested type relies on the argument (i.e. if the requested Controller itself depends on the baseUri
). If the dependency on baseUri
is buried deeper in the dependency hierarchy, it doesn't work by default, because inline arguments aren't propagated to deeper layers.
This behavior can be changed with a custom IDependencyResolver (a Castle Windsor IDependencyResolver, not an 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);
}
}
Notice that true
is being passed as the propagateInlineDependencies
constructor argument instead of false
, which is the default implementation.
In order to wire up a container instance with the InlineDependenciesPropagatingDependencyResolver class, it must be constructed in this way:
this.container =
new WindsorContainer(
new DefaultKernel(
new InlineDependenciesPropagatingDependencyResolver(),
new DefaultProxyFactory()),
new DefaultComponentInstaller());
I'm wondering if this is the best solution to this problem, or if there's a better/simpler way?
回答1:
It seems to me that your InlineDependenciesPropagatingDependencyResolver is actually masking something fairly critical to the architecture of your application: that one or more of your components have dependencies that cannot be reliably resolved statically, from the container, or from dynamic context.
It violates the assumption most developers would make when passing inline dependencies to Resolve() (that they only get passed down one level of dependency resolution) and in certain scenarios could cause a dependency to incorrectly override some other configured service. (E.g. if you had another component many levels down which had a dependency of the same type and name). It could be a potential cause of bugs that would be very difficult to identify.
The issue at the heart of this is a difficult one for DI and really indicates that IoC is not really feasible (i.e. our dependency(ies) need to be 'pushed' and cannot be 'pulled' for us by the container). It seems to me there are two options:
1) rectify the problem that is preventing 'inversion'. i.e. wrap the HttpControllerContext/HttpContext, augment that wrapper to behave as required in a self-hosted scenario and have your components rely on that wrapper, rather than HttpControllerContext/HttpContext directly.
2) reflect the shortcomings of the environment you are working with (that it doesn't fully support 'inversion') and make the workaround to handle those shortcomings very explicit. In your scenario this would probably involve utilising a typed factory (interface) to instantiate the component requiring a baseUri
in your IHttpControllerActivator.Create()
. This would mean that if this component was further down the dependency hierarchy, you would need to explicitly build up your dependency hierarchy until you had your controller.
I would probably go for the second option simply because when conventions don't cut it I prefer to be as explicit as possible.
UPDATED
Assuming we had a controller type A
, which relied on component B
, which in turn relied on the baseUri, the second option might look something like:
// 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);
}
The key points are that this explicitly deals with the shortcomings of our environment, it binds the affected types specifically with the dependency overrides we provide imperatively and ensures that we are not accidentally overriding any other dependencies.
回答2:
Just for the sake of completeness, an answer I got from Krzysztof Koźmic (the current maintainer of Castle Windsor) on Twitter indicated that the method outlined in the question is, indeed, the correct way of achieving this particular goal.
(However, I can't link to that tweet, since Krzysztof's twitter account is protected (tweets are not publicly visible.))
来源:https://stackoverflow.com/questions/10854701/resolving-httpcontrollercontext-with-castle-windsor