简体   繁体   中英

Do I really need to create a new transaction here?

I'm sending an email from an MVC controller.

[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");
}

Here are two questions: 1. Do I really need a transaction here? Wouldn't using _dbContext.SaveChanges() be enough in this case? I used a transaction to revert everything back in case SendFeedbackEmailAsync failed and no email sent.

  1. transaction.Commit() doesn't seem to be updating thisUser.FeedbackSendLimit . Should I retrieve the user in the transaction using block to get it working?

Technologies:

  • Entity Framework 6.0
  • ASP.NET MVC 5

Do you need the explicit transaction: No. If your Try block completes and calls SaveChanges, the changes will be committed as one effective transaction. If the exception gets caught, no SaveChanges happens so the transaction is rolled back when the Context is disposed.

Why your User change isn't saved? This is most likely because your user was loaded by a different DbContext instance in the GetThisApplicationUserAsync() method, or was loaded AsNoTracking() or otherwise detached.

When retrieving data and performing updates, do it within the scope of a single DbContext instance.

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();
  //....

From there when the Context SaveChanges is called, that user is tracked by the Context and will be persisted. The uglier way to deal with it is to check if the thisUser is tracked by the context (no) or a user with the same PK is tracked by the context (no, if a new DbContext) and if not, Attach the user to that context, however the user instance needs to be first detached from any DbContext instance it may still be attached to. Messy.

I'm not a fan of initializng a module-level DbContext but rather ensuring instances are instantiated and disposed in the scope they are needed. Module level contexts make it harder to predict method chains where changes may be inadvertently saved when some method decides to call SaveChanges, and leads to odd placement of explicit transactions and such to try and normalize behaviour. Using blocks make that a lot easier. If you want to DI a context then I recommend considering either a Repository pattern and/or a DbContextFactory/UnitOfWork dependency to enable mocking the results (Repository) or mocking the DbContext (Factory).

My go-to pattern is Repository (non-generic) /w the DbContextScopeFactory/Locator pattern for Unit of Work by Mehdime.

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