/// <summary> /// Updates an object; returns an updated object or null if the object does not exist /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="TAccount"></typeparam> /// <param name="dbCtx"></param> /// <param name="userAccountService"></param> /// <param name="uuid"></param> /// <returns></returns> protected internal virtual async Task <T> UpdateAsync <T, TAccount>(DbContext dbCtx, UserAccountService <TAccount> userAccountService, Guid uuid) where T : MapHiveUserBase where TAccount : RelationalUserAccount { T output; //reassign guid - it usually comes through rest api, so not always present on the object itself //this is usually done at the base lvl, but need proper id for the user validation! Uuid = uuid; //need to validate the model first await ValidateAsync(dbCtx); //make sure the email is ALWAYS lower case Email = Email.ToLower(); //Note: user account resides in two places - MembershipReboot and the MapHive metadata database. //therefore need t manage it in tow places and obviously make sure the ops are properly wrapped into transactions //first get the user as saved in the db var mbrUser = userAccountService.GetByID(uuid); if (mbrUser == null) { throw new BadRequestException(string.Empty); } //in order to check if some mbr ops are needed need to compare the incoming data with the db equivalent var currentStateOfUser = await ReadAsync <T>(dbCtx, uuid); //work out if email is being updated and make sure to throw if it is not possible! var updateEmail = false; if (currentStateOfUser.Email != Email) { //looks like email is about to be changed, so need to check if it is possible to proceed var mbrUserWithSameEmail = userAccountService.GetByEmail(Email); if (mbrUserWithSameEmail != null && mbrUserWithSameEmail.ID != uuid) { throw Validation.Utils.GenerateValidationFailedException(nameof(Email), ValidationErrors.EmailInUse); } //looks like we're good to go. updateEmail = true; } DbContext mbrDbCtx = GetMembershipRebootDbCtx(userAccountService); System.Data.Common.DbTransaction mbrTrans = null; System.Data.Common.DbTransaction mhTransaction = null; //since this method wraps the op on 2 dbs into transactions, it must handle connections manually and take care of closing it aftwerwards //it is therefore required to clone contexts with independent conns so the base contexts can be reused var clonedMhDbCtx = dbCtx.Clone(contextOwnsConnection: false); var clonedMbrDbCtx = mbrDbCtx.Clone(false); try { //open the connections as otherwise will not be able to begin transaction await clonedMbrDbCtx.Database.Connection.OpenAsync(); await clonedMhDbCtx.Database.Connection.OpenAsync(); //begin the transaction and set the transaction object back on the db context so it uses it //do so for both contexts - mbr and mh mbrTrans = clonedMbrDbCtx.Database.Connection.BeginTransaction(); clonedMbrDbCtx.Database.UseTransaction(mbrTrans); mhTransaction = clonedMhDbCtx.Database.Connection.BeginTransaction(); clonedMhDbCtx.Database.UseTransaction(mhTransaction); //check if mbr email related work is needed at all... if (updateEmail) { //Note: //since the change comes from the user edit, can assume this is an authorised operation... userAccountService.SetConfirmedEmail(uuid, Email); } //also check the IsAccountClosed, as this may be modified via update too, not only via Destroy //btw. destroy just adjust the model's property and delegates the work to update if (currentStateOfUser.IsAccountClosed != IsAccountClosed) { if (IsAccountClosed) { userAccountService.CloseAccount(uuid); } else { userAccountService.ReopenAccount(uuid); } } //check the account verification status if (!mbrUser.IsAccountVerified) { if (IsAccountVerified) { userAccountService.SetConfirmedEmail(uuid, Email); } } else { //force one way changes only IsAccountVerified = true; } //mbr work done, so can update the user within the mh metadata db output = await base.UpdateAsync <T>(clonedMhDbCtx, uuid); //looks like we're good to go, so can commit mbrTrans.Commit(); mhTransaction.Commit(); } catch (Exception ex) { mbrTrans?.Rollback(); mhTransaction?.Rollback(); throw Validation.Utils.GenerateValidationFailedException(ex); } finally { //try to close the connections as they were opened manually and therefore may not have been closed! clonedMhDbCtx.Database.Connection.CloseConnection(dispose: true); clonedMbrDbCtx.Database.Connection.CloseConnection(dispose: true); mbrTrans?.Dispose(); mhTransaction?.Dispose(); } return(output); }