Dependency Injection for Dummies

I’ve been working on a project to scan and archive content from social media sites based on certain rules. I thought it would be fun to do a deeper dive into dependency injection than heretofore, and wanted to share part of what I learned from doing so. This is all based on Autofac, but I suspect the same or similar approaches would work with other DI frameworks.

Because my Visual Studio solution includes multiple projects, I needed a way to spread the DI across multiple assemblies. I tried out a number of different approaches before hitting on one that worked well, is easy to maintain, and seems like it should be easily extendable.

It’s based on two insights: group the registration of Types into modules — which can be incorporated as standalone packages when you build the DI container — and design your system in such a way that only the application root — the project which knits together all the other assemblies that rely on DI — defines the actual DI container.

Using modules in Autofac is easy. You just create a class derived from Autofac.Module, and override the Load() method:

    public class ScannerModule : Autofac.Module
    {
        protected override void Load( ContainerBuilder builder )
        {
            base.Load( builder );

            builder.RegisterType<RemoteWebDriverFactory>()
                .As<IRemoteWebDriverFactory>()
                .SingleInstance();

            builder.RegisterType<SeleniumScannerFactory>()
                .As<ISeleniumScannerFactory>()
                .SingleInstance();

            builder.RegisterType<CommunityScannerDbFactory>()
                .As<ICommunityScannerDbFactory>()
                .SingleInstance();

            builder.RegisterType<ArticleParserFactory>()
                .As<IArticleInfoFactory>()
                .SingleInstance();

            builder.RegisterType<ArticleListFactory>()
                .As<IArticleListFactory>()
                .SingleInstance();

            builder.RegisterType<Scanner>();
        }
    }

You use whatever modules you need when you define your container:

    public static class Container
    {
        private static IContainer _instance;

        public static IContainer Instance
        {
            get
            {
                if( _instance == null )
                {
                    var builder = new ContainerBuilder();

                    builder.RegisterModule<SerilogModule>();
                    builder.RegisterModule<KeyVaultModule>();
                    builder.RegisterModule<ConfigurationModule>();
                    builder.RegisterModule<CommunityScannerDbModule>();
                    builder.RegisterModule<ScannerModule>();
                    builder.RegisterModule<EmailSMSModule>();
                    builder.RegisterModule<TopShelfModule>();

                    _instance = builder.Build();
                }

                return _instance;
            }
        }
    }

Most people use a GetContainer() syntax; for whatever reason, since the IContainer object is static, I like to use the property-with-implicit-registration approach.

The second insight forced me to have to redesign my solution, because originally I was using IContainer objects within various class libraries (i.e., they weren’t in the root application). I was doing that because the .Net Core configuration subsystem — which has a bunch of cool features — requires that all the configuration classes that it creates have public parameterless constructors…which, so far as I can see, rules out using construction-injection DI.

You can’t, for example, inject a Serilog ILogger instance into a configuration class’ constructor. That caused me a bunch of problems, because I wrote my configuration classes to be “self validating”, by way of a Validation() method, and I wanted to log any errors that were found.

In the end, I restructured the self-validation concept so that errors were stashed into a protected ValidationErrors property and, if any were found, a LogValidationErrors() method which pushed the errors out to the log was called. That eliminated the need to do constructor DI injection in the configuration classes, but at the cost of creating an instance of the configuration data manually within Autofac, so validation could be performed. Here’s how I did that (this code fragment was defined within an Autofac.Module Load() override):

        builder.Register<AppConfiguration>( ( c, p ) => {
            var retVal = new AppConfiguration();

            config.Bind( retVal );
            ILogger logger = c.Resolve<ILogger>();

            if( logger == null )
                throw new NullReferenceException( "Could not resolve ILogger while initializing AppConfiguration" );

            if( !retVal.Validate() )
                retVal.LogValidationErrors( logger );

            var kvm = new KeyVaultManager( retVal.Azure.VaultUrl, retVal.Azure.AppID, logger );

            // retrieve the database connection passwords from the Azure Key Vault
            foreach( var dbConfig in retVal.Database.Connections )
            {
                dbConfig.UserID = kvm.GetSecret( $"DatabaseCredentials--{dbConfig.Location}--UserID" );
            }

            if( !retVal.Database.Connections.Any( x =>
                x.Location.Equals( retVal.Database.Location, StringComparison.OrdinalIgnoreCase ) ) )
            {
                var mesg = $"No database connection defined for location '{retVal.Database.Location}'";
                logger.Fatal( mesg );
                throw new ArgumentOutOfRangeException( mesg );
            }

            return retVal;
        } )
    .SingleInstance();

The key here is that I’m using IContainer — that’s the “c” in the lambda expression’s arguments — to resolve various Types that I need to populate and validate the instance of AppConfiguration that Autofac is going to return.

In my case it’s a single instance (i.e., the same instance throughout the app), but that’s not a requirement.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Archives
Categories