【发布时间】:2018-03-13 22:02:11
【问题描述】:
我有一个使用 Entity Framework 6.0、用于 DI 和 CQRS 架构的 Autfac 的 WebApi 项目。我遇到的问题是 DbContext 没有按照它应该的方式处理。我采取的行动:
- 我运行了两个快速请求,例如从 Postman 向一个端点发送请求,运行时在控制器方法中的断点处停止,我将第二个请求发送到不同控制器中的另一个端点。
- 恢复运行时
- 如果第二个请求在第一个请求完成之前完成,第一个请求会抛出 dbcontext 已处理的错误,并且它无法运行它应该执行的任何操作
原来问题是我一个接一个地从前端发帖和打补丁的时候出现的。
似乎生命周期范围并不是真正的每个请求。似乎所有 dbcontexts 都位于请求的一端。另一个没有任何工作。
它是如何配置的?
从最高层-控制器开始:
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");
}
}
}
服务层:
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;
}
}
命令总线所在的服务工具接口:
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; }
}
以及任何命令处理程序:
public class EditUserHandler : IHandleCommand<EditUserCommand>
{
private readonly ICommandsContext _commandsContext;
public SaveUserHandler(ICommandsContext commandsContext)
{
_commandsContext = commandsContext;
}
public void Handle(EditUserCommand command)
{
... using dbcontext here...
}
}
}
对于 DI,我使用 Autofac,所有资源都设置为每个请求的生命周期,分为模块,例如数据访问模块
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);
}
}
两个接口的区别在于 IQueryContext 不能改变实体状态并使用 SaveChagnes() 方法。 IQueryContext 中包含所有 DbSet,而 ICommandsContext 继承自它并添加了 SettingState 方法(添加、修改、删除)和 SaveChanges() 方法。 IQueryContext 被注入到查询中,而 ICommandsContext 被注入到命令中,如上例所示。 现在命令总线的 Autofac 配置如下所示:
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);
}
}
虽然命令总线看起来像那样
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);
}
}
应用程序的翻译使用完全独立的上下文,但我在这里并不重要。
我没有找到任何有类似问题的帖子。它仅在同时处理两个请求时发生。我不知道是配置错误还是 Autofac 搞砸了,因为从技术上讲,它不应该处理分配给另一个请求的 dbcontext。
对不起,文字墙;)我希望有人能帮忙。
将 dbcontext 的生命周期更改为 SingleInstance 解决了这个问题,但我们不希望这样 :)
解决方案编辑:
@ZeljkoVujaklija 注意到 InfrastractureModule 中的 CommandsDbContext 声明似乎很奇怪。我从 InfrastractureModule 中删除了整个 CommandBus 注册。相反,我在所有命令所在的程序集中创建了 CommandsModule。看起来是这样的:
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();
}
}
它不仅解决了问题,还摆脱了庞大的工厂。
【问题讨论】:
-
您是否尝试过使用派生类来实现 ICommandsContext 接口,而不是为两个接口使用同一个类?我对 AutoFac 没有任何经验,但我会尝试这样做
-
刚试过。没有帮助。我还删除了 IQueryContext 并在各处注入了 ICommandsContext,但这也没有解决问题。
-
InfrastructureModule 中的变量 ICommandsContext _commandsContext 在类级别声明。这很奇怪。在我看来,您在 CreateHandlersFactory 中没有找到任何其他方式来访问它。为什么不在 CommandBus 中解析 ICommandsContext 和 ITranslationsCommandsContext ,即将它们作为 ctor 参数。 CreateHandlersFactory 可以将它们作为参数。
-
@ZeljkoVujaklija 就是这样!!!我改变了命令/命令总线的注册方式。我无法弄清楚您提出的方式,但我在原始帖子中添加了我的解决方案。谢谢你,先生:)
标签: c# asp.net entity-framework autofac cqrs