How to create transaction with asp.net identity?

2019-02-06 19:00发布

问题:

I am doing registration on which i am asking for 5 things:

FullName,EmailId,Password,ContactNumber,Gender

Now emailid and password i am storing with register method and given in below two link:

public async Task<ActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = new ApplicationUser { UserName = model.Email, Email = model.Email };

            using var context = new MyEntities())
            {
                using (var transaction = context.Database.BeginTransaction())
                {
                    try
                    {
                        var DataModel = new UserMaster();
                        DataModel.Gender = model.Gender.ToString();
                        DataModel.Name = string.Empty;
                        var result = await UserManager.CreateAsync(user, model.Password);//Doing entry in AspnetUser even if transaction fails
                        if (result.Succeeded)
                        {
                            await this.UserManager.AddToRoleAsync(user.Id, model.Role.ToString());
                            this.AddUser(DataModel, context);
                            transaction.Commit();
                            return View("DisplayEmail");
                        }
                        AddErrors(result);
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                        return null;
                    }

                }
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

public int AddUser(UserMaster _addUser, MyEntities _context)
    {
        _context.UserMaster.Add(_addUser);           
        _context.SaveChanges();
        return 0;
    }

Now with this below 2 lines:

var result = await UserManager.CreateAsync(user, model.Password);//entry is done in AspnetUsers table.
await this.UserManager.AddToRoleAsync(user.Id, model.Role.ToString());//entry is done is Aspnetuserrole table

Now this Fullname,contactno,gender i am having in another table that is UserMaster.

So when i will submit my registration form i will save this details in UserMaster and AspnetUsers,AspnetUserinrole table.

But consider if there any problem occurs while saving record in UserMaster then i dont want to save entry in Aspnetuser and Aspnetuserinrole too.

I would like to create a transaction where i would rollback if any problem occurs during saving any record in any table i.e no entry should be done in AspnetUser,AspnetUserinRole nd userMaster.

Records should be saved successfully only if there is no problem in saving record in this 3 tables otherwise whiole transaction should be role back.

I am using Microsoft.AspNet.Identity for login,Register,role management and other and following this tutorial:

http://www.asp.net/mvc/overview/security/create-an-aspnet-mvc-5-web-app-with-email-confirmation-and-password-reset

http://www.asp.net/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

But as await UserManager.CreateAsync and UserManager.AddToRoleAsync method are built in method how would i synchonize it to work with entity framework.

So can anybody guide me how to create such transaction or anything that would solve this?

IdentityConfig:

public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
        {
            var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<ApplicationUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };

            // Configure user lockout defaults
            manager.UserLockoutEnabledByDefault = true;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;

            // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
            // You can write your own provider and plug it in here.
            manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
            {
                MessageFormat = "Your security code is {0}"
            });
            manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
            {
                Subject = "Security Code",
                BodyFormat = "Your security code is {0}"
            });
            manager.EmailService = new EmailService();
            manager.SmsService = new SmsService();
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = 
                    new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
            }
            return manager;
        }
    }

    // Configure the application sign-in manager which is used in this application.
    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
            : base(userManager, authenticationManager)
        {
        }

        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }

        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
        {
            return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
        }
    }

回答1:

You should not create a new db context, but use the existing one.

var context = Request.GetOwinContext().Get<MyEntities>()

It is created per request if you use default implementation.

app.CreatePerOwinContext(ApplicationDbContext.Create);

Update:

OK, since you are using two different contexts your code will look something like this:

public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };

        var appDbContext = HttpContext.GetOwinContext().Get<ApplicationDbContext>();
        using( var context = new MyEntities())
        using (var transaction = appDbContext.Database.BeginTransaction())
        {
            try
            {
                var DataModel = new UserMaster();
                DataModel.Gender = model.Gender.ToString();
                DataModel.Name = string.Empty;

                // Doing entry in AspnetUser even if transaction fails
                var result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    await this.UserManager.AddToRoleAsync(user.Id, model.Role.ToString());
                    this.AddUser(DataModel, context);
                    transaction.Commit();
                    return View("DisplayEmail");
                }
                AddErrors(result);
            }
            catch (Exception ex)
            {
                transaction.Rollback();
                return null;
            }
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

public int AddUser(UserMaster _addUser, MyEntities _context)
{
    _context.UserMaster.Add(_addUser);
    _context.SaveChanges();
    return 0;
}

Here, appDbContext is the same context that is used by UserManager.



回答2:

You can solve it with TransactionScope class:

using (TransactionScope scope = new TransactionScope())
{
    var result = await UserManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        await this.UserManager.AddToRoleAsync(user.Id, model.Role.ToString());
        string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");
        return View("DisplayEmail");
    }
    scope.Complete();
}

So, both actions will be done in one transaction and if method Comlete does not call, both actions will be canceled (roolback).

If you want to solve it with EF only (without TransactionScope), you need to refactor your code. I don't know implementation of class UserManager and methods CreateAsync and AddToRoleAsync, but I guess that they creates new DBContext for each operation. So, first of all, for all transactional operations you need one DBContext (for EF solution). If you add this methods, I'll modify my answer according to EF solution.