Search code examples
c#.netdependency-injectioncastle-windsor

Specify component name to resolve from type factory at runtime


Suppose I have an interface:

public interface IService
{
    void DoThing();
}

Now let's say I want to have a few implementations of this, so I can swap out which implmentation I want to use at runtime:

container.Register(Component.For<IService>().ImplementedBy<IServiceImplementationOne>().Named("First"));
container.Register(Component.For<IService>().ImplementedBy<IServiceImplementationTwo>().Named("Second"));
container.Register(Component.For<IService>().ImplementedBy<IServiceImplementationThree>().Named("Third"));

So I would be able to pick one by name:

container.Resolve<IService>("First"); //returns IServiceImplementationOne
container.Resolve<IService>("Second"); //returns IServiceImplementationTwo
container.Resolve<IService>("Third"); //returns IServiceImplementationThree

Now I'd like to be able to use the built in type facility to pick which one I want at runtime and not have more code sitting in my codebase if Windsor can do this automagically.

So let's say I have a factory service now:

public interface IServiceFactory
{
     IService Get(string name);
}

If I register that like so:

container.Register(Component.For<IServiceFactory>().AsFactory());

If I try calling Get("First") I get an exception saying it can't find a registration named ''.

According to the docs, anything after Get in your method name will be used as the name to resolve, so you can use GetXXX() in your factory interface to boil it down to container.Resolve<IService>("XXX"), and I was hoping that just using Get as the method name to let me specify that at runtime, but apparently that is not the case.

The docs also say you can use the word Create in your method names to be able to forward constructor arguments to the component created by the factory, but that's not what I want either.

Does the built in typed factory allow for this behaviour? I realise I can implement a custom ITypedFactoryComponentSelector but if I'm going that far then I may as well just implement my own concrete factory class anyway.


Solution

  • I realise I can implement a custom ITypedFactoryComponentSelector but if I'm going that far then I may as well just implement my own concrete factory class anyway.

    If you implement your own ITypedFactoryComponentSelector then you're creating logic which tells the factory which implementation to select. But once that decision is made, the selected implementation is still being resolved from the container. That's a huge benefit over just implementing your own concrete factory class. If the implementations are resolved from the container then they can have their own individual dependencies which is easy for the container to manage, but more work if the concrete factory has to know in detail how to construct those objects and their dependencies and so on.

    If you're trying to get a named implementation and the name is a string available at runtime then this is really easy.

    public class MyComponentSelector : DefaultTypedFactoryComponentSelector
    {
        protected override string GetComponentName(MethodInfo method, object[] arguments)
        {
            return arguments[0].ToString();
        }
    }
    

    It's just taking the string that you pass in and using it as the component name. Whatever name you pass in to your factory's Create method, that is the component name that is returned.

    This is even more useful if you pass in some other class or value and this selector does the work of figuring out a component name based on the input.

    Here's a more detailed example where the component name isn't selected directly from a string passed to the factory, but determined by an object passed in. That's a little bit more practical since our application code isn't likely to know the component names in our DI setup.

    Here's a selector. It takes an Address object and uses the country code from the address to select the component name for an AddressValidator implementation. It also specifies use of a fallback so that a "generic" address validator can be used if there is no match.

    public class AddressValidatorSelector : DefaultTypedFactoryComponentSelector
    {
        public AddressValidatorSelector() 
            : base(fallbackToResolveByTypeIfNameNotFound: true) { }
    
        protected override string GetComponentName(MethodInfo method, object[] arguments)
        {
            return "AddressValidatorFor_" + ((Address)arguments[0]).CountryCode;
        }
    }
    

    Then here's the actual component registration. It registers several specific implementations, a fallback, and the factory itself. When it registers the factory it specifies that it will use the AddresssValidatorSelector.

    public class WindsorInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.AddFacility<TypedFactoryFacility>();
            container.Register(
                Component.For<IAddressValidator,UnitedStatesAddressValidator>()
                    .Named("AddressValidatorFor_USA"),
                Component.For<IAddressValidator, FinlandAddressValidator>()
                    .Named("AddressValidatorFor_FIN"),
                Component.For<IAddressValidator, MalawiAddressValidator>()
                    .Named("AddressValidatorFor_MWI"),
                Component.For<IAddressValidator, CommonCountryAddressValidator>()
                    .Named("FallbackCountryAddressValidator")
                    .IsDefault()            
                );
            container.Register(
                Component.For<IAddressValidatorFactory>()
                    .AsFactory(new AddressValidatorSelector())
                );
        }
    }
    

    This also allows you to re-use the same component for different names. For example, suppose France and Germany both use the same EU-specific validator - you could just register the same implementation under both names.