Perform multiple Identity actions in a transaction

73 Views Asked by At

I want to perform multiple Identity-related actions in a transaction. There is some guidance in the docs for EF in general, but not for Identity in particular.

This approach is used in many SO posts:

using var transaction = await _context.Database.BeginTransactionAsync();
try
{
  var result1 = await _userManager.Foo();
  if (!result1.Succeeded) throw new Exception("Could not foo.");

  var result2 = await _userManager.Bar();
  if (!result2.Succeeded) throw new Exception("Could not bar.");

  await transaction.CommitAsync();
}
catch (Exception e)
{
  _logger.LogError(e, "Something bad happened, but changes were rolled back.");
  // ...handle error
}

Ordinarily, without Identity, if execution enters the catch block then the transaction is not committed and is disposed (and thus automatically rolled back).

But:

  • Identity calls SaveChanges after every action, so at face value it seems like the rollback would accomplish nothing.
  • I don't know whether _context is the same one used internally by _userManager? I think the context is registered by default as scoped (per request), so I assume that within an ASP.NET Core HTTP request, the same context will be used by both Identity and my code.

Is that correct?

UPDATE:

I can't find definitive documentation for these issues, and am uncomfortable relying on internal implementation details for something so critical. So I opened a docs issue on the repo, asking for official guidance. Please upvote it if this concerns you too.

Update: they closed that docs request without proper consideration. So I opened another on the main repo. Please upvote that.

2

There are 2 best solutions below

2
mehdo On

Your understanding is correct. In ASP.NET Core, the default behavior is to register the DbContext as scoped, meaning that within the same HTTP request, the same instance of the context will be shared across all components that participate in that request's scope, including the _userManager.

So, in your code, both _context and _userManager will typically share the same instance of the DbContext within the same HTTP request. Therefore, using a transaction to perform multiple actions involving both _context and _userManager should work as expected.

Your code looks fine in terms of using transactions to perform multiple identity-related actions within a single unit of work. However, there are a couple of points to consider for improvement:

  1. Error Handling: Your error handling approach is good, as it ensures that if any of the actions fail, the transaction is rolled back. However, throwing a generic Exception may not provide enough information about what exactly went wrong. It's a good practice to use more specific exception types or to provide more context about the error.

  2. Async/Await Pattern: You're using async/await pattern correctly, which is good for performance and scalability in ASP.NET Core applications.

  3. Logging: You're logging errors, which is important for diagnosing issues. However, make sure to log enough information to facilitate troubleshooting.

  4. Dependency Injection: Ensure that _userManager and _context are properly registered and injected into your class. It seems you're doing it correctly, but it's worth mentioning.

Overall, your approach seems solid for performing multiple identity-related actions within a transaction in ASP.NET Core. Just make sure to handle errors effectively and provide enough logging and context for debugging purposes.

2
sa-es-ir On

Short answer: Yes, it's the same instance of DbContext for all subsequent services within a request, you may already faced this error The instance of entity type cannot be tracked because another instance with the same key value for { 'ID'} is already being tracked, this happens if you try to get same entity twice in the same scope (of course without using AsNotTracking).

Internal Transaction in EF Core is about how you call the SaveChanges method, EF opens a transaction when you want to write something into the database like insert/update/delete, when you call the SaveChanges method EF will wrap all operations in a transaction so if any exception raised then they will be rolled back.

But in your scenario, it depends on what you're doing in the Foo and Bar methods, imagine without using Transaction: if you are writing something in those methods means for each you call SaveChanges separately so if the Foo ran successfully and Bar failed, EF can't roll back the Foo changes because the transaction is already committed but Bar changes will roll back.

Based on the docs you already shared:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
//....

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

The ApplicationDbContextis registered by calling AddDbContext but AddEntityFrameworkStores method is only referring to it because it needs to generate migration and seeding data. another point here is Transactions is applying on the database side, so because of having one DbContext instance per request and pointing to the same database (connectionString) then still I would say your approach is right and safe.

Worth checking the code here as well: https://github.com/dotnet/aspnetcore/blob/main/src/Identity/EntityFrameworkCore/src/IdentityEntityFrameworkBuilderExtensions.cs