Sunday, December 7, 2008

Castle Windsor factory method support

After using ninject for a short project (some modifications to a Subtext blog) I came back to Windsor and found myself missing ToMethod(). ToMethod() in ninject gives you factory method support, that is, you pass it a Func and the container will call it whenever it needs to instantiate the component.

Now Windsor has had factory support for years, but it requires the factory to be a class, registered as a component in the container. This is ok as it's the most flexible approach, but sometimes all you need is a factory method, and creating a class and then registering it seems a bit overkill. It would be nicer to write something like:

Container.Register(Component.For<HttpServerUtilityBase>()
   .FactoryMethod(() => new HttpServerUtilityWrapper(HttpContext.Current.Server))
   .LifeStyle.Is(LifestyleType.Transient));

With this little extension method, you can do just that:

    public static class ComponentRegistrationExtensions {
        public static IKernel Kernel { private get; set; }

        public static ComponentRegistration<T> FactoryMethod<T, S>(this ComponentRegistration<T> reg, Func<S> factory) where S: T {
            var factoryName = typeof(GenericFactory<S>).FullName;
            Kernel.Register(Component.For<GenericFactory<S>>().Named(factoryName).Instance(new GenericFactory<S>(factory)));
            reg.Configuration(Attrib.ForName("factoryId").Eq(factoryName), Attrib.ForName("factoryCreate").Eq("Create"));
            return reg;
        }

        private class GenericFactory<T> {
            private readonly Func<T> factoryMethod;

            public GenericFactory(Func<T> factoryMethod) {
                this.factoryMethod = factoryMethod;
            }

            public T Create() {
                return factoryMethod();
            }
        }
    }

 

Of course, this needs the FactorySupportFacility to be installed:

Container.AddFacility("factory.support", new FactorySupportFacility());

And since this isn't really a part of the Kernel, it needs a reference to the container's kernel:

ComponentRegistrationExtensions.Kernel = Container.Kernel;

UPDATE 5/7/2009: I submitted a patch which Ayende applied in revision 5650 so this is now part of the Windsor trunk, only instead of FactoryMethod() it's called UsingFactoryMethod()

UPDATE 7/30/2010: as of Windsor 2.5, UsingFactoryMethod() no longer requires the FactorySupportFacility.

11 comments:

George Mauer said...

Let's up the stakes.
I'll post on here if I'm able to come up with it.
I would like to be able to do something like this:

Container.Register(Component.For<IHttpServerUtility>()
.FactoryMethod(IWindsorContainer container => container.Resolve<HttpServerUtilityFactory>().Create());

mausch said...

It's pretty much the same, here are the changes.

George Mauer said...

Close, but not quite there, notice that in my example I'm registering an interface not a concrete class (The method returns an interface too - therefore allowing for an abstract factory).

I tried my hand at this and there seems to be a bug/some poor design in castle though I fully admit the possibility of a lack of knowledge on my part.

Started a post on this issue on the castle developer list here

mausch said...

HttpServerUtilityBase is not a concrete class, it's an abstract class. The ASP.NET MVC team decided it would be best to abstract this to abstract classes instead of interfaces, see here for the rationales behind this. You could just replace HttpServerUtilityBase with your IHttpServerUtility, it's the same.

George Mauer said...

Good point. I am not all that familiar with ASP.NET MVC quite yet and I didn't realize that we were talking about it. Let's continue this over on the thread on the castle list

Steven M Hoff said...

Windsor already has factory method support. Simply register the object twice, once for the factory method and once to create it.
<component id="KnownDocumentFactory" type="Engine.Services.KnownDocumentTypeService, Engine" />
<component id="IKnownDocumentTypeService"
service="Engine.Interfaces.IKnownDocumentTypeService, Engine"
type="Engine.Services.KnownDocumentTypeService, Engine"
factoryId="KnownDocumentFactory"
factoryCreate="Create" lifestyle="transient"/>

mausch said...

@Steven: it's not quite the same:

1. You use xml registration while I use fluent registration. Not that it's bad, but xml is way more verbose.
2. You assume that the component can manufacture (?) itself, that is, the Create method is on the component itself, which isn't good practice IMHO. After all, one of the purpose of factories is to delegate the component creation to someone else.
3. You can't use a lambda method as factory in a xml registration... maybe I should have called this "lambda factory support" to avoid confusions with the "factory method pattern" as defined by Fowler. Hope the difference is clear now.

Fabio Maulo said...

There is a little bug
typeof(GenericFactory<S>).FullName

You may have same Factory class, same factory method but called with different parameters values.

This implementation is ignoring the custom component Name.
container.Register(Component.For<ISessionFactory>().UsingFactoryMethod(() => M1SessionFactory()).Named("M1CustomName"));
container.Register(Component.For<ISessionFactory>().UsingFactoryMethod(() => M2SessionFactory()).Named("M2CustomName"));

The code end in a duplication of key.

mausch said...

@fabio: ok, please file an issue on the bugtracker

mausch said...

Forgot to say that months ago I fixed the bug Fabio mentioned, the fix will be included in Windsor 2.1

Jesica Alba said...
This comment has been removed by a blog administrator.