The Siege Project: Siege.ServiceLocation, Part 3 – Extending the container with custom use cases

The Siege Project

 

Adding new functionality to the container without having to change Siege.ServiceLocation

When I started this project, I had a few main goals in mind. I wanted to be able to register things with a straightforward and simple syntax. I wanted to be able to use any IoC framework to do the heavy lifting of type resolution for me. I wanted to be able to resolve multiple different types straight out of the container when state changed. And finally, I wanted to be able to extend it without having to constantly update the framework and put out a new release.

This post shows how I do it.

I’m going to throw a lot at you guys in short order. I’ll do my best to keep it simple and make it understandable. While simplicity has been my goal overall for this whole library, this is going to be the most involved aspect of the system. It’s not bad, I promise. The interfaces are small and the amount of code you have to implement is minimal. But there’s a few types that come into play. I’ll take the time to explain each one as we go and try to draw a simplified picture of how they work together. If anything is confusing or not clear, please reply in the comments and I’ll do my best to explain. If it’s a big topic, I’ll put up another post to go over that piece.

Let’s hit the code.

How use cases work

As I’ve mentioned in previous posts, use cases are specific implementations of the IUseCase interface. Let’s take a look at the interface.

public interface IUseCase

{

    Type GetBoundType();

    Type GetUseCaseBindingType();

    object Resolve(IInstanceResolver locator, IList<object> context);

    object Resolve(IInstanceResolver locator);

    bool IsValid(IList<object> context);

}

As you can see, there are a handful of methods that are required by this interface. Let’s go through them real quick.

  1. GetBoundType() – this function is used by the SiegeContainer to determine the type that this particular instance of your use case is using. For example, when you do a registration like Give<IFoo>.Then<Foo>(), you get back an instance of DefaultUseCase<Foo>. When you call GetBoundType(), you will get the Foo type.
  2. Resolve() – This method allows you to control exactly how items are resolved.
  3. IsValid() – This method returns true if a use case is applicable according to context – false if it is not.
  4. GetUseCaseBinding() – This returns a type (assumed to be an implementation of IUseCaseBinding) which is used to register use cases.

IInstanceResolver is one of the interfaces that IServiceLocator and IServiceLocatorAdapter inherit from.  As the name implies, it resolves instances.

public interface IInstanceResolver

{

    object GetInstance(Type type, string key);

    object GetInstance(Type type);

}

In most cases you can inherit from the abstract type GenericUseCase<T>, which only forces you to implement the method GetUseCaseBinding() and manages all the other methods for you (you can of course override any you feel are necessary). All use case bindings are implementations of IUseCaseBinding, which looks like this:

public interface IUseCaseBinding

{

    void Bind(IUseCase useCase);   

}

Packaged with Siege.ServiceLocation are three use case binding types: IDefaultUseCaseBinding, IConditionalUseCaseBinding and IKeyBasedUseCaseBinding (used for registering and resolving by a particular string key). These binding types are in turn implemented by each of the adapters so that the use cases can be supported by the underlying IoC provider.

It is important to note that if you create a new type of IUseCaseBinding you will need to write concrete implementations for whatever adapter you intend to use. Custom implementations of IUseCase that use one of the bundled use case bindings do not need to do this. Only create new IUseCaseBinding types if you have specialized instructions for how you want to register and/or resolve with a specific IoC provider.

Custom types of bindings integrate easily with the container to provide seamless support for your new use case and binding. To do this, simple use the provided method on the SiegeContainer before you do your registrations:

IServiceLocator AddBinding(Type baseBinding, Type targetBinding);

Let’s take a look at a specific example.

Putting it all together

For this example, I am using Sean Chamber’s implementation of the decorator pattern as a reference point. This example will show you how to extend the container to conditionally decorate types based on runtime conditions. Bear in mind that this example is highly contrived and exists solely for the purpose of demonstration. I will be using the Ninject framework as a support for this post. All source code (including examples for all three containers) is included in the unit tests project in the source code, which is found here.

First, let’s look at the syntax to register types with decorators.

locator

    .AddBinding(typeof(IDecoratorUseCaseBinding<>), typeof(DecoratorUseCaseBinding<>))

    .Register(Given<ICoffee>.Then<Coffee>())

    .Register(Given<ICoffee>

                  .When<Ingredients>(ingredients => ingredients == Ingredients.WhippedCream)

                  .DecorateWith<WhippedCreamDecorator>())

    .Register(Given<ICoffee>

                  .When<Ingredients>(ingredients => ingredients == Ingredients.Espresso)

                  .DecorateWith<EspressoShotDecorator>());

Right off the bat you should see that I’m adding a custom binding, letting the framework know that when use cases specify IDecoratorUseCaseBinding<T> as their binding type to use DecoratorUseCaseBinding<T> as the implementation. This is one of the key parts of extending the container to support your custom registration. 

public class DecoratorUseCaseBinding<TService> : IDecoratorUseCaseBinding<TService>

{

    private IKernel kernel;

    private IServiceLocatorAdapter locator;

 

    public DecoratorUseCaseBinding(IKernel kernel, IServiceLocatorAdapter locator)

    {

        this.kernel = kernel;

        this.locator = locator;

    }

 

    public void Bind(IUseCase useCase)

    {

        Bind((IDecoratorUseCase<TService>)useCase);

    }

 

    public object Resolve(Type typeToResolve, Type argumentType, object rootObject)

    {

        string parameterName = typeToResolve.GetConstructor(new[] {argumentType}).GetParameters().Where(parameter => parameter.ParameterType == argumentType).First().Name;

 

        return kernel.Get(typeToResolve, new ConstructorArgument(parameterName, rootObject));

    }

 

    private void Bind(IDecoratorUseCase<TService> useCase)

    {

        kernel.Bind(useCase.GetBoundType()).ToSelf();

    }

}

As you can see, DecoratorUseCaseBinding includes instructions to the container on how to register your use case with Ninject. You’ll also notice that I included a function called Resolve() on this class. This isn’t required by the interface, but I decided to keep these instructions here for the sake of simplicity in this example. Because this scenario requires objects to be specially constructed, I ask Ninject to resolve a type and give it a type argument and value to use during construction when it resolves the requested type.

The next thing you should notice about our registration is the presence of the DecorateWith<TDecorator> function. This is not included with the Siege.ServiceLocation project. I have made it up for this example. It looks like this:

public static class RegistrationExtensions

{

    public static IContextualServiceLocator serviceLocator;

    public static void Initialize(IContextualServiceLocator locator)

    {

        serviceLocator = locator;

    }

 

    public static IDecoratorUseCase<TDecorator> DecorateWith<TDecorator>(this IConditionalActivationRule rule)

    {

        var decoratorUseCase = new DecoratorUseCase<TDecorator>(serviceLocator);

 

        decoratorUseCase.BindTo(rule.GetBoundType());

        decoratorUseCase.SetActivationRule(rule);

        

        return decoratorUseCase;

    }

}

I decided to extend the existing registration rather than create a new class that builds instances of DecoratorUseCase. While there is nothing forcing any consumer to do this, I decided to do it so that I could leverage the existing functionality that creates rules to associate with the use case, and to show how you can bolt things on to the existing infrastructure.

Finally, let’s take a look at DecoratorUseCase. This class inherits from GenericUseCase<T> and implements the IDecoratorUseCase<T> interface. It also overrides some functionality in GenericUseCase, which I will explain.

public class DecoratorUseCase<TService> : GenericUseCase<TService>, IDecoratorUseCase<TService>

{

    private IContextualServiceLocator serviceLocator;

 

    public DecoratorUseCase(IContextualServiceLocator serviceLocator)

    {

        this.serviceLocator = serviceLocator;

    }

 

    public override Type GetUseCaseBindingType()

    {

        return typeof (IDecoratorUseCaseBinding<>);

    }

 

    protected override IActivationStrategy GetActivationStrategy()

    {

        return new DecoratorActivationStrategy(serviceLocator, GetBoundType());

    }

 

    protected class DecoratorActivationStrategy : IActivationStrategy

    {

        private readonly IContextualServiceLocator serviceLocator;

        private readonly Type decoratedType;

 

        public DecoratorActivationStrategy(IContextualServiceLocator serviceLocator, Type decoratedType)

        {

            this.serviceLocator = serviceLocator;

            this.decoratedType = decoratedType;

        }

 

        public object Resolve(IInstanceResolver locator, IList<object> context)

        {

            var useCaseBinding = (IDecoratorUseCaseBinding)locator.GetInstance(typeof(IDecoratorUseCaseBinding<TService>));

            var decorators = new List<Type>();

            var rootObject = locator.GetInstance(decoratedType);

 

            foreach (IDecoratorUseCase useCase in serviceLocator.GetRegisteredUseCasesForType(decoratedType))

            {

                if(useCase.IsValid(context)) decorators.Add(useCase.GetDecoratorType());

            }

 

            return GetInstance(useCaseBinding, rootObject, decoratedType, decorators);

        }

 

        private object GetInstance(IDecoratorUseCaseBinding binding, object rootObject, Type encapsulatedType, IList<Type> decorators)

        {

            if (decorators.Count == 0) return rootObject;

 

            Type decorator = decorators[0];

            IList<Type> prunedDecoratorList = new List<Type>(decorators);

            prunedDecoratorList.RemoveAt(0);

 

            object resolvedObject = binding.Resolve(decorator, encapsulatedType, rootObject);

 

            return GetInstance(binding, resolvedObject, encapsulatedType, prunedDecoratorList);

        }

    }

 

    public Type GetDecoratorType()

    {

        return typeof (TService);

    }

}

As noted earlier, by using GenericUseCase we are only required to implement the GetUseCaseBindingType() function, which returns the IDecoratorUseCase<> type as its value. This is used by SiegeContainer during registration to find the type specified in the AddBinding statement and use it to direct the underlying IoC how to register your custom implementation of IUseCase.

The GetActivationStrategy() function is overriden in this class to return a new type of IActivationStrategy. By default, GenericUseCase<T> uses a GenericActivationStrategy which simply calls GetInstance() on the IServiceLocatorAdapter. By overriding this and providing a new implementation, we are able to give the SiegeContainer high-level instructions on how to construct our object for this use case. The strategy checks all applicable use cases for the supplied context and gathers the appropriate decorator types, then recursively calls itself to build the object.

This function calls back into the binding to do the ninject-specific type resolution, which specially constructs the object for me. When it’s all said and done, I have a fully decorated object in line with the context that has been added to my IContextStore. If I specify whipped cream, I get an instance of Coffee decorated with a WhippedCreamDecorator. If I specify espresso, I get an instance of Coffee decorated with an EspressoDecorator. If I specify both, I get an instance of Coffee decorated with both.

By default SiegeContainer resolves types based on the first satisfied rule it encounters in the context, or it uses a default case. This example significantly changes how the container works by providing a mechanism for it to use ALL applicable context instead of only the first applicable context.

 

So that’s how it works

Extending the library is probably the most complex usages of the this project, but I hope that it is not prohibitively so. It’s also a feature that I expect will not be used as much as everything else that the container does. I’m not sure how useful the community will find this; while I have been able to recognize areas this might apply at my company, the scope of where it would be useful is fairly narrow. Still, I think it is a nice feature and a good way to show how to extend a system without having to modify its internals.

In the comments of the last post, a responder mentioned wanting to be able to use factory methods to construct objects depending on certain conditions being met. I believe that this feature enables that a developer to extend this project with that very functionality without having to go and update the project itself.

I would like very much to hear feedback on what you guys think of this. Is it too hard? Is it too confusing? Is it useful?

Until next time, Happy Coding!

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

This entry was posted in IoC, Siege. Bookmark the permalink. Follow any comments here with the RSS feed for this post.