/// <summary>
        /// Tries to renew the registration session. This method locks tables
        /// during processing. After returning, you must check the <see cref="RegistrationSession.RegistrationCount"/>
        /// property to ensure that enough spots still exist. This can happen if
        /// the session was expired but was able to be partially renewed.
        /// </summary>
        /// <param name="sessionGuid">The session unique identifier.</param>
        /// <returns>The <see cref="RegistrationSession"/> that was renewed or <c>null</c> if it could not be found.</returns>
        public static RegistrationSession TryToRenewSession(Guid sessionGuid)
        {
            using (var rockContext = new RockContext())
            {
                var registrationSessionService          = new RegistrationSessionService(rockContext);
                var registrationService                 = new RegistrationService(rockContext);
                RegistrationSession registrationSession = null;

                var wasRenewed = 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)");

                    // Try to find the session to renew, if we can't find it then
                    // return a failure indication.
                    registrationSession = registrationSessionService.Get(sessionGuid);

                    if (registrationSession is null)
                    {
                        return(false);
                    }

                    // Attempt to get the context that describes the registration
                    // and the number of available slots.
                    var context = registrationService.GetRegistrationContext(registrationSession.RegistrationInstanceId, out var errorMessage);

                    if (errorMessage.IsNotNullOrWhiteSpace())
                    {
                        return(false);
                    }

                    // Check if the session has expired already before we set the new
                    // expiration window.
                    var wasExpired = registrationSession.ExpirationDateTime < RockDateTime.Now;

                    // Set the new expiration
                    registrationSession.ExpirationDateTime = context.RegistrationSettings.TimeoutMinutes.HasValue
                        ? RockDateTime.Now.AddMinutes(context.RegistrationSettings.TimeoutMinutes.Value)
                        : RockDateTime.Now.AddDays(1);

                    // If the session was expired then the number of reserved spots
                    // might no longer be valid. Check if there are fewer spots
                    // actually available and update the count.
                    if (wasExpired && context.SpotsRemaining.HasValue)
                    {
                        if (context.SpotsRemaining.Value < registrationSession.RegistrationCount)
                        {
                            registrationSession.RegistrationCount = context.SpotsRemaining.Value;
                        }
                    }

                    // Persist the new session information to the database.
                    rockContext.SaveChanges();

                    return(true);
                });

                return(wasRenewed ? registrationSession : null);
            }
        }
        /// <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);
            }
        }