Auto-registration in ASP.NET MVC
Coming off a week at DevTeach. Others have commented on how great it is so I'll just use my own observation as a lead-in for the real meat. DevTeach gives me a renewed interest in learning. After seeing presentations on topics I've kinda sorta grokked/solubled, it's nice to gain further insight into them. So I come away with a laundry list of patterns, techniques, tools, and frameworks to look into.
And one of those techniques is auto-registration of components in an IoC container. Ayende has talked about this quite a bit with Binsor and I did get to see it in action. But recently, there have been enhancements to Windsor itself to allow for this in a relatively clean(er) way. Hammett has an example here as does Ken Egozi (who took it directly from the Castle dev list). Note that the syntax is slightly different 'twixt the two (AllTypes<T>.Of vs. AllTypesOf<T>). I just got the latest from the trunk and it appears to be the former syntax at the moment.
A concrete example. Here is the former code in my ASP.NET MVC application (modified, as usual, from the CodeCampServer sample app):
foreach ( var type in Assembly.GetExecutingAssembly( ).GetTypes( ) ) { if ( typeof (IController).IsAssignableFrom( type ) ) { _container.AddComponentWithLifestyle( type.Name.ToLower( ), type, LifestyleType.Transient ); } }
And here is the new version:
_container.Register( AllTypes.Of( ) .FromAssembly( Assembly.GetExecutingAssembly( ) ) .Configure( c => c.LifeStyle.Transient.Named( c.Implementation.Name.ToLower( ) ) ) );
Note the call to Configure. In most cases, this won't be necessary. But with MvcContrib, it seems to retrieve controllers from the container based on the controller name in lower case. So we need to register them with that same name.
Also note that this is based on a version of Windsor from the trunk (I think). Which is why I didn't update the CodeCampServer project myself (though there are already comments in there to do exactly that).
Here's another comparison. In this case, it's a more "traditional" registration in that we loop through all the classes in an assembly and register it with the container based on its first interface. The before
var presentationTypes = Assembly.Load( "Trilogy.Gunton.Presentation.Services" ) .GetTypes( ) .Where( t => t.IsClass == true && t.IsAbstract == false && t.GetInterfaces( ).Length > 0 );
foreach ( var type in presentationTypes ) { _container.AddComponentLifeStyle( type.Name.ToLower( ), type.GetInterfaces( )[0], type, LifestyleType.Transient ); }
And after:
_container.Register( AllTypes.Pick( ) .FromAssemblyNamed( "Trilogy.Gunton.Presentation.Services" ) .WithService.FirstInterface( ) );
I do have classes in this assembly that are concrete but it appears the FirstInterface call will ignore them. Not so much when I do it more manually.
The last example is more interesting. In this case, I have a bunch of repositories, each of which derive from a RepositoryBase
For example, JobRepository would derive from RepositoryBase<Job> as well as implement IJobRepository. RepositoryBase<T> implements IRepository<T> while IJobRepository implements IRepository<Job>. A lot of the time, the specific interfaces (like IJobRepository, IOfficeRepository, etc.) will be empty but I've been burned before so I usually create them so I can add functionality later if I need it. Plus it makes things clearer when I'm passing them around.
For the purpose of container registration, this presents a slight problem because the repositories now implement two interfaces. For example, IJobRepository implements both IJobRepository and IRepository<Job>. But I want it registered against IJobRepository in the container.
Here's the previous code I had to do this, which I will admit could use some tuning:
var repositoryTypes = Assembly.Load( "Trilogy.Gunton.DataAccess" ) .GetTypes( ) .Where( t => t.IsClass && t.GetInterfaces( ).Length > 0 ); foreach ( var type in repositoryTypes ) { var types = type.GetInterfaces( ).Where( t => t.IsGenericType == false && t.Namespace.StartsWith( "Trilogy.Gunton" ) ); if ( types.Count( ) > 0 ) { _container.AddComponentLifeStyle( type.Name.ToLower( ), types.ElementAt( 0 ), type, LifestyleType.Transient ); } }
The corresponding code isn't quite as terse:
_container.Register( AllTypes.Pick( ) .FromAssemblyNamed( "Trilogy.Gunton.DataAccess" ) .WithService.Select( delegate( Type type ) { var interfaces = type.GetInterfaces( ) .Where( t => t.IsGenericType == false && t.Namespace.StartsWith( "Trilogy.Gunton" ) ); if ( interfaces.Count( ) > 0 ) { return interfaces.ElementAt( 0 ); } return null; } ) );
But we can extract the delegate into an extension method that we can use similar to the FirstInterface method:
public static class CastleExtensions { public static TypesDescriptor FirstNonGenericTrilogyInterface( this ServiceDescriptor descriptor ) { return descriptor.Select( delegate( Type type ) { var interfaces = type.GetInterfaces( ) .Where( t => t.IsGenericType == false && t.Namespace.StartsWith( "Trilogy.Gunton" ) ); if ( interfaces.Count() > 0 ) { return interfaces.ElementAt( 0 ); } return null; } ); } }
And now our new code to register repositories is:
_container.Register( AllTypes.Pick( ) .FromAssemblyNamed( "Trilogy.Gunton.DataAccess" ) .WithService.FirstNonGenericTrilogyInterface( ) );
The condition to check that the namespace is mine is because I have a UnitOfWork class in there that implements IDisposable as well as IUnitOfWork. So I want that one in the container only for the IUnitOfWork interface.
Putting it all together, here is the complete code to register my repositories, services, and controllers in the Windsor container:
_container.Register( AllTypes.Pick( ) .FromAssemblyNamed( "Trilogy.Gunton.DataAccess" ) .WithService.FirstNonGenericTrilogyInterface( ) ); _container.Register( AllTypes.Pick( ) .FromAssemblyNamed( "Trilogy.Gunton.Presentation.Services" ) .WithService.FirstInterface( ) ); _container.Register( AllTypes.Of( ) .FromAssembly( Assembly.GetExecutingAssembly( ) ) .Configure( c => c.LifeStyle.Transient.Named( c.Implementation.Name.ToLower( ) ) ) );
Phew, so much for being billable today...
Kyle the Registered