grokky
grokky

Reputation: 9255

Get per-request dependency from an Autofac func factory

I'm using ASP.NET Core and Autofac. Almost everything is registered as per lifetime scope ("per request"). So my database context DbContext is the same instance throughout a request.

However I have a singleton which also depends on DbContext. To avoid a captive dependency, it is injected as Func<Owned<DbContext>>, which means a new DbContext instance each time.

The problem is I need the same instance, as everywhere else during the request, not a new one.

I want to avoid a captive dependency bug, but I also want the same instance. Is that possible via tagging or a custom registration?

Upvotes: 3

Views: 1862

Answers (1)

Tseng
Tseng

Reputation: 64131

From the comments the least "architectural" painful approach may be by creating your own Scoped<T> class which will resolve the DbContext from current HttpContext

// Use an interface, so we don't have infrastructure dependencies in our domain
public interface IScoped<T> where T : class
{
    T Instance { get; }
}

// Register as singleton too.
public sealed class Scoped<T> : IScoped<T> where T : class
{
    private readonly IHttpContextAccessor contextAccessor;
    private HttpContext HttpContext { get; } => contextAccessor.HttpContext;

    public T Instance { get; } => HttpContext.RequestServices.GetService<T>();

    public Scoped(IHttpContextAccessor contextAccessor)
    {
        this.contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor));
    }
}

Register it as

// Microsoft.Extensions.DependencyInjection
services.AddSingleton(typeof(IScoped<>), typeof(Scoped<>);
// Autofac
containerBuilder.RegisterType(typeof(Scoped<>))
            .As(typeof(IScoped<>));

Then inject this into your validator service.

public class CustomerValidator: AbstractValidator<Customer>
{
    private readonly IScoped<AppDbContext> scopedContext;
    protected AppDbContext DbContext { get } => scopedContext.Instance;

    public CustomValidator(IScoped<AppDbContext> scopedContext)
    {
        this.scopedContext = scopedContext ?? throw new ArgumentNullException(nameof(scopedContext));

        // Access DbContext via this.DbContext
    }
}

This way you can inject any scoped service w/o further registrations.

Additional notes

Autofac is considered a "conformer" (see docs) DI and integrates well with ASP.NET Core and Microsoft.Extensions.DependencyInjection.

From the documentation

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Add services to the collection.
    services.AddMvc();

    // Create the container builder.
    var builder = new ContainerBuilder();

    // Register dependencies, populate the services from
    // the collection, and build the container. If you want
    // to dispose of the container at the end of the app,
    // be sure to keep a reference to it as a property or field.
    builder.RegisterType<MyType>().As<IMyType>();
    builder.Populate(services);
    this.ApplicationContainer = builder.Build();

    // Create the IServiceProvider based on the container.
    return new AutofacServiceProvider(this.ApplicationContainer);
}

There a few subtle differences to the default usage of Startup class and Microsoft.Extensions.DependencyInjection container.

  1. ConfigureServices isn't void anymore, it returns IServiceProvider. This will tell ASP.NET Core to use the returned provider instead of DefaultServiceProvider from Microsoft.Extensions.DependencyInjection.
  2. We return the Autofac container adapter: new AutofacServiceProvider(this.ApplicationContainer) which is the root container.

This is important to make ASP.NET Core use the container everywhere in ASP.NET Core, even inside middlewares which resolve per request dependencies via HttpContext.RequestedServices.

For that reasons you can't use .InstancePerRequest() lifetime in Autofac, because Autofac isn't in control of creating scopes and only ASP.NET Core can do it. So there is no easy way to make ASP.NET Core use Autofac's own Request lifetime.

Instead ASP.NET Core will create a new scope (using IServiceScopeFactory.CreateScope()) and use a scoped container of Autofac to resolve per-request dependencies.

Upvotes: 5

Related Questions