Search code examples
.netautomapperbootstrappingsolid-principlesopen-closed-principle

Configuring Automapper in Bootstrapper violates Open-Closed Principle?


I am configuring Automapper in the Bootstrapper and I call the Bootstrap() in the Application_Start(), and I've been told that this is wrong because I have to modify my Bootstrapper class each time I have to add a new mapping, so I am violating the Open-Closed Principle.

How do you think, do I really violate this principle?

public static class Bootstrapper
{
    public static void BootStrap()
    {
        ModelBinders.Binders.DefaultBinder = new MyModelBinder();
        InputBuilder.BootStrap();
        ConfigureAutoMapper();
    }

    public static void ConfigureAutoMapper()
    {
        Mapper.CreateMap<User, UserDisplay>()
            .ForMember(o => o.UserRolesDescription,
                       opt => opt.ResolveUsing<RoleValueResolver>());
        Mapper.CreateMap<Organisation, OrganisationDisplay>();
        Mapper.CreateMap<Organisation, OrganisationOpenDisplay>();
        Mapper.CreateMap<OrganisationAddress, OrganisationAddressDisplay>();
    }    
}

Solution

  • I would argue that you are violating two principles: the single responsibility principle (SRP) and the open/closed principle (OCP).

    You are violating the SRP because the bootstrapping class have more than one reason to change: if you alter model binding or the auto mapper configuration.

    You would be violating the OCP if you were to add additional bootstrapping code for configuring another sub-component of the system.

    How I usually handle this is that I define the following interface.

    public interface IGlobalConfiguration
    {
        void Configure();
    }
    

    For each component in the system that needs bootstrapping I would create a class that implements that interface.

    public class AutoMapperGlobalConfiguration : IGlobalConfiguration
    {
        private readonly IConfiguration configuration;
    
        public AutoMapperGlobalConfiguration(IConfiguration configuration)
        {
            this.configuration = configuration;
        }
    
        public void Configure()
        {
            // Add AutoMapper configuration here.
        }
    }
    
    public class ModelBindersGlobalConfiguration : IGlobalConfiguration
    {
        private readonly ModelBinderDictionary binders;
    
        public ModelBindersGlobalConfiguration(ModelBinderDictionary binders)
        {
            this.binders = binders;
        }
    
        public void Configure()
        {
            // Add model binding configuration here.
        }
    }
    

    I use Ninject to inject the dependencies. IConfiguration is the underlying implementation of the static AutoMapper class and ModelBinderDictionary is the ModelBinders.Binder object. I would then define a NinjectModule that would scan the specified assembly for any class that implements the IGlobalConfiguration interface and add those classes to a composite.

    public class GlobalConfigurationModule : NinjectModule
    {
        private readonly Assembly assembly;
    
        public GlobalConfigurationModule() 
            : this(Assembly.GetExecutingAssembly()) { }
    
        public GlobalConfigurationModule(Assembly assembly)
        {
            this.assembly = assembly;
        }
    
        public override void Load()
        {
            GlobalConfigurationComposite composite = 
                new GlobalConfigurationComposite();
    
            IEnumerable<Type> types = 
                assembly.GetExportedTypes().GetTypeOf<IGlobalConfiguration>()
                    .SkipAnyTypeOf<IComposite<IGlobalConfiguration>>();
    
            foreach (var type in types)
            {
                IGlobalConfiguration configuration = 
                    (IGlobalConfiguration)Kernel.Get(type);
                composite.Add(configuration);
            }
    
            Bind<IGlobalConfiguration>().ToConstant(composite);
        }
    }
    

    I would then add the following code to the Global.asax file.

    public class MvcApplication : HttpApplication
    {
        public void Application_Start()
        {
            IKernel kernel = new StandardKernel(
                new AutoMapperModule(),
                new MvcModule(),
                new GlobalConfigurationModule()
            );
    
            Kernel.Get<IGlobalConfiguration>().Configure();
        }
    }
    

    Now my bootstrapping code adheres to both SRP and OCP. I can easily add additional bootstrapping code by creating a class that implements the IGlobalConfiguration interface and my global configuration classes only have one reason to change.