/// <summary> /// Creates or update a registration session. This method operates /// inside a database lock to prevent other sessions from being /// modified at the same time. /// </summary> /// <param name="sessionGuid">The session unique identifier.</param> /// <param name="createSession">The method to call to get a new <see cref="RegistrationSession"/> object that will be persisted to the database.</param> /// <param name="updateSession">The method to call to update an existing <see cref="RegistrationSession"/> object with any new information.</param> /// <param name="errorMessage">The error message.</param> /// <returns>The <see cref="RegistrationSession"/> that was created or updated; or <c>null</c> if an error occurred.</returns> public static RegistrationSession CreateOrUpdateSession(Guid sessionGuid, Func <RegistrationSession> createSession, Action <RegistrationSession> updateSession, out string errorMessage) { using (var rockContext = new RockContext()) { var registrationSessionService = new RegistrationSessionService(rockContext); RegistrationSession registrationSession = null; string internalErrorMessage = null; rockContext.WrapTransactionIf(() => { /* * This is an anti-pattern. Do not just copy this and use it * as a pattern elsewhere. Discuss with the team before trying * to use this anywhere else. * * This single use-case was discussed between Jon and Daniel on * 9/17/2021 and deemed the best choice for the moment. In the * future we might convert this to a helper method that doesn't * require custom SQL each place it is used - but we really * shouldn't do full table locks as a matter of practice. * * Daniel Hazelbaker 9/17/2021 */ // Initiate a full table lock so nothing else can query data, // otherwise they might get a count that will no longer be // valid after our transaction is committed. rockContext.Database.ExecuteSqlCommand("SELECT TOP 1 Id FROM [RegistrationSession] WITH (TABLOCKX, HOLDLOCK)"); var registrationService = new RegistrationService(rockContext); // Load the registration session and determine if it was expired already. registrationSession = registrationSessionService.Get(sessionGuid); var wasExpired = registrationSession != null && registrationSession.ExpirationDateTime < RockDateTime.Now; var oldRegistrationCount = registrationSession?.RegistrationCount ?? 0; // If the session didn't exist then create a new one, otherwise // update the existing one. if (registrationSession == null) { registrationSession = createSession(); registrationSessionService.Add(registrationSession); } else { updateSession(registrationSession); } // Get the context information about the registration, specifically // the timeout and spots available. var context = registrationService.GetRegistrationContext(registrationSession.RegistrationInstanceId, out internalErrorMessage); if (internalErrorMessage.IsNotNullOrWhiteSpace()) { return(false); } // Set the new expiration date. registrationSession.ExpirationDateTime = context.RegistrationSettings.TimeoutMinutes.HasValue ? RockDateTime.Now.AddMinutes(context.RegistrationSettings.TimeoutMinutes.Value) : RockDateTime.Now.AddDays(1); // Determine the number of registrants. If the registration was // expired then we need all spots requested again. Otherwise we // just need to be able to reserve the number of new spots since // the last session save. var newRegistrantCount = wasExpired ? registrationSession.RegistrationCount : (registrationSession.RegistrationCount - oldRegistrationCount); // Handle the possibility that there is a change in the number of // registrants in the session. if (context.SpotsRemaining.HasValue && context.SpotsRemaining.Value < newRegistrantCount) { internalErrorMessage = "There is not enough capacity remaining for this many registrants."; return(false); } rockContext.SaveChanges(); internalErrorMessage = string.Empty; return(true); }); errorMessage = internalErrorMessage; return(errorMessage.IsNullOrWhiteSpace() ? registrationSession : null); } }
/// <summary> /// Handles the SaveClick event of the mdSendTest control. /// </summary> protected void mdSendTest_SaveClick(object sender, EventArgs e) { const bool DISABLE_PERSON_HISTORY = true; string currentEmail = CurrentPerson.Email; using (var rockContext = new RockContext()) { rockContext.WrapTransactionIf(() => { try { var mergeInfo = BuildSystemCommunication(); var rockEmailMessage = mergeInfo.RockEmailMessageRecord; if (rockEmailMessage == null) { throw new Exception($"A valid system communication was not selected."); } if (rockEmailMessage.SystemCommunicationId.GetValueOrDefault(0) == 0) { throw new Exception($"The system communication specified is not valid."); } var emailPerson = GetTargetPerson(rockContext); // Remove the lava debug command if it is specified in the message template. var message = rockEmailMessage.Message.Replace(PageConstants.LavaDebugCommand, string.Empty); rockEmailMessage.AdditionalMergeFields = mergeInfo.MergeFields.ToDictionary(k => k.Key, v => ( object )v.Value); rockEmailMessage.CurrentPerson = emailPerson; rockEmailMessage.Message = message; var sendErrorMessages = new List <string>(); // Set person email to the email specified in the dialog emailPerson.Email = ebSendTest.Text; rockContext.SaveChanges(disablePrePostProcessing: DISABLE_PERSON_HISTORY); var recipient = new RockEmailMessageRecipient(emailPerson, mergeInfo.MergeFields); rockEmailMessage.AddRecipient(recipient); rockEmailMessage.Send(out sendErrorMessages); if (sendErrorMessages.Count == 0) { nbSendTest.Text = $"Email submitted to <i>{recipient.EmailAddress}</i>"; nbSendTest.NotificationBoxType = NotificationBoxType.Info; nbSendTest.Visible = true; } else { var errorString = $"<ul>[ERRORS]</ul>"; var sbError = new StringBuilder(); foreach (var error in sendErrorMessages) { sbError.AppendLine($"<li>{error}</li>"); } errorString = errorString.Replace("[ERRORS]", sbError.ToString()); nbSendTest.Text = errorString; nbSendTest.NotificationBoxType = NotificationBoxType.Danger; nbSendTest.Visible = true; } // Restore email to original email address emailPerson.Email = currentEmail; rockContext.SaveChanges(disablePrePostProcessing: DISABLE_PERSON_HISTORY); } catch (Exception ex) { nbSendTest.Text = ex.Message; nbSendTest.NotificationBoxType = NotificationBoxType.Danger; nbSendTest.Visible = true; return(false); } return(true); }); // End transaction } }