3
votes

We have a very large, complex enterprise application that started life in 2005 before IOC containers were widespread in .NET. We would like to retrofit an IOC container as part of our migration to a complete event-driven architecture based on RabbitMQ (and easynetq). As a business we are agreed this will give us a commercial advantage over our competitors.

I feel it’s important to give some preamble as implementation strategy is key:

  • 1.5 million+ lines of C# using .net 4, in 600+ projects, 30000+ classes, 40000+ Unit Tests deployed in over 50 different applications end points (command line exe, Windows services, web services etc.).
  • The amount of simple CRUD the application has forms less than 10%. Most of the application is complex transaction processing where a single input value can easily pass through 20 to 30 dependencies which maybe talk to 2 or 3 other sub-systems and/or modules.
  • We ship a major new release once a month to all customers with all various different configurations and the application is very stable but we still aggressively innovate. We need to migrate over a period of 12 to 36 months.
  • Scalability and performance is very important to us. We have load tested to over 1500 transactions per second and 80ms response time. Every millisecond counts.
  • Very stable and fairly consistent architecture that continuously evolves in a controlled manor – Development is fairly painless.

At the moment all the dependency injection is constructor based and hand rolled:

public sealed class TestCommandHandler
{
        private readonly IUnitOfWork _unitOfWork;
        private readonly ITestCommandValidator _testCommandValidator;

        public TestCommandHandler(IUnitOfWork unitOfWork)
        {
            this._unitOfWork = unitOfWork;
            this._testCommandValidator = new ITestCommandValidator(unitOfWork);
        }

        public TestCommandHandler(IUnitOfWork unitOfWork, IValidator testCommandValidator)
        {
            this._unitOfWork = unitOfWork;
            this._testCommandValidator = testCommandValidator;
        }
    }

Unit of Work contains access to repositories which can easily be mocked:

public class UnitOfWork : IUnitOfWork, IDisposable
{
        private IAccountRepository _accountRepository;

        public IAccountRepository Account
        {
            get
            {
                if(this._accountRepository == null)
                {
                    this._accountRepository = new AccountRepository(this);
                }
                return this._accountRepository;
            }
            set
            {
                this._accountRepository = value;
            }
        }
        //Begin Tx, Commit Tx etc
}

At the moment all the test dependencies are conditionally compiled in debug mode. The release code uses the non-conditional code where we create the concrete dependency. The biggest object dependency is the UnitOfWork which is normally created around every business transaction and passed down the stack. E.g.,

using(var unitOfWork = new UnitOfWork())
{
}

This is normally wrapped inside another class that also supports IDisposable. We also plan to pass in the AccountID into the UnitOfWork so we can easily Shard against different databases moving forward using a mod function.

In order to move to a dependency framework it feels like we need to get the UnitOfWork sorted first but we need a baby steps. I am really looking for advice on the best way to achieve this with such a large application. We plan to do phase 1 over Xmas period where we have a good freeze and merge in all desperate branches into one mainline branch to make big changes.

We are open on which dependency inject framework to use. We have had a bit of a play with StructureMap. We see Ninject has high download stats on Nuget but read it scores poorly on performance. We don’t really want a further migration process to yet another dependency injection framework. So we are open to suggestions. This isn't a religious war on which is best, it’s more important that we have something to migrate into. The most important requirement being it’s fluently configured to avoid config hell.

Other concerns we have are in StructureMap terms, is how we declare the registries. Do we declare the registries per assembly? Any recommended naming standard around folders, class names in a large application? My rough guess is will have around 1000 registries. Also, any ideas on scanning strategies for such a large code base? Should we be concerned?

Thanks for ready this far but the background is important as it's not a 10-minute job.

Hubert

1
I've also used ninject quite a while and love its interface. Since you require performance, i would look into Autofac. It's feature-wise similar to Ninject but seems to perform better. Also see: palmmedia.de/blog/2011/8/30/… - BatteryBackupUnit

1 Answers

2
votes

Posting this as an answer but there is no correct asnwer to your problem, just to overcome the comment limitation.

You have a lot of advantages in your scenario in order to implement IoC:

  1. Your classes are already decoupled and prepared for constructor injection;
  2. You split your dependencies using conditional flags.

So, I'll just point out a few points of advice, bases on your current situation.

  • If performance is critical, take care if using Ninject. As a heavy Ninject user, I find it extremely flexible and powerful for its Fluent Configuration, Modules and Contextual Bindings. But all this power comes with a price, and its performance per activation request is much worse if compared to other IoC containers.

  • Regardless of framework of choice and since you are starting introducing those changes, you don't want to be tied to a container. Make sure you abstract it and then you can switch on the fly for another.

  • You already have your "modules" split by configuration. Make sure you gather them in modules when you plug in the container configuration code. Don't leave all dependencies for all modules configured in one place. Ninject has support for modules, but it is very easy to achieve this with any container, specially true if you abstract them. Don't use the same module names for dummy/real implementations, but rather load one module or another based on your configuration.

  • As per "auto-register" or "by-convention", you are to be really strict and careful about the standard or else it is very easy to mess things up. I strongly advise to not use agressive upfront auto-register queries and keep them to a minimum and very simple cases such as "IAccountRepository" -> "AccountRepository" or "AccountRepositoryImpl". And even those, you can have the conventions split by module so you can override them for your testing purposes.

  • Do small changes normally during your release cycle, don't do branch changes and keep them there until you integrate them. Like you said, baby steps. You already have dependency injection, so this will be painless. Reinforce this policy within the team to use the container and do small changes as the features are implemented or reworked.

Thats my 2 cents, hope it helps.