Unity IOC Recursion - A use for interceptors?
So, I'm trying to solve a problem that I'm sure someone else has already encountered. Basically, I want the call to my IoC container to recursively resolve the dependencies, but also potentially execute some custom code to alter the outcome based on a set of predefined criteria. That's vague, so let me give an example:
Say I have a controller like so:
public class SampleController : Controller
{
protected SampleType _sampleType = null;
public SampleController(SampleType sampleType)
{
_sampleType = sampleType;
}
}
I've also got some test versions of this controller (say I refactored it and I want to make sure it doesnt break horribly in prod by AB testing it's exposure):
public class SampleController_V2 : SampleController
{
protected SampleType _sampleType = null;
protected AnotherType _anotherType = null;
public SampleController_V2(SampleType sampleType, AnotherType anotherType)
{
_sampleType = sampleType;
_anotherType = anotherType;
}
}
I've extended the DefaultControllerFactory
to use Unity when creating the controllers. This is all working fine. Now, what I want to do, it provide the ability to AB test any particular type in the hierarchy if things to resolve. This works well for the top level, but not for child elements as it recurses through the object graph.
Right now, it will choose the appropriate controller to resolve and give it its dependencies. However, I cant seem to intercept the individual calls to dependencies to also AB test those. I can define a test through database configuration, and then have the IOC container resolve it based on the criteria. Example:
SessionIds that start with the letter 'a': SampleController_V2
Everyone Else : SampleController
UserIds ending in 9 : SampleType_V2
Everyone Else : SampleType
This all works for the top level item. However, the call to _unityContainer.Resolve(type)
doesnt seem to be a recursive call; I'd like to be able to inject that code into any point when it tries to resolve a type:
-> Attempt to Resolve SampleController
-> Test Code Tells Us to use _V2 if possible.
-> Attempt to Resolve SampleType
-> Test Code tells us to use the _V1 if possible.
-> Resolves SampleType_V1
-> Attempt to Resolve AnotherType
-> No Test Defined, Use the Default
-> Resolves AnotherType
-> Resolves SampleController_V2 (passing SampleType_V1 as the dependency and then AnotherType as the other dependency)
Looking through some online articles, it sounds like I need to use some kind of Unity interceptor, but it's almo开发者_运维问答st like I'm writing my own IoC container at this point with some kind of testing architecture built in.
Hopefully someone has a good idea on how to do this before I go down to pain of finding the constructor and then resolving each type recursively.
EDIT: So It actually hasnt turned out to be that horrible to create my own injection by inspecting the constructor parameters of each dependency recursively, but I think the boss people might get a bit perturbed if I throw out Unity for my own custom solution.
I would try to swap out the Unity container for the duration of the request. Detect that it's your "V2" condition then make a new container but with the different set of types registered. Use that in the scope of that request. It wouldn't be a good idea to make a new container on every request in production but it should be a fine idea for testing.
Assuming you can write an ABEmailSenderFactory
with a Create
method...
In Autofac it would be as easy as
builder.RegisterType<ABEmailSenderFactory>().AsSelf();
builder.Register(c => c.Resolve<ABEmailSenderFactory>().Create())
.As<EmailSender>();
I don't know much about Unity, but it looks like it may not be as easy.
I think I'm just going to go down the road of making my custom solution. In effect, I was having to hack the current IoC I'm using to get the effect I wanted, which isnt much better than just making my own solution. I'm not even using the IoC for most of the functionality other than the simple resolution of dependencies, so I'm not sure I'll be missing using it. I ended up with the following method:
public virtual object Resolve(Type requestedType, Session session = null, RequestContext requestContext = null)
{
ConstructorInfo constructor = requestedType.GetConstructors().First();
object[] dependencies = null;
Type resultType = requestedType;
if (session == null || requestContext == null)
{
dependencies = constructor.GetParameters().Select(parameter => Resolve(parameter.ParameterType)).ToArray();
}
else
{
InitializeTestingArchitecture();
var testVersion = _testingProvider.GetTestVersionFor(requestedType, requestContext, session);
if(testVersion == null)
return Resolve(requestedType);
resultType = _testTypeLoader.LoadTypeForTestVersion(requestedType, testVersion);
constructor = resultType.GetConstructors().First();
dependencies = constructor.GetParameters().Select(parameter => Resolve(parameter.ParameterType, session, requestContext)).ToArray();
}
return Activator.CreateInstance(resultType, dependencies);
}
This way, I can control the exposure of the AB class instances via the database records.
There are a couple of options you can use here. I'm assuming Unity 2.0 here, this was harder in earlier versions.
You could pre-calculate the various permutations ahead of time and use resolver overrides in the resolve call:
container.Resolve(figureOutControllerType(),
new DependencyOverride<SampleType>(figureOutSampleType()));
This will replace the resolved object for SampleType wherever it appears in the tree, but it does require an instance directly.
You could use named registrations and do the NxM set of registrations for each combination of factors you're A/B testing against. Although that gets really messy after about 4 changes.
You could use an injectionfactory for things you're testing against - the factory could figure out which one to use:
container.RegisterType<SimpleType>(
new InjectionFactory((c) => container.Resolve(figureOutWhichType()));
This will do a runtime switch based on whatever the figureOutWhichType method does.
A third option would be a container extension that adds a type-mapping strategy to do the logic inside the resolve chain.
Of these options, I'd probably start with the factory approach, and move to the custom extension if things get way out of hand.
精彩评论