首页 > 解决方案 > 我真的需要在这里创建一个新的交易吗?

问题描述

我正在从 MVC 控制器发送电子邮件。

[HttpPost]
public async Task<ActionResult> Send(SendModel model)
{
    var userId = HttpContext.User.Identity.GetUserId();
    // This method takes current user ID and retrieves the user from the DB
    var thisUser = await GetThisApplicationUserAsync();

    if (thisUser.FeedbackSendLimit <= 0) return RedirectToActionWithMessage(MessageType.Error, "You can't send emails anymore! You have exceeded your send limit!", "Send");

    // Handling the case when the passed model is invalid
    if (!ModelState.IsValid) return View(model);

    using (var transaction = _dbContext.Database.BeginTransaction())
    {
        // _dbContext is a DbContext initialized in the controller's constructor
        var companiesToSend = _dbContext
            .Companies
            .Where(x => model.CompanyIds.Contains(x.Id))
            .ToArray();

        try
        {
            // Each user has a limit of emails they can send monthly
            thisUser.FeedbackSendLimit -= 1;

            // Each company has a limit of how many emails we can address them as well
            foreach (var company in companiesToSend)
            {
                company.FeedbackCounter -= 1;
            }

            var newSend = new FeedbackSend
            {
                Id = Guid.NewGuid(),
                UserId = userId,
                SentAt = DateTime.Now,
                Companies = companiesToSend
            };
            _dbContext.FeedbackSends.Add(newSend);

            await _dbContext.SaveChangesAsync();

            // This generates an HTML email and sends it to specified email address
            await SendFeedbackEmailAsync(model.ToEmail, thisUser, companiesToSend);

            transaction.Commit();
        }
        catch (Exception e)
        {
            transaction.Rollback();

            return RedirectToActionWithMessage(MessageType.Error, "An error occurred while trying to send feedback", "Send");
        }
    }

    return RedirectToActionWithMessage(MessageType.Success, "Sent successfully", "Send");
}

这里有两个问题: 1. 我真的需要在这里交易吗?在这种情况下使用_dbContext.SaveChanges()还不够吗?我使用事务将所有内容还原,以防万一SendFeedbackEmailAsync失败且未发送电子邮件。

  1. transaction.Commit()好像没有更新thisUser.FeedbackSendLimit。我应该在事务using块中检索用户以使其正常工作吗?

技术:

标签: c#asp.net.netasp.net-mvcentity-framework

解决方案


您是否需要显式事务:不需要。如果您的 Try 块完成并调用 SaveChanges,则更改将作为一个有效事务提交。如果异常被捕获,则不会发生 SaveChanges,因此在处置 Context 时事务将回滚。

为什么您的用户更改未保存?这很可能是因为您的用户是由 GetThisApplicationUserAsync() 方法中的不同 DbContext 实例加载的,或者被加载为 AsNoTracking() 或以其他方式分离。

检索数据和执行更新时,请在单个 DbContext 实例的范围内进行。

using (var context = new MyDbContext())
{
    var thisUser = context.Users.Where(x => x.Id == userId).Single();
    var companiesToSend = context.Companies
        .Where(x => model.CompanyIds.Contains(x.Id))
        .ToArray();
  //....

从那里调用 Context SaveChanges 时,该用户被 Context 跟踪并将被持久化。处理它的更丑陋的方法是检查 thisUser 是否被上下文跟踪(否)或者具有相同 PK 的用户是否被上下文跟踪(否,如果是新的 DbContext),如果不是,则将用户附加到那个上下文,但是用户实例需要首先与它可能仍附加到的任何 DbContext 实例分离。凌乱。

我不喜欢初始化模块级 DbContext,而是确保实例在所需范围内被实例化和处理。模块级上下文使得预测方法链变得更加困难,当某些方法决定调用 SaveChanges 时,可能会无意中保存更改,并导致显式事务的奇怪放置等尝试和规范化行为。使用块使这更容易。如果您想 DI 上下文,那么我建议考虑使用存储库模式和/或 DbContextFactory/UnitOfWork 依赖项来启用模拟结果(存储库)或模拟 DbContext(工厂)。

我的首选模式是 Repository (non-generic) /w Mehdime 工作单元的 DbContextScopeFactory/Locator 模式。


推荐阅读