And over the years, my use of this tool has rarely seen drastic change. The pattern from that old team, which used the Common Service Locator (of which I use a smaller home-grown version), worked very well and has fit the bill for almost all of my control inverting needs. From time to time, however, I would come up with some new need and have to create something new to handle it.
To date, my biggest change was the use of a custom convention scanner. Nothing fancy, it just scanned assemblies and used my own custom naming convention for interfaces (I dropped the "I" and never looked back) and matched implementations to interfaces. The scanner itself looked like this:
public class DomainInterfaceNamingConvention : IRegistrationConvention { public void Process(Type type, Registry registry) { if (IsntRegisterable(type)) return; Type interfaceType = type.GetInterface(type.Name.Replace("Implementation", string.Empty)); registry.AddType(interfaceType, type); } private bool IsntRegisterable(Type type) { return type.IsAbstract || !type.IsClass || !ImplementsACustomInterface(type); } private bool ImplementsACustomInterface(Type type) { foreach (var iface in type.GetInterfaces()) if (iface.Namespace.Contains("Acme")) return true; return false; } }
Simple enough. If the type is one of the types I want to register, it gets the interface that it's implementing based on the word "Implementation" at the end of that interface (my own convention, favored over configuration) and adds it to the registry. This has worked splendidly for years. Until I thought of something I wanted to support but couldn't with this.
This convention assumes something I'd always been assuming and had never bothered me. Namely that all instances are default instances. And in all fairness, I've never had a need for named instances. I was able to swap out different implementations by changing some custom configuration settings. The bootstrapper which references this convention scanner would dynamically build the assemblies based on those configuration settings.
So, let's say I have three implementations of a set of data repositories. Using a naming convention (favored over configuration again, and borrowed in large part from that same job long ago), the assemblies would be named something like this:
This convention assumes something I'd always been assuming and had never bothered me. Namely that all instances are default instances. And in all fairness, I've never had a need for named instances. I was able to swap out different implementations by changing some custom configuration settings. The bootstrapper which references this convention scanner would dynamically build the assemblies based on those configuration settings.
So, let's say I have three implementations of a set of data repositories. Using a naming convention (favored over configuration again, and borrowed in large part from that same job long ago), the assemblies would be named something like this:
- Acme.Infrastructure.DAL.SQLExpress
- Acme.Infrastructure.DAL.XMLFiles
- Acme.Infrastructure.DAL.Mock
These would be three valid implementations of my repositories, all transparent to the rest of the domain, and which one any given application instance uses would be a config setting.
But recently I found a situation where I might want the same application instance to use multiple implementations for the same dependency. Without going into too many details, let's say for the sake of argument that the application needs to move data from one place to another. If I can only use one implementation of a given dependency, then one of those two "places" would have to be a completely different dependency.
This led me down a distasteful path. Things which logically should just be repository implementations (because they're just persisting data) ended up being their own isolated dependencies, filled with DTOs that were littering my models.
The first example of this on a project was when I had to integrate some different calendar systems into our event data. We have a database for storing event data, and that's essentially the system of record for that data. It has repository implementations accordingly. However, the business wanted to manage events using a third party tool (in this case some crappy-but-functional desktop calendar application), and additionally wanted to publish events to a third party tool (in this case, Google Calendar).
Well, the DAL dependency was already taken up by the event data repositories. So I introduced a new dependency called CalendarManager and another called CalendarPublisher. I wrote implementations for them using these two third party systems, as well as mock implementations for testing. And essentially the process would be to read from the CalendarManager, persist to the Repositories, perform some domain logic, read from the Repositories, persist to the CalendarPublisher.
It worked, but it was distasteful. I already have models and repository structures for Events. These other dependencies should just be alternate implementations of the repositories for those models. But then one application instance wouldn't be able to use all three.
What I needed were named instances. But whenever I've seen named instances used in the past, they were on a class level instead of an assembly level. I don't want to have to specify every individual class in my bootstrapping code. For starters, that would favor configuration over convention which I don't want to do. But more importantly it would mean that any implementation or any interface that's added to the domain would have to be manually specified there. Unintuitive at best, error prone at worst. I'd much rather just have to specify the assemblies (of which there are several) instead of the classes (of which there are hundreds).
So how can I name my instances at the assembly level? How can I scan my assemblies and add them into the object graph from which I could essentially pull named instances like this?:
var eventSource = IoCFactory.GetInstance<EventRepository>("CalendarPlanner"); var eventDestination = IoCFactory.GetInstance<EventRepository>("GoogleCalendar"); var eventData = IoCFactory.GetInstance<EventRepository>();
Basically I'm looking to be able to specify an instance, or take a default. (In this case the default for the repositories would be the database implementation.)
After some back and forth on Stack Overflow and a lot of tinkering, I've ended up with this:
Still favoring convention over configuration, I've continued with the assumption of my assembly names. Given that, it takes a minimal amount of reflection to get the name of the assembly for the implementation and use that convention to name the instances.
It was then irresistible to take it a step further and define some more custom configuration for these overrides. After all, one of the biggest reasons I have this setup is for testing. I like to create custom mock implementations for dependencies and then my testing instance (which has its own config file) can simply specify to use the mock implementations instead of the default ones. This is especially useful for automated integration testing because I can keep multiple config files for the tests and just have the build scripts deploy and run multiple instances of the test code with different config files. Thus isolating individual dependency implementations for testing while using mocks for everything else. This greatly reduces the number of variables in automated testing for me.
So now in that custom configuration section I can override defaults as well as override named instances. Thus, even if my code calls for this:
I might decide to override that for a specific application, essentially telling it that "even if I ask you for this specific instance, give me this other one (such as the Mock) instead." Thus far this has prevented me from too tightly coupling the code asking for the implementations with the implementations themselves. The application isn't tightly bound to the actual implementation, it can override it. So as long as I keep my names fairly general, I'm happy with the level of coupling.
Not shown here is the implementation of ConfigurationFactory, which is local to my IoC implementation project. But basically all it does is check the config file (using a standard .NET custom config section implementation) for any specified defaults or overrides. So, for example, by default the non-named instance for the repositories might be the SQLExpress implementation. In the config file I might then say to use the XML one as the default instead, so non-named instances become the XML one for that application. I may then also take it a step further and configure it to use the XML one even when the SQLExpress one is explicitly requested. (Which doesn't happen in the codebase at this time, but it does for other implementations such as the aforementioned Event stuff.)
All in all, I'm pretty happy with this implementation. And I'll be a lot happier once I go back through the code and re-implement these custom dependencies as repositories which they should have been all along. This will reduce the number of service DTOs in the system to almost none, making use of the existing models instead. And it will get rid of several mock implementations since I can just re-use the one I already have for the DAL.
After some back and forth on Stack Overflow and a lot of tinkering, I've ended up with this:
public class DomainInterfaceNamingConvention : IRegistrationConvention { public void Process(Type type, Registry registry) { if (IsntRegisterable(type)) return; var interfaceType = GetInterfaceType(type); var dependencyName = GetDependencyName(type); var implementationName = GetImplementationName(type); AddInstanceUnlessOverridden(type, registry, interfaceType, dependencyName, implementationName); AddInstanceOverrides(type, registry, interfaceType, dependencyName, implementationName); AddDefaultInstance(type, registry, interfaceType, dependencyName, implementationName); } private static void AddDefaultInstance(Type type, Registry registry, Type interfaceType, string dependencyName, string implementationName) { if (ConfigurationFactory.GetDefault(dependencyName) == implementationName) registry.For(interfaceType).Use(type); } private static void AddInstanceUnlessOverridden(Type type, Registry registry, Type interfaceType, string dependencyName, string implementationName) { if (!ConfigurationFactory.GetOverrides(dependencyName).Keys.Contains(implementationName)) registry.For(interfaceType).Add(type).Named(implementationName); } private static void AddInstanceOverrides(Type type, Registry registry, Type interfaceType, string dependencyName, string implementationName) { if (ConfigurationFactory.GetOverrides(dependencyName).Values.Contains(implementationName)) foreach (var dependencyOverride in ConfigurationFactory.GetOverrides(dependencyName).Where(o => o.Value == implementationName)) registry.For(interfaceType).Add(type).Named(dependencyOverride.Key); } private static string GetImplementationName(Type type) { return type.Assembly.GetName().Name.Split('.').Last(); } private static string GetDependencyName(Type type) { return type.Assembly.GetName().Name.Split('.').Reverse().Skip(1).First(); } // etc. }
Still favoring convention over configuration, I've continued with the assumption of my assembly names. Given that, it takes a minimal amount of reflection to get the name of the assembly for the implementation and use that convention to name the instances.
It was then irresistible to take it a step further and define some more custom configuration for these overrides. After all, one of the biggest reasons I have this setup is for testing. I like to create custom mock implementations for dependencies and then my testing instance (which has its own config file) can simply specify to use the mock implementations instead of the default ones. This is especially useful for automated integration testing because I can keep multiple config files for the tests and just have the build scripts deploy and run multiple instances of the test code with different config files. Thus isolating individual dependency implementations for testing while using mocks for everything else. This greatly reduces the number of variables in automated testing for me.
So now in that custom configuration section I can override defaults as well as override named instances. Thus, even if my code calls for this:
var eventDestination = IoCFactory.GetInstance<EventRepository>("GoogleCalendar");
I might decide to override that for a specific application, essentially telling it that "even if I ask you for this specific instance, give me this other one (such as the Mock) instead." Thus far this has prevented me from too tightly coupling the code asking for the implementations with the implementations themselves. The application isn't tightly bound to the actual implementation, it can override it. So as long as I keep my names fairly general, I'm happy with the level of coupling.
Not shown here is the implementation of ConfigurationFactory, which is local to my IoC implementation project. But basically all it does is check the config file (using a standard .NET custom config section implementation) for any specified defaults or overrides. So, for example, by default the non-named instance for the repositories might be the SQLExpress implementation. In the config file I might then say to use the XML one as the default instead, so non-named instances become the XML one for that application. I may then also take it a step further and configure it to use the XML one even when the SQLExpress one is explicitly requested. (Which doesn't happen in the codebase at this time, but it does for other implementations such as the aforementioned Event stuff.)
All in all, I'm pretty happy with this implementation. And I'll be a lot happier once I go back through the code and re-implement these custom dependencies as repositories which they should have been all along. This will reduce the number of service DTOs in the system to almost none, making use of the existing models instead. And it will get rid of several mock implementations since I can just re-use the one I already have for the DAL.
No comments:
Post a Comment