简体   繁体   中英

DbContext is Disposed When Using Autofac Dependency Injection on WebApi project

I have a WebApi project using Entity Framework 6.0, Autfac for DI and CQRS architecture. The problem I have that DbContext isn't disposing how it supposed to. The action I take:

  • I run two quick requests, eg send request from Postman to one endpoint, runtime stops on breakpoint in controller method, I send second request to another endpoint in different controller.
  • Resume Runtime
  • if the second request finished before the first one is done, the first one throws and error that dbcontext was disposed and it cannot run whatever it was supposed to do

Originally problem appeared when I posted and patched from frontend one after another.

It seems like lifetime scope is not really per-request. It seems like all dbcontexts are disposed on one of the request's end. The other one does not have anything to work with.

How is it configured?

Starting from the highest layer - controller:

public class UsersController : BaseController, IUsersApi

{
    private readonly IUserService _userService;

    public UsersController(IUserService userService, ILogging logging) : base(logging)
    {
        _userService = userService;
    }

    [HttpGet]
    [Route("api/users")]
    public IList<UserDto> GetUsers()
    {
        try
        {
            return _userService.GetAllUsers();
        }
        catch (Exception e)
        {
            _logger.Error(e);
            _logger.Trace(e);
            throw;
        }

    }


    [HttpPatch]
    [Route("api/users/")]
    public IHttpActionResult EditUsers(ICollection<UserEditDto> model)
    {
        try
        {
            _userService.EditUsers(model);
            return Ok();
        }
        catch (Exception e)
        {
            _logger.Error(e);
            _logger.Trace(e);
            return BadRequest("Error");
        }
    }
}

Service layer:

 public class UserService : IUserService
{
    private readonly IServiceTools _serviceTools;
    private readonly IUserQuerier _userQuerier;

    public UserService(IServiceTools serviceTools, IUserQuerier userQuerier)
    {
        _serviceTools = serviceTools;
        _userQuerier = userQuerier;
    }


    public void EditUsers(ICollection<UserEditDto> model)
    {
        var mapper = _serviceTools.AutoMapperConfiguration.Configure().CreateMapper();
        var userEditCommands = mapper.Map<ICollection<UserEditDto>, ICollection<EditUserCommand>>(model);
        foreach (var command in userSaveCommands)
        {
            _serviceTools.CommandBus.SendCommand(command);
            CacheHelper.Clear(command.Id.ToString());
        }
    }

    public IList<UserDto> GetAllUsers()
    {
        var allUsers = _userQuerier.GetAllUsers();
        var result = allUsers.Select(x => new UserDto()
        {
            ...
        }).ToList();
        return result;
    }        
}

Service Tools interface where command bus sits:

public interface IServiceTools
{
    ICommandBus CommandBus { get; }
    IAutoMapperConfiguration AutoMapperConfiguration { get; }
    IIdentityProvider IdentityProvider { get; }
}

public class ServiceTools : IServiceTools
{
    public ServiceTools(ICommandBus commandBus, IAutoMapperConfiguration autoMapperConfiguration, IIdentityProvider identityProvider)
    {
        CommandBus = commandBus;
        AutoMapperConfiguration = autoMapperConfiguration;
        IdentityProvider = identityProvider;
    }

    public ICommandBus CommandBus { get; }
    public IAutoMapperConfiguration AutoMapperConfiguration { get; }

    public IIdentityProvider IdentityProvider { get; }
}

And whatever handler for command:

public class EditUserHandler : IHandleCommand<EditUserCommand>
{
    private readonly ICommandsContext _commandsContext;

    public SaveUserHandler(ICommandsContext commandsContext)
    {
        _commandsContext = commandsContext;
    }

    public void Handle(EditUserCommand command)
    {
        ... using dbcontext here...
    }
}

}

For DI I use Autofac, all resources are set to per-request lifetime, split into modules, eg module for data access

public class DataModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<AppNameDbContext>().As<ICommandsContext>().InstancePerRequest();
        builder.RegisterType<AppNameDbContext>().As<IQueryContext>().InstancePerRequest();
        base.Load(builder);
    }
}

The difference between both interfaces is that IQueryContext cannot change entity states and use SaveChagnes() method. IQueryContext have all DbSets in it, while ICommandsContext inherits from it and adds SettingState methods (added, modified, deleted) and SaveChanges() method. IQueryContext is injected into queries and ICommandsContext into commands as seend in example aboove. Now the Autofac config for command bus looks like that:

public class InfrastractureModule : Module
{
    private ICommandsContext _commandsContext;
    private ITranslationsCommandsContext _translationsCommandsContext;

    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<AutoMapperConfiguration>().
                         As<IAutoMapperConfiguration>().InstancePerRequest();
        builder.RegisterType<ServiceTools>().As<IServiceTools>().InstancePerRequest();
        builder.Register(c =>
        {
            _commandsContext = c.Resolve<ICommandsContext>();
            _translationsCommandsContext = c.Resolve<ITranslationsCommandsContext>();
            return new CommandBus(CreateHandlersFactory);
        })
        .As<ICommandBus>().InstancePerRequest();
        base.Load(builder);
    }

    private IHandleCommand CreateHandlersFactory(Type type)
    {
        if (type == typeof(XXXCommand))
        {
            return new XXXHandler(_commandsContext);
        }
    }

While the command bus looks like that

public class CommandBus : ICommandBus
{
    private readonly Func<Type, IHandleCommand> _handlersFactory;

    public CommandBus(Func<Type, IHandleCommand> handlersFactory)
    {
        _handlersFactory = handlersFactory;
    }

    public void SendCommand<T>(T command) where T : ICommand
    {
        var handler = (IHandleCommand<T>) _handlersFactory(typeof(T));
        handler.Handle(command);
    }
}

There is completely separate context used for translations for the app, but I do not thing that is important here.

I did not find any posts with similar problem. It only occurs when where two requests processed at the same time. I do not know if the configuration is wrong or Autofac messes things up, because it should not technically dispose dbcontext which was allocated for another request.

Sorry for the wall of text ;) I hope someone can help with that.

Obiously changing dbcontext's lifetime to SingleInstance fixed the problem, but we do not want that :)

SOLUTION EDIT:

As @ZeljkoVujaklija noticed CommandsDbContext declarations in InfrastractureModule seemed strange. I removed whole CommandBus registration from InfrastractureModule. Instead I created CommandsModule in the assembly where all the commands sit. It looks like that:

public class CommandsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.RegisterAssemblyTypes(ThisAssembly)
            .Where(x => x.IsAssignableTo<IHandleCommand>())
            .AsImplementedInterfaces();

        builder.Register<Func<Type, IHandleCommand>>(c =>
        {
            var ctx = c.Resolve<IComponentContext>();

            return t =>
            {
                var handlerType = typeof(IHandleCommand<>).MakeGenericType(t);
                return (IHandleCommand)ctx.Resolve(handlerType);
            };
        });

        builder.RegisterType<CommandBus>()
            .AsImplementedInterfaces();
    }

}

Not only it fixes the problem but also gets rid of huge factory.

If you are running within ASP.NET Core you should run InstancePerLifetimeScope instead of InstancePerRequest

Use InstancePerLifetimeScope instead of InstancePerRequest. In previous ASP.NET integration you could register a dependency as InstancePerRequest which would ensure only one instance of the dependency would be created per HTTP request. This worked because Autofac was in charge of setting up the per-request lifetime scope. With the introduction of Microsoft.Extensions.DependencyInjection, the creation of per-request and other child lifetime scopes is now part of the conforming container provided by the framework, so all child lifetime scopes are treated equally - there's no special “request level scope” anymore. Instead of registering your dependencies InstancePerRequest, use InstancePerLifetimeScope and you should get the same behavior. Note if you are creating your own lifetime scopes during web requests, you will get a new instance in these child scopes.

http://autofaccn.readthedocs.io/en/latest/integration/aspnetcore.html#differences-from-asp-net-classic

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM