An interesting topic, that I always love to discuss, is how to find a balance between building a pure domain model and being pragmatic and getting the job done. It just so happens, that getting the job done, and being able to add features to a system quickly, usually conflicts with my inner purist, who really wishes to keep my domain model and services oblivious of everything infrastructure-related.
Usually, I go for pragmatism. Not only because I’m lazy, but also because I think it’s funny to come up with solutions that accelerate development, and generally make the sun shine brighter.
Actually, this post should have been #2 in a series called “Polluting My Domain”, and this post should have been #1, because in #1 I showed how I usually use attributes to give hints to the automapper in Fluent NHibernate – e.g.
[Cascade] on a relation to configure NHibernate to cascade operations across that relation, or
[Indexed("ix__something")] to make that column be indexed in the database, or
[Encrypted] to make that particular property be backed by an encrypting
IUserType.
This post, however, will show a pragmatic, elegant and flexible way to make component registration easy in an IoC container.
Most IoC containers that I know of can be configured with XML and through some kind of more or less fluent API in code. I’ll spare you the XML, so I’ll just show a small example on Castle Windsor’s fluent API:
|
container.Register(Component.For<ISomeService>().ImplementedBy<SomeServiceImplementation>()) |
or you can perform multiple registrations like this:
|
container.Register(AllTypes.FromAssemblyContaining<SomeService>() .BasedOn<IService>() .WithService.FromInterface()) |
This is all fine and dandy, but I think the fluent API becomes pretty complicated when you throw customized registrations per customer and/or environment into the mix – mostly because it will become kind of obscure which services get registered where.
Therefore, one of the first things I have put in my recent projects, have been a registration routine based on attributes. Pretty simple, and yes, it does pollute my domain services with infrastructure-related stuff, but this is a great example where I prefer pragmatism and simplicity over purity.
My most recent project has two attributes,
ServiceAttribute and
RegisterInAttribute that look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class ServiceAttribute { readonly Type serviceType; public ServiceAttribute() { } public ServiceAttribute(Type serviceType) { if (serviceType == null) throw new ArgumentNullException("serviceType"); this.serviceType = serviceType; } public Type ServiceType { get { return serviceType; } } } public enum Environment { Development, Test, Staging, Production, } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class RegisterInAttribute { readonly Environment environment; public RegisterInAttribute(Environment environment) { this.environment = environment; } public Environment Environment { get { return environment; } } } |
and then the registration code looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public class ComponentRegistrar { public static void RegisterComponentsFromAssemblyOf<TSomeType>(IWindsorContainer container, Environment environment) { var components = typeof(TSomeType).Assembly.GetTypes() .Where(t => ShouldBeRegistered(t, environment)) .SelectMany(t => ToComponentRegistrations(t)); container.Register(components); } static IEnumerable<TAttribute> GetAttributes(ICustomAttributeProvider provider) { return provider.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>(); } static bool ShouldBeRegistered(ICustomAttributeProvider provider, Environment environment) { var attributes = GetAttributes<RegisterInAttribute>(); return !attributes.Any() || attributes.Any(a => a.Environment == environment) } static IEnumerable<IRegistration> ToComponentRegistrations(Type type) { var serviceType = a.ServiceType ?? type; return GetAttributes<ServiceAttribute>(type) .Select(a => Component.For(serviceType) .ImplementedBy(type) .Lifestyle.Transient); } } |
So, having established the current value of
environment, my component registration will look like this:
|
var environment = DetermineEnvironmentFromAppSettingsOrSomethingLikeThat(); var container = new WindsorContainer(); ComponentRegistrar.RegisterComponentsFromAssemblyOf<HomeController>(container, environment); ComponentRegistrar.RegisterComponentsFromAssemblyOf<SomeDomainService>(container, environment); ComponentRegistrar.RegisterComponentsFromAssemblyOf<SomeInfrastructureService>(container, environment); // yay! |
– and that’s it! But the best of it is that adding services to the system now becomes a breeze – check this out – registering a concrete type, offering itself as a service:
|
[Service] public class HomeController : Controller { // (....) } |
– and here’s registering different stuff depending on the environment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
[Service] [RegisterIn(Environment.Development)] public class DebugController : Controller { // (....) } [Service(typeof(IMailSender))] [RegisterIn(Environment.Production)] public class SmtpMailSender : IMailSender { // (...) } [Service(typeof(IMailSender))] [RegisterIn(Environment.Staging)] public class FakeSmtpMailSender : IMailSender { // (...) } [Service(typeof(IMailSender))] [RegisterIn(Environment.Development)] [RegisterIn(Environment.Test)] public class LoggingMailSender : IMailSender { // (...) } |
This way of registering component has proven to me several times to be a simple and nifty way of managing the differences between environments, and even differences between customers (which would require a few extensions to the example above though), still being able to add services to the system quickly.
Another benefit is that it’s pretty clear what happens, even to developers who might not be that experienced in using IoC containers. If I were the only developer on a project, I would probably prefer component registration based on conventions, but when you have a team, you sometimes need to make some things more explicit.