Search code examples
c#.net-coresimple-injector

SimpleInjector `Controller is registered as transient, but implements IDisposable` Error for ApiController


So, this error has been giving other users troubles for years as much as I could see on my research. Their suggested solutions on the other hand, don't work for my project somehow.

So, my startup class is;

public class Startup
{
    private Container container = new Container();
    public IConfigurationRoot Configuration { get; private set; }

    public void ConfigureServices(IServiceCollection services)
    {
        container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
        var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        this.Configuration = new ConfigurationBuilder().SetBasePath(
            Directory.GetCurrentDirectory()).AddJsonFile($"appsettings.{environmentName}.json").Build();
        services.AddCors(options =>
        {
            options.AddPolicy(
              "CorsPolicy",
              builder => builder.WithOrigins(new[] { "http://localhost" })
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials()
            );
        });

        services.AddControllers(); // ERR 1

        services.AddSimpleInjector(container, options => /// ERR 2
        {
            options
                .AddAspNetCore()
                .AddControllerActivation();
        });
        services.AddCors();

        InitializeContainer();
    }

    private void InitializeContainer()
    {
        // Register services
        ConnectionConfig conf = new ConnectionConfig();
        AppSettings settings = new AppSettings();
        PathConfig path = new PathConfig();
        Configuration.GetSection("AppSettings").Bind(settings);
        Configuration.GetSection("ConnectionConfig").Bind(conf);
        Configuration.GetSection("PathConfig").Bind(path);
        container.RegisterInstance<ConnectionConfig>(conf);
        container.RegisterInstance<PathConfig>(path);
        container.RegisterInstance<AppSettings>(settings);

        new CoreModule().RegisterServices(container);
        new DataModule().RegisterServices(container);
        new ServiceModule().RegisterServices(container);
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();
        app.UseCors(
           options => options
           .AllowAnyOrigin()
           .AllowAnyMethod()
           .AllowAnyHeader()
        );

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

        app.UseSimpleInjector(container);

        container.Verify(); /// ERR 3
    }
}

The error for ERR 3 is; Error

The suggested solutions say, that I should register these controllers with the LifestyleScope of Scoped but when I try to do that, it gives error at ERR 2. ERR 2:

A registration for the Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager is missing from the ASP.NET Core configuration system. This is most likely caused by a missing call to services.AddMvcCore() or services.AddMvc() as part of the ConfigureServices(IServiceCollection) method of the Startup class. A call to one of those methods will ensure the registration of the ApplicationPartManager.

at SimpleInjector.SimpleInjectorAspNetCoreBuilderMvcCoreExtensions.GetApplicationPartManager(IServiceCollection services, String methodName)
at SimpleInjector.SimpleInjectorAspNetCoreBuilderMvcCoreExtensions.AddControllerActivation(SimpleInjectorAspNetCoreBuilder builder)
at ....Startup.<>c.<ConfigureServices>b__5_1(SimpleInjectorAddOptions options) in C:\Projects\...\Startup.cs:line 48
at SimpleInjector.SimpleInjectorServiceCollectionExtensions.AddSimpleInjector(IServiceCollection services, Container container, Action`1 setupAction)
at ....Startup.ConfigureServices(IServiceCollection services) in C:\Projects\...\Startup.cs:line 46
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.InvokeCore(Object instance, IServiceCollection services)
at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.<>c__DisplayClass9_0.<Invoke>g__Startup|0(IServiceCollection serviceCollection)
at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.Invoke(Object instance, IServiceCollection services)
at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.<>c__DisplayClass8_0.<Build>b__0(IServiceCollection services)
at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass12_0.<UseStartup>b__0(HostBuilderContext context, IServiceCollection services)
at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at ....Program.Main(String[] args) in C:\Projects\...\Program.cs:line 16

My issue's root is, my poor intensions on returning IHttpAction from the BaseApiController. My reasons for that is, to make sure my API's client is always in the knowledge of what's happening in case an error happens. Without returning InternalServerError or Ok() etc, it's kind of harder for me to explain my error to the client.

This is the BaseApiController:

// When I use `ControllerBase` here for the inheritance, API works like a charm 
// after getting rid of the `IHttpActionResult` and writing `T` instead.

public class BaseApiController : ApiController 
{
    private readonly LogModule _logModule;

    public BaseApiController(LogModule logModule)
    {
        _logModule = logModule;
    }

    public IHttpActionResult ExecuteAction<T>(Func<BaseResponse<T>> func)
    {
        try
        {
            return Ok(func());
        }
        catch (Exception ex)
        {
            var requestScope = Request.GetDependencyScope();
            var logModule = requestScope.GetService(typeof(LogModule)) as LogModule;

            StackTrace stackTrace = new StackTrace();
            var description = "API." + this.GetType().Name + "." + stackTrace.GetFrame(1).GetMethod().Name;
            logModule.Error(description, ex);
            return InternalServerError();
        }
    }
}

And this is a controller I use;

[Route("api/foo")]
[ApiController]
public class FooController : BaseApiController
{
    private readonly FooService _fooService;

    public FooController(FooService fooService, LogModule logModule) : base(logModule)
    {
        _fooService = fooService;
    }

    [HttpPost]
    [Route("create_foo")]
    public System.Web.Http.IHttpActionResult GetFoo([FromBody] FooRequest request)
    {
        var baseResponse = ExecuteAction(() =>
        {
            return _fooService.AddFoo(request);
        });
        return baseResponse;
    }
}

In conclusion, how can I use SimpleInjector with ApiControllers?


Solution

  • To register your Controllers as Scoped, you can use the following code:

    // inside ConfigureServices or Startup ctor
    this.container.Options.LifestyleSelectionBehavior = new ApiControllersAsScopedBehavior();
    
    // behavior
    internal class ApiControllersAsScopedBehavior : ILifestyleSelectionBehavior
    {
        public Lifestyle SelectLifestyle(Type implementationType) =>
            typeof(ApiController).IsAssignableFrom(implementationType)
                ? Lifestyle.Scoped
                : Lifestyle.Transient;
    }