Search code examples
c#.net-coreautofac

Autofac module order registration causes objects to be injected with their default instance values (default(T))


I'm facing a problem with Autofac registrations. In short, if I register the models BEFORE my configuration, when I load the configuration, it works smoothly but, if I register the models AFTER I register the configuration, the configuration models are loaded with their default types (default(T)). Below is the code to reproduce the problem:

using System;
using System.IO;

using Autofac;

using Microsoft.Extensions.Configuration;

namespace AutofacConfigurationTest.CrossCutting
{
    public class ModuleModel : Module
    {
        protected override void Load(ContainerBuilder containerBuilder)
        {
            containerBuilder.RegisterType<Cache.Configuration>()
                .As<Cache.IConfiguration>();

            containerBuilder.RegisterType<Repository.Configuration>()
                .As<Repository.IConfiguration>();
        }
    }

    public class ModuleConfiguration : Module
    {
        protected override void Load(ContainerBuilder containerBuilder)
        {
            var configurationRoot = new Configuration.Container().ConfigurationRoot;

            containerBuilder.RegisterInstance(configurationRoot).As<IConfigurationRoot>();

            containerBuilder
                .RegisterInstance(configurationRoot.GetSection(Cache.Configuration.Name)
                    .Get<Cache.Configuration>()).As<Cache.IConfiguration>();

            containerBuilder
                .RegisterInstance(configurationRoot.GetSection(Repository.Configuration.Name)
                    .Get<Repository.Configuration>()).As<Repository.IConfiguration>();
        }
    }

    public class Container
    {
        public IContainer Kernel { get; }

        public Container()
        {
            var containerBuilder = new ContainerBuilder();

            // uncomment the line below to make it work //
            containerBuilder.RegisterModule(new ModuleModel()); // if we register the models here, before the configuration, the configuration works properly //

            containerBuilder.RegisterModule(new ModuleConfiguration());

            // comment the line below to make it work //
            containerBuilder.RegisterModule(new ModuleModel()); // if we register the models here, after the configuration, the configuration cannot load the data //

            Kernel = containerBuilder.Build();
        }
    }
}

namespace AutofacConfigurationTest.Configuration
{
    public class Container
    {
        private const string ConfigurationFile = "AppSettings.json";

        public Container()
        {
            ConfigurationRoot = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(ConfigurationFile).Build();
        }

        public IConfigurationRoot ConfigurationRoot { get; }
    }
}

namespace AutofacConfigurationTest.Cache
{
    public enum Engine
    {
        None,
        Default
    }

    public interface IConfiguration
    {
        Engine Engine { get; set; }
        int Duration { get; set; }
        string ConnectionString { get; set; }
    }

    public class Configuration : IConfiguration
    {
        public const string Name = "Cache";

        public Engine Engine { get; set; }
        public int Duration { get; set; }
        public string ConnectionString { get; set; }
    }
}

namespace AutofacConfigurationTest.Repository
{
    public enum Engine
    {
        None,
        LiteDb
    }

    public interface IConfiguration
    {
        Engine Engine { get; set; }
        string ConnectionString { get; set; }
    }

    public class Configuration : IConfiguration
    {
        public const string Name = "Repository";

        public Engine Engine { get; set; }
        public string ConnectionString { get; set; }
    }
}

namespace AutofacConfigurationTest
{
    internal class Program
    {
        private static IContainer _container;

        private static void RegisterServices() => _container = new CrossCutting.Container().Kernel;

        private static void DisposeServices()
        {
            if (_container != null &&
                _container is IDisposable disposable)
                disposable.Dispose();
        }

        private static void Main(string[] args)
        {
            try
            {
                RegisterServices();

                // the following objects will be have a default(T) instance
                // if the in the Autofac modules the Model is registered AFTER the Configuration

                var cacheConfiguration = _container.Resolve<Cache.IConfiguration>();
                var repositoryConfiguration = _container.Resolve<Repository.IConfiguration>();

                Console.ReadLine();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                DisposeServices();
            }
        }
    }
}

I have a second question for you: I use the interfaces to force the contract in my models. In short, these interfaces are NEVER injected anywhere, it's just to make easier the maintenance for me. Should I remove the DI/IoC for these models or is there any reason to keep the models registration in the container?


Solution

  • The observed difference in behavior that now depends on the sequence of module registration, is due to the fact that both modules are registering services keyed to Cache.IConfiguration and Repository.IConfiguration. Therefore, the last module to be registered will "win".

    When ModuleModel is registered last it will override any previous registration of the two configuration interfaces, and resolution will yield instances of Cache.Configuration and Repository.Configuration.

    If ModuleConfiguration is registered last, resolution will yield instances provided by the configurationRoot object.

    Inferring your intent from the two modules, since ModuleConfiguration registrations are actually trying to resolve Cache.Configuration and Repository.Configuration, ModuleModel must register these types keyed to those types instead of keyed to the interfaces. You do that using .AsSelf() instead of .As<some interface>(). Read more about AsSelf here.