Containers are dumb – what if they were smart?
One of the things I really wanted to do when I set out to create the ServiceLocator in Siege.Requisitions was to create a container that could map an interface to multiple types, and then at runtime, automatically figure out which one of those types I really wanted. To accomplish this, I put at it’s core a small rule engine to allow users to create rules that would help the container understand how to pick implementations. It works great — but there’s one problem. You have to specifically tell it what you want. That’s hardly much better than resolving the type directly; you have to program intent directly into your application, and you carry around references to IServiceLocator.
Very ugly.
I keep coming back to the original premise … I want the container to figure it out for me. I want to explain to the container what I want it to do, and under which conditions. Then I want it to figure it out for itself at runtime by understanding the state of the application, the user session, whatever I have explained to it. I want a smart container. But the problem is, most containers are dumb. You map an interface to a type — thats it. You want multiple types? Register them with names to distinguish them. Register different “profiles” to instruct the container to run in certain modes. Create child containers.
I just didn’t feel like those solutions worked well for me. I want a smart container that can figure it all out. I don’t want to spend time building families of containers and wiring it all together and telling them how to interact. I don’t want to have to make my application look up values by a name, or set my container into a certain “profile mode”. I want to focus on my application. I want to explain my scenario to the container and move on and not think about my container anymore.
A practical example
As I mentioned in my previous post, we encounter this scenario in our every day development at my company. Where I work, we integrate with a lot of partners. There are a lot of commonalities between these vendors and we have abstracted them out dutifully into interfaces with multiple implementations. Basic abstraction and polymorphism. We routinely have scenarios where user input/selections on a UI translates into a vendor call. Ultimately, this means resolving a specific implementation of an interface to send or request data from that vendor.
We have our registrations set up with rules like so:
serviceLocator.Register<Singleton>(Given<IExampleService>.When<SelectionType>(selection => selection == SelectionType.OptionA).Then<OptionAService>();
serviceLocator.Register<Singleton>(Given<IExampleService>.When<SelectionType>(selection => selection == SelectionType.OptionB).Then<OptionBService>();
For the sake of this example, SelectionType is an enum representing a vendor we integrate with. Implementations of IExampleService contain code that we use to integrate with those vendors. The registration API allows you to specify the lifestyle with the method call (in this case, singleton). And with those registrations in place, you’d see controller code like this:
public class SampleController : Controller
{
private IServiceLocator serviceLocator;
public SampleController(IServiceLocator locator)
{
this.serviceLocator = locator;
}
public ViewResult Foo(SelectionType selectionType)
{
IExampleService service = locator.GetInstance<IExampleService>(new ContextArgument(selectionType));
//invoke methods on service
}
}
Like I said, ugly stuff. In this example, we get a reference to the service locator, pass in some context to help the container identify which rule applies so it knows which type to create. I don’t want to carry around that reference to IServiceLocator, and I don’t want my controllers having to understand how to help the container to select the rule that applies based on user input.
Like I said, I want a smart container. Turns out the solution was pretty easy to implement, and didn’t even require me to change how Siege works!
Introducing Awareness.Of<T>
Siege simplifies thing for consumers by exposing only one method to do registrations — I call it Register (novel, I know). Register takes in instances of IRegistration, or Action<IServiceLocator>. There are several different syntaxes available to help you do registrations … Given<T>, which helps you register individual types … Using.Convention<TConvention> which allows you bundle registrations together for reuse, and now … Awareness.Of<T>, which tells the container how to figure out the things it needs to evaluate its rules on its own.
This abstraction allows consumers to extend the Service Locator easily — by providing your own implementation of IRegistration, the container understands how to handle your new and unique registration type. To prove this premise, I add all new functionality to Siege.Requisitions.Extensions without extending the core framework. It is completely pluggable. But, that’s a tangent, and I’ll cover it more in some other post.
With Awareness, we have a smart container, which can identify the rules associated with a requested type and use its own awareness to determine validity of those rules.
How does it work?
Awareness.Of<T> works like this: When you resolve a type, the container looks up all rules associated with that type. In our example, it receives rules based on SelectionType. If the container is “Aware” of SelectionType, it will automatically use that awareness to find the current SelectionType value and then give it to the rule engine to evaluate. It continues this process until either a rule is satisfied or no rules match.
Awareness.Of<T> takes a delegate as an argument. This delegate is a Func<T> which tells the container how to get an instance of T to use in rule evaluation. For our MVC applications, we have used Awareness.Of<T> to make our container aware of the Model Binding mechanism in ASP.NET MVC, which it uses to pull data into an object like model binding for controller actions done (form data, query string data, etc), to make our container aware of any configuration files, to make it aware of the session, to make it aware of a wide variety of things. The container becomes more intelligent the more aware it becomes; Your other processes cease to depend on the container at all and cease to have to explicitly direct the container to make decisions.
It all happens naturally, and intuitively.
Making your container contextually aware
To make your container aware is very easy. To follow with our example, I will show you how we’ve made our container aware of the model binding system in ASP.NET MVC. This registration is from code in the Siege.Requisitions.Web.dll file. It’s very simple, just provide a registration like this:
serviceLocator.Register(Awareness.Of(ModelBinding.For<SelectionType>().UsingDefaultBinder());
This registration instructs the service locator to use the default model binder to pull create a SelectionType object. There are also methods on ModelBinding.For<T> which allow you to specify a custom binder (if, for example, you need to look an object up by ID before evaluating it with a rule).
Our example, revisited with Awareness.
So here’s what the whole thing looks like with awareness. I will also use controller action injection, as I covered in my last post.
//from global.asax.cs, or wherever you initialize the container
serviceLocator.Register<Singleton>(Given<IExampleService>.When<SelectionType>(selection => selection == SelectionType.OptionA).Then<OptionAService>();
serviceLocator.Register<Singleton>(Given<IExampleService>.When<SelectionType>(selection => selection == SelectionType.OptionB).Then<OptionBService>();
serviceLocator.Register(Awareness.Of(ModelBinding.For<SelectionType>().UsingDefaultBinder());
public class SampleController : Controller
{
public SampleController()
{
}
public ViewResult Foo(IExampleService service)
{
//invoke methods on service
}
}
Looks much cleaner, don’t you think? All properly decoupled and automatically done for you. The best part is — it works with whatever container you’re already using. One of the other priorities I had when I started Siege was to be a value-added proposition for existing containers. If you use Windsor, StructureMap, Unity, whatever… Siege integrates seamlessly with them and makes them smarter. It adds additional abilities to what those containers can already do.
Happy Coding!
Post Footer automatically generated by Add Post Footer Plugin for wordpress.
