Custom dependency resolver in regular ASP.NET Framework project

Challenge:

Implement dependency injection using IoC container – Inversion of control container (a.k.a DI container – Dependency Injection container) in ASP.NET MVC Framework project.

We will see how an IoC container like Microsoft.Extensions.DependencyInjection can be configured in the ASP.NET Framework project of MVC type.

Solution:

I have used Visual studio 2019, create a new project and add MVC and web API folder and code references. See below snapshots.

In this example, we have a property in a controller. We will try to inject IRepo instance to constructor of a controller. Consider below code for IRepo and DBRepo.

public interface IRepo
{
	string Content { get; set; }
}

public class DBRepo : IRepo
{
	private string _content;
	public string Content { get => this._content; set => this._content = value; }
}

Consider HomeController code as below.

public class HomeController : Controller
{
	public IRepo repo;

	public HomeController(IRepo vrepo)
	{
		this.repo = vrepo;
	}

	public ActionResult Index()
	{
		return View();
	}
}

This is only for demo purpose. You could provide your own inteface and its concrete class.

Install the nuget package – Microsoft.Extensions.DependencyInjection.

Add Global.asax file in the project if it does not exists.

Key place is the app_start in global.asax where our implementation is done. Most important code is as follows:

void Application_Start(object sender, EventArgs e)
{
	// Code that runs on application startup
	AreaRegistration.RegisterAllAreas();
	GlobalConfiguration.Configure(WebApiConfig.Register);
	RouteConfig.RegisterRoutes(RouteTable.Routes);

	var services = new ServiceCollection();
	services.AddTransient(typeof(IRepo), typeof(DBRepo));
	services.Add(typeof(HomeController), Lifetime.Transient);
	var defaultResolver = new DefaultDependencyResolver(services.BuildServiceProvider());

	/*
		Below code line is the key. Here we need to pass our instance implementing the desired IoC container. Since it accepts an instance of IDependencyResolver, we need to implement this interface. DefaultDependencyResolver code follows and implements the interface IDependencyResolver.

Basically, provides a registration point for dependency resolvers, using the specified dependency
        //     resolver interface.
	*/
	DependencyResolver.SetResolver(defaultResolver);  
}

And the implementation of DefaultDependencyResolver is as below.

public class DefaultDependencyResolver : IDependencyResolver
{
	private IServiceProvider serviceProvider;
	public DefaultDependencyResolver(IServiceProvider serviceProvider)
	{
		this.serviceProvider = serviceProvider;
	}

	public object GetService(Type serviceType)
	{
		return this.serviceProvider.GetService(serviceType);
	}

	public IEnumerable<object> GetServices(Type serviceType)
	{
		return this.serviceProvider.GetServices(serviceType);
	}
}

Above is the bare minimum code requied for implementing the dependency injection.

We could improvise as below and write some generic code for including all the controllers.

public class Global : HttpApplication
{
	void Application_Start(object sender, EventArgs e)
	{
		// Code that runs on application startup
		AreaRegistration.RegisterAllAreas();
		GlobalConfiguration.Configure(WebApiConfig.Register);
		RouteConfig.RegisterRoutes(RouteTable.Routes);

		var services = new ServiceCollection();
		ConfigureServices(services);
		var defaultResolver = new DefaultDependencyResolver(services.BuildServiceProvider());
		DependencyResolver.SetResolver(defaultResolver);
	}

	private void ConfigureServices(ServiceCollection services)
	{
		services.AddTransient(typeof(IRepo), typeof(DBRepo));

		services.AddMvcControllers("*");

		/*		services.AddControllersAsServices(typeof(Global).Assembly.GetExportedTypes()
		.Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition)
		.Where(t => typeof(IController).IsAssignableFrom(t)
		|| t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)));
		*/
	}
}

Below is the implementation for the extension methods on IServiceCollection.

    public static class ServiceProviderExtensions
    {
        private const string DefaultControllerFilter = "*Controller";
        /*
        public static IServiceCollection AddControllersAsServices(this IServiceCollection services, IEnumerable<Type> serviceTypes)
        {
            foreach (var type in serviceTypes)
            {
                services.AddTransient(type);
            }

            return services;
        }
        */
        public static void AddMvcControllers(this IServiceCollection serviceCollection, params string[] assemblyFilters)
        {
            serviceCollection.AddMvcControllers(GetAssemblies(assemblyFilters));
        }
        private static Assembly[] GetAssemblies(IEnumerable<string> assemblyFilters)
        {
            var assemblies = new List<Assembly>();
            foreach (var assemblyFilter in assemblyFilters)
            {
                assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(assembly => IsWildcardMatch(assembly.GetName().Name, assemblyFilter)).ToArray());
            }
            return assemblies.ToArray();
        }

        private static bool IsWildcardMatch(string input, string wildcard)
        {
            return input == wildcard || Regex.IsMatch(input, "^" + Regex.Escape(wildcard).Replace("\\*", ".*").Replace("\\?", ".") + "$", RegexOptions.IgnoreCase);
        }
        public static void AddMvcControllers(this IServiceCollection serviceCollection, params Assembly[] assemblies)
        {
            serviceCollection.AddMvcControllers(assemblies, new[] { DefaultControllerFilter });
        }

        private static void AddMvcControllers(this IServiceCollection serviceCollection, IEnumerable<Assembly> assemblies, string[] classFilters)
        {
            var controllers = GetTypesImplementing(typeof(IController), assemblies, classFilters);

            foreach (var controller in controllers)
            {
                serviceCollection.Add(controller, Lifetime.Transient);
            }
        }

        public static void Add(this IServiceCollection serviceCollection, Type type, Lifetime lifetime)
        {
            switch (lifetime)
            {
                case Lifetime.Singleton:
                    serviceCollection.AddSingleton(type);
                    break;
                case Lifetime.Transient:
                    serviceCollection.AddTransient(type);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null);
            }
        }

        private static IEnumerable<Type> GetTypesImplementing(Type implementsType, IEnumerable<Assembly> assemblies, params string[] classFilter)
        {
            var types = GetTypesImplementing(implementsType, assemblies.ToArray());
            if (classFilter != null && classFilter.Any())
            {
                types = types.Where(type => classFilter.Any(filter => IsWildcardMatch(type.FullName, filter)));
            }
            return types;
        }

        private static IEnumerable<Type> GetTypesImplementing(Type implementsType, params Assembly[] assemblies)
        {
            if (assemblies == null || assemblies.Length == 0)
            {
                return new Type[0];
            }

            var targetType = implementsType;

            return assemblies
                .Where(assembly => !assembly.IsDynamic)
                .SelectMany(GetExportedTypes)
                .Where(type => !type.IsAbstract && !type.IsGenericTypeDefinition && targetType.IsAssignableFrom(type))
                .ToArray();
        }

        private static IEnumerable<Type> GetExportedTypes(Assembly assembly)
        {
            try
            {
                return assembly.GetExportedTypes();
            }
            catch (NotSupportedException)
            {
                // A type load exception would typically happen on an Anonymously Hosted DynamicMethods
                // Assembly and it would be safe to skip this exception.
                return Type.EmptyTypes;
            }
            catch (FileLoadException)
            {
                // The assembly points to a not found assembly - ignore and continue
                return Type.EmptyTypes;
            }
            catch (ReflectionTypeLoadException ex)
            {
                // Return the types that could be loaded. Types can contain null values.
                return ex.Types.Where(type => type != null);
            }
            catch (Exception ex)
            {
                // Throw a more descriptive message containing the name of the assembly.
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to load types from assembly {0}. {1}", assembly.FullName, ex.Message), ex);
            }
        }
    }

    public enum Lifetime
    {
        Transient,
        Singleton
    }

Use the required namespaces.

Put a break-point in HomeController constructor. Run the application in debug mode, you will see the concrete instance DBRepo is assigned to IRepo.

That’s all. Please let me know if you found it to be helpful. If yes, then please do like it and share it with your colleagues. Also, do let me know if you have any suggestion in the comment section below. Do follow this blog if you want to receive notification for the future blog posts. Thank you so much.