How to test all ASP.NET Core Controllers Dependency Injection is valid?

You can write it like this:

[TestFixture]
[Category(TestCategory.Integration)]
public class ControllersResolutionTest
{
    [Test]
    public void VerifyControllers()
    {
        var builder = new WebHostBuilder()
            .UseStartup<IntegrationTestsStartup>();
        var testServer = new TestServer(builder);
        var controllersAssembly = typeof(UsersController).Assembly;
        var controllers = controllersAssembly.ExportedTypes.Where(x => typeof(ControllerBase).IsAssignableFrom(x));
        var activator = testServer.Host.Services.GetService<IControllerActivator>();
        var serviceProvider = testServer.Host.Services.GetService<IServiceProvider>();
        var errors = new Dictionary<Type, Exception>();
        foreach (var controllerType in controllers)
        {
            try
            {
                var actionContext = new ActionContext(
                    new DefaultHttpContext
                    {
                        RequestServices = serviceProvider
                    },
                    new RouteData(),
                    new ControllerActionDescriptor
                    {
                        ControllerTypeInfo = controllerType.GetTypeInfo()
                    });
                activator.Create(new ControllerContext(actionContext));
            }
            catch (Exception e)
            {
                errors.Add(controllerType, e);
            }
        }

        if (errors.Any())
        {
            Assert.Fail(
                string.Join(
                    Environment.NewLine,
                    errors.Select(x => $"Failed to resolve controller {x.Key.Name} due to {x.Value.ToString()}")));
        }
    }
}

This code actually goes through full process of setting up asp.net core application with database configuration and what not you have in you startup so you might want to derive from it and remove/mock some stuff. Also this code requires Microsoft.AspNetCore.TestHost nuget.


I changed original code that I posed as it was not working as expected.


Adapted @Rafal's answer to xUnit to avoid managing Exception iteration and skip dependency on TestHost:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace redacted.WebApi.Test {
    using Core;

    public class VerifyDependencies {
        [Theory]
        [MemberData(nameof(Controllers))]
        public void VerifyController(Type controllerType) {
            var services = new WebHostBuilder().UseStartup<Startup>().Build().Services;
            ControllerUtilities.Create(
                controllerType,
                services.GetService<IControllerActivator>(),
                services.GetService<IServiceProvider>()
            );
        }

        public static IEnumerable<object[]> Controllers() {
            return ControllerUtilities.GetControllers<ApiController>().Select(c => new object[] { c });
        }
    }

    public class ControllerUtilities {
        public static IEnumerable<Type> GetControllers<TProject>() {
            return typeof(TProject)
                .Assembly.ExportedTypes
                .Where(x => typeof(Controller).IsAssignableFrom(x));
        }

        public static Controller Create(Type controllerType, IControllerActivator activator, IServiceProvider serviceProvider) {
            return activator.Create(new ControllerContext(new ActionContext(
                new DefaultHttpContext {
                    RequestServices = serviceProvider
                },
                new RouteData(),
                new ControllerActionDescriptor {
                    ControllerTypeInfo = controllerType.GetTypeInfo()
                })
            )) as Controller;
        }

        public static TController Create<TController>(IControllerActivator activator, IServiceProvider serviceProvider) where TController : Controller {
            return Create(typeof(TController), activator, serviceProvider) as TController;
        }
    }
}

The best method is to throw ArgumentNullExceptions. For example, in your controller's constructor:

_foo = foo ?? throw new ArgumentNullException(nameof(foo));

That will cause any action in the controller (any time the controller is constructed) to fail if the dependency is not satisified. Then, assuming you've got any sort of integration test suite around that controller, all your tests will instantly fail and you'll no exactly why: the constructor argument was not satisfied.


Summarised from https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/, please see link for more details.

As of ASP.NET 3.0 there is now a way to validate controller dependencies on build:

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddControllersAsServices(); // This part adds Controllers to DI

Program.cs:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .UseDefaultServiceProvider((context, options) =>
            {
                options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                // Validate DI on build
                options.ValidateOnBuild = true;
            });

Notes:

  • Service provider validation is only enabled in the Development environment by default.
  • Will not work for run-time ServiceProvider look ups (service locator pattern) e.g. _service = provider.GetRequiredService<MyService>();
  • Will not work for [FromServices] parameters in methods (i.e. it only checks constructor dependencies)
  • Will not work for 'open generics' e.g. services.AddSingleton(typeof(MyServiceWithGeneric<>));
  • Will not work for services registered with factory functions e.g.
    services.AddSingleton<MyService>(provider => 
    {
        var nestedService = provider.GetRequiredService<MyNestedService>();
        return new MyService(nestedService);
    });