如何在EF Core中使用有限作用域的DbContext来实现数据库弹性?

3

我有一个基于CQRS模式的简单命令如下:

public sealed class EditPersonalInfoCommandHandler : ICommandHandler<EditPersonalInfoCommand> {

        private readonly AppDbContext _context;

        public EditPersonalInfoCommandHandler(AppDbContext context) {
            _context = context;
        }

        public Result Handle(EditPersonalInfoCommand command) {
            var studentRepo = new StudentRepository(_context);
            Student student = studentRepo.GetById(command.Id);
            if (student == null) {
                return Result.Failure($"No Student found for Id {command.Id}");
            }

            student.Name = command.Name;
            student.Email = command.Email;

            _context.SaveChanges();
            return Result.Success();
        }

}

现在我有一个需求,如果_context.SaveChanges()由于异常失败,则要尝试最多5次。为此,我可以在该方法中简单地使用for循环来实现:
for(int i = 0; i < 5; i++) {
    try {
        //required logic
    } catch(SomeDatabaseException e) {
        if(i == 4) {
           throw;
        }
    }
}

要求将该方法作为单个单位执行。问题是,一旦_context.SaveChanges()抛出异常,相同的_context不能用于重新尝试逻辑。文档如下:

丢弃当前的DbContext。 创建一个新的DbContext,并从数据库中恢复应用程序的状态。 告知用户上次操作可能未成功完成。

然而,在Startup.cs中,我将AppDbContext注册为一个范围依赖项。为了重新尝试方法逻辑,我需要一个新的AppDbContext实例,但被注册为范围的将不允许这样做。
我想到的一个解决方案是使AppDbContext成为临时的。但我有一种感觉,这样做会为自己带来全新的问题。有人能帮我吗?

什么是确切的异常? - mjwills
在执行 _context.SaveChanges() 失败时,会抛出 DbUpdateException 异常。 - Navjot Singh
1个回答

2
保存上下文时可能会出现至少两种错误。第一种发生在命令执行期间。第二种发生在提交期间(这种情况非常罕见)。
即使数据已经成功更新,第二种错误也可能发生。因此,您的代码只处理了第一种错误,而没有考虑第二种错误。
对于第一种错误,您可以将DbContext工厂注入到命令处理程序中,或直接使用IServiceProvider。虽然这有点反模式,但在这种情况下我们别无选择,像这样:
readonly IServiceProvider _serviceProvider;
public EditPersonalInfoCommandHandler(IServiceProvider serviceProvider) {
        _serviceProvider = serviceProvider;
}

for(int i = 0; i < 5; i++) {
  try {
    using var dbContext = _serviceProvider.GetRequiredService<AppDbContext>();
    //consume the dbContext
    //required logic
  } catch(SomeDatabaseException e) {
    if(i == 4) {
       throw;
    }
  }
}

但是如我所说,为了处理这两种类型的错误,我们应该在EFCore中使用所谓的IExecutionStrategy。 这里有几个选项,如此处所介绍的。 但我认为以下内容最适合您的情况:

public Result Handle(EditPersonalInfoCommand command) {
    var strategy = _context.Database.CreateExecutionStrategy();
    
    var studentRepo = new StudentRepository(_context);
    Student student = studentRepo.GetById(command.Id);
    if (student == null) {
        return Result.Failure($"No Student found for Id {command.Id}");
    }

    student.Name = command.Name;
    student.Email = command.Email;
    const int maxRetries = 5;
    int retries = 0;
    strategy.ExecuteInTransaction(_context,
                                  context => {
                                      if(++retries > maxRetries) {
                                         //you need to define your custom exception to be used here
                                         throw new CustomException(...);
                                      }
                                      context.SaveChanges(acceptAllChangesOnSuccess: false);
                                  },
                                  context => context.Students.AsNoTracking()
                                                    .Any(e => e.Id == command.Id && 
                                                              e.Name == command.Name &&
                                                              e.Email == command.Email));

    _context.ChangeTracker.AcceptAllChanges();
    return Result.Success();
}

请注意,我假设你的上下文通过属性Students公开了一个DbSet<Student>。 如果您遇到与连接无关的任何其他错误,则IExecutionStrategy将无法处理它,这是有道理的。因为这时需要修复逻辑,重试成千上万次都不会有帮助,最终仍然会出现该错误。这就是为什么我们不需要关心详细的原始异常(当我们使用IExecutionStrategy时未公开)。相反,我们使用自定义异常(如我的代码中所述)来通知由于某些连接相关问题而无法保存更改的失败情况。

ExecuteInTransaction 方法中,针对每次重试是否都会内部处理创建新的 context(考虑到context仍然作为作用域依赖项添加)? 并且返回 AsNoTracking() 有具体的原因吗? - Navjot Singh
@NavjotSingh ExecuteInTransaction 会为您处理一切,此处使用相同的上下文(每次重试不会创建新实例),因为我们没有使用工厂作为使用 IServiceProvider 的第一种方法。使用 AsNoTracking 可以获得最佳查询性能,仅查询数据(这里是用于验证更新成功的查询)。 - King King
@KingKing 如果我有一个正在使用仓储模式的项目,并且我想将其应用于整个项目,是否可以重写 dbcontext 类? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接