public async Task DeleteTokenAsync()
        {
            // we cannot delete a code which has not yet been created
            if (_id == null)
            {
                return;
            }

            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            int RESULT_KEY_CONFLICT = -1;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();

            //int iArgument = 1;
            // if the code has already been deleted, return success
            luaBuilder.Append(
                "if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n" +
                "  return 1\n" +
                "end\n");
            //
            luaBuilder.Append(
                "redis.call(\"DEL\", KEYS[1])\n");
            //
            luaBuilder.Append("return 1\n");

            long          luaResult = 0;
            List <string> keys      = new List <string>();

            keys.Add(REDIS_PREFIX_OAUTH2CODE + REDIS_PREFIX_SEPARATOR + _id);
            luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

            // NOTE: the result will contain a negative integer (error) or positive one (success)
            if (luaResult == 1)
            {
                // reset our server-assigned values
                _id = null;
            }
            else if (luaResult == RESULT_KEY_CONFLICT)
            {
                // key name conflict; abort
                return;
            }
            else
            {
                // unknown error
                throw new Exception("Critical Redis error!");
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }
        public static async Task <OAuth2AuthorizationCode> LoadAuthCodeAsync(string authCodeId, LoadAuthCodeOptions options)
        {
            if ((options & LoadAuthCodeOptions.SearchLocal) == LoadAuthCodeOptions.SearchLocal)
            {
                if (_redisClient == null)
                {
                    _redisClient = await Singletons.GetRedisClientAsync();
                }

                string fullyQualifiedAuthCodeKey = REDIS_PREFIX_OAUTH2CODE + REDIS_PREFIX_SEPARATOR + authCodeId;
                bool   localAuthCodeExists       = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedAuthCodeKey }) > 0);
                if (localAuthCodeExists)
                {
                    Dictionary <string, string> authCodeDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedAuthCodeKey);

                    long expiresInMilliseconds = await _redisClient.PttlAsync(fullyQualifiedAuthCodeKey);

                    string clientId  = authCodeDictionary.ContainsKey("client-id") ? authCodeDictionary["client-id"] : null;
                    string accountId = authCodeDictionary.ContainsKey("account-id") ? authCodeDictionary["account-id"] : null;
                    if (accountId == null)
                    {
                        return(null);
                    }
                    string userId      = authCodeDictionary.ContainsKey("user-id") ? authCodeDictionary["user-id"] : null;
                    string redirectUri = authCodeDictionary.ContainsKey("redirect-uri") ? authCodeDictionary["redirect-uri"] : null;

                    // get "is-used" value (which, when present, indicates that the authorization code has already been submitted to the token endpoint).
                    bool isUsed = authCodeDictionary.ContainsKey("is-used");
                    // if the code is used, it may also have already been assigned a token-id; we store this in case the code is compromised before it expires (i.e. and we need to revoke the token).
                    string tokenId = authCodeDictionary.ContainsKey("token-id") ? authCodeDictionary["token-id"] : null;

                    OAuth2AuthorizationCode result = new OAuth2AuthorizationCode();
                    result._id          = authCodeId;
                    result._clientId    = clientId;
                    result._accountId   = accountId;
                    result._userId      = userId;
                    result._redirectUri = redirectUri;
                    result._isUsed      = isUsed;
                    result._tokenId     = tokenId;
                    if (expiresInMilliseconds >= 0)
                    {
                        result._expirationTime = DateTimeOffset.UtcNow.AddMilliseconds(expiresInMilliseconds);
                    }

                    return(result);
                }
            }

            // valid auth code could not be found
            return(null);
        }
        public static async Task <OAuth2InitialAccessToken> LoadInitialAccessTokenAsync(string tokenId, LoadTokenOptions options)
        {
            if ((options & LoadTokenOptions.LocalTokens) == LoadTokenOptions.LocalTokens)
            {
                if (_redisClient == null)
                {
                    _redisClient = await Singletons.GetRedisClientAsync();
                }

                string fullyQualifiedTokenKey = REDIS_PREFIX_OAUTH2_TOKEN + REDIS_PREFIX_SEPARATOR + tokenId;
                bool   localTokenExists       = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedTokenKey }) > 0);
                if (localTokenExists)
                {
                    Dictionary <string, string> tokenDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedTokenKey);

                    string tokenType = tokenDictionary.ContainsKey("type") ? tokenDictionary["type"] : null;
                    if (tokenType == null || tokenType != TOKEN_TYPE_INITIAL_ACCESS_TOKEN)
                    {
                        return(null);
                    }

                    string tokenIsCachedAsString = tokenDictionary.ContainsKey("cached") ? tokenDictionary["cached"] : null;
                    bool   tokenIsCached         = (tokenIsCachedAsString != null && tokenIsCachedAsString != "0");

                    string timeCreatedAsString           = tokenDictionary.ContainsKey("time-created") ? tokenDictionary["time-created"] : null;
                    Int64? timeCreatedInUnixMicroseconds = null;
                    Int64  timeCreatedAsInt64;
                    if (timeCreatedAsString != null && Int64.TryParse(timeCreatedAsString, out timeCreatedAsInt64))
                    {
                        timeCreatedInUnixMicroseconds = timeCreatedAsInt64;
                    }

                    string timeUpdatedAsString           = tokenDictionary.ContainsKey("time-updated") ? tokenDictionary["time-updated"] : null;
                    Int64? timeUpdatedInUnixMicroseconds = null;
                    Int64  timeUpdatedAsInt64;
                    if (timeUpdatedAsString != null && Int64.TryParse(timeUpdatedAsString, out timeUpdatedAsInt64))
                    {
                        timeUpdatedInUnixMicroseconds = timeUpdatedAsInt64;
                    }

                    OAuth2InitialAccessToken resultToken = new OAuth2InitialAccessToken();
                    resultToken._softwareId = tokenDictionary.ContainsKey("software-id") ? tokenDictionary["software-id"] : null;
                    if (resultToken._softwareId == null)
                    {
                        return(null);
                    }
                    resultToken._accountId = tokenDictionary.ContainsKey("account-id") ? tokenDictionary["account-id"] : null;

                    // if our result token could be loaded, populate the default fields common to all OAuth2Tokens.
                    resultToken._id = tokenId;
                    ParsingHelper.ServerDetails?loginServerDetails = ParsingHelper.ExtractServerDetailsFromAccountServerIdIdentifier(tokenId);
                    if (loginServerDetails == null)
                    {
                        throw new Exception();
                    }
                    resultToken._loginServerDetails            = loginServerDetails.Value;
                    resultToken._isCached                      = tokenIsCached;
                    resultToken._timeCreatedInUnixMicroseconds = timeCreatedInUnixMicroseconds;
                    resultToken._timeUpdatedInUnixMicroseconds = timeUpdatedInUnixMicroseconds;

                    return(resultToken);
                }
            }

            // valid token could not be found
            return(null);
        }
        public async Task DeleteTokenAsync()
        {
            // we only support saving a local token (i.e. not updating a remote token)
            if (_isCached)
            {
                throw new InvalidOperationException();
            }
            // we cannot delete a token which has not yet been created
            if (_timeCreatedInUnixMicroseconds == null)
            {
                return;
            }

            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            int RESULT_KEY_CONFLICT = -1;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            // if the token has already been deleted, return success
            luaBuilder.Append(
                "if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n" +
                "  return 1\n" +
                "end\n");
            // for deletions: make sure that the "time-created" timestamp has no changed (i.e. that a new key has not replaced the old key)
            luaBuilder.Append("local time_created = redis.call(\"HGET\", KEYS[1], \"time-created\")\n");
            luaBuilder.Append("if time_created ~= ARGV[" + iArgument.ToString() + "] then\n" +
                              "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                              "end\n");
            arguments.Add(_timeCreatedInUnixMicroseconds.ToString());
            iArgument++;
            //
            luaBuilder.Append(
                "redis.call(\"DEL\", KEYS[1])\n");
            //
            // remove the token from corresponding sets (filtered and non-filtered indexes)
            luaBuilder.Append("redis.call(\"SREM\", KEYS[2], ARGV[" + iArgument.ToString() + "])\n");
            luaBuilder.Append("redis.call(\"SREM\", KEYS[3], ARGV[" + iArgument.ToString() + "])\n");
            arguments.Add(_id);
            iArgument++;
            //
            luaBuilder.Append("return 1\n");

            long          luaResult = 0;
            List <string> keys      = new List <string>();

            keys.Add(REDIS_PREFIX_OAUTH2_TOKEN + REDIS_PREFIX_SEPARATOR + _id);
            if (_accountId != null)
            {
                // index of all oauth2tokens for this account
                keys.Add(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS);
                // index of initial access oauth2tokens for this account
                keys.Add(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT);
            }
            else
            {
                // index of all oauth2tokens for the root
                keys.Add(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS);
                // index of initial access oauth2tokens for the root
                keys.Add(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT);
            }
            luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

            // NOTE: the result will contain a negative integer (error) or positive one (success)
            if (luaResult == 1)
            {
                // reset our server-assigned values
                _timeCreatedInUnixMicroseconds = null;
                _timeUpdatedInUnixMicroseconds = null;
                _id       = null;
                _isCached = false;
            }
            else if (luaResult == RESULT_KEY_CONFLICT)
            {
                // key name conflict; abort
                return;
            }
            else
            {
                // unknown error
                throw new Exception("Critical Redis error!");
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }
        public async Task SaveTokenAsync()
        {
            // we only support saving a local token (i.e. not updating a remote token)
            if (_isCached)
            {
                throw new InvalidOperationException();
            }

            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_timeCreatedInUnixMicroseconds == null);

            int RESULT_KEY_CONFLICT       = -1;
            int RESULT_DATA_CORRUPTION    = -2;
            int RESULT_UPDATED_SINCE_LOAD = -3;

            // get current server time
            long newTimeUpdatedInUnixMicroseconds = await _redisClient.TimeAsync();

            if (newTimeUpdatedInUnixMicroseconds < 0)
            {
                throw new Exception("Critical Redis error!");
            }
            if (newTimeUpdatedInUnixMicroseconds < _timeUpdatedInUnixMicroseconds)
            {
                throw new Exception("Critical Redis error!");
            }

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            if (objectIsNew)
            {
                // for new tokens: if a token with this token-id already exists, return 0.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" +
                    "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                    "end\n");
            }
            else
            {
                // for updated: make sure that the "time-created" timestamp has no changed (i.e. that a new key has not replaced the old key)
                luaBuilder.Append("local time_created = redis.call(\"HGET\", KEYS[1], \"time-created\")\n");
                luaBuilder.Append("if time_created ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeCreatedInUnixMicroseconds.ToString());
                iArgument++;

                // for updates: make sure that our old "time-updated" timestamp has not changed
                luaBuilder.Append("local old_time_updated = redis.call(\"HGET\", KEYS[1], \"time-updated\")\n");
                luaBuilder.Append("if old_time_updated ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_UPDATED_SINCE_LOAD.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            if (objectIsNew)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"type\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(TOKEN_TYPE_INITIAL_ACCESS_TOKEN);
                iArgument++;
            }
            //
            if (objectIsNew)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"time-created\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            luaBuilder.Append(
                "if redis.call(\"HSET\", KEYS[1], \"time-updated\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                "end\n");
            arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
            iArgument++;
            //
            if (_softwareId_IsDirty)
            {
                if (_softwareId != null)
                {
                    // if there is a software-id assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"software-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_softwareId);
                    iArgument++;
                }
                else
                {
                    // if the software-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"software-id\")\n");
                }
                // clear the dirty flag
                _softwareId_IsDirty = false;
            }
            //
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account-id assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"account-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_accountId);
                    iArgument++;
                }
                else
                {
                    // if the account-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"account-id\")\n");
                }
                // clear the dirty flag
                _accountId_IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < 1000; iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a new 32-byte (192-bit) token_id
                    _id = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_OAUTH2_TOKEN + REDIS_PREFIX_SEPARATOR + _id);
                luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                // NOTE: the result will contain a negative integer (error) or one (success)
                //if we were able to create a key, break out of this loop; otherwise, try generating new keys up to 1000 times.
                if (luaResult == 1)
                {
                    // save our "time-updated" timestamp
                    _timeUpdatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                    if (objectIsNew)
                    {
                        // save our "time-created" timestamp
                        _timeCreatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                        if (_accountId == null)
                        {
                            // if the token belongs to the entire system (and not to an account), add it to the root token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, new string[] { _id });

                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT, new string[] { _id });
                        }
                        else
                        {
                            // if the token belongs to the account (and not to the user), add it to the account's token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, new string[] { _id });

                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT, new string[] { _id });
                        }
                    }
                    break;
                }
                else if (luaResult == RESULT_KEY_CONFLICT)
                {
                    // key name conflict; try again
                }
                else if (luaResult == RESULT_DATA_CORRUPTION)
                {
                    // data corruption
                    throw new Exception("Critical Redis error!");
                }
                else if (luaResult == RESULT_UPDATED_SINCE_LOAD)
                {
                    // token was updated since we loaded it; we need to reload the token, make the changes again, and then attempt to save it again
                    throw new Exception("Critical Redis error!");
                }
                else
                {
                    // unknown error
                    throw new Exception("Critical Redis error!");
                }
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }
        public async Task <bool> MarkAsUsedAsync()
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_id == null);

            int RESULT_DATA_CORRUPTION = -2;
            int RESULT_DOES_NOT_EXIST  = -4;
            int RESULT_ALREADY_USED    = -5;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            if (objectIsNew)
            {
                // for new authorization codes: if a token with this authcode-id does not already exist, return 0 and we will simply abort.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n" +
                    "  return " + RESULT_DOES_NOT_EXIST.ToString() + "\n" +
                    "end\n");
            }
            // make sure that this authorization code is not already "used"; if it is, abort.
            luaBuilder.Append(
                "if redis.call(\"HEXISTS\", KEYS[1], \"is-used\") == 1 then\n" +
                "  return " + RESULT_ALREADY_USED.ToString() + "\n" +
                "end\n");
            arguments.Add("1");
            iArgument++;
            // mark this authorization code as "used" (which indicates that it has been submitted to the token endpoint)
            luaBuilder.Append(
                "if redis.call(\"HSET\", KEYS[1], \"is-used\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                "end\n");
            arguments.Add("1");
            iArgument++;
            // if the authorization code expired while we were updating the flag (and therefore we RE-CREATED the key), delete the accidentally-just-recreated key
            luaBuilder.Append(
                "if redis.call(\"PTTL\", KEYS[1]) == -1 then\n" +
                "  redis.call(\"DEL\", KEYS[1])\n" +
                "  return " + RESULT_DOES_NOT_EXIST.ToString() + "\n" +
                "end\n");
            //
            luaBuilder.Append("return 1\n");

            List <string> keys = new List <string>();

            keys.Add(REDIS_PREFIX_OAUTH2CODE + REDIS_PREFIX_SEPARATOR + _id);
            long luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

            if (luaResult == 1)
            {
                _isUsed = true;
                return(true);
            }
            else
            {
                return(false);
            }
        }
        public async Task SaveAuthCodeAsync()
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_id == null);

            int RESULT_KEY_CONFLICT    = -1;
            int RESULT_DATA_CORRUPTION = -2;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            if (objectIsNew)
            {
                // for new authorization codes: if a token with this authcode-id already exists, return 0...and we will try again.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" +
                    "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                    "end\n");
            }
            if (_clientId_IsDirty)
            {
                if (_clientId != null)
                {
                    // if there is a client assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"client-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_clientId);
                    iArgument++;
                }
                else
                {
                    // if the client-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"client-id\")\n");
                }
                // clear the dirty flag
                _clientId_IsDirty = false;
            }
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"account-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_accountId);
                    iArgument++;
                }
                else
                {
                    // if the account-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"account-id\")\n");
                }
                // clear the dirty flag
                _accountId_IsDirty = false;
            }
            if (_userId_IsDirty)
            {
                if (_userId != null)
                {
                    // if there is a user assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"user-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_userId);
                    iArgument++;
                }
                else
                {
                    // if the user-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"user-id\")\n");
                }
                // clear the dirty flag
                _userId_IsDirty = false;
            }
            if (_redirectUri_IsDirty)
            {
                if (_redirectUri != null)
                {
                    // if there is a redirect-uri assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"redirect-uri\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_redirectUri);
                    iArgument++;
                }
                else
                {
                    // if the redirect-uri has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"redirect-uri\")\n");
                }
                // clear the dirty flag
                _redirectUri_IsDirty = false;
            }
            // NOTE: when a token is assigned to an authorization code, we re-save the code with the token (in case the same token is reused...in which case we can revoke the already-allocated token)
            if (_tokenId_IsDirty)
            {
                if (_tokenId != null)
                {
                    // if there is a token assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"token-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return 0\n" +
                        "end\n");
                    arguments.Add(_tokenId);
                    iArgument++;
                }
                else
                {
                    // if the token-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"token-id\")\n");
                }
                // clear the dirty flag
                _tokenId_IsDirty = false;
            }
            if (_expirationTime_IsDirty)
            {
                if (_expirationTime != null)
                {
                    double expirationMilliseconds = _expirationTime.Value.Subtract(DateTimeOffset.UtcNow).TotalMilliseconds;
                    if (expirationMilliseconds >= 0)
                    {
                        long expirationMillisecondsAsWholeNumber = (long)expirationMilliseconds;

                        // if there is an expiration time assigned to this authorization code, set it.
                        luaBuilder.Append(
                            "if redis.call(\"PEXPIRE\", KEYS[1], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                            (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                            "  return 0\n" +
                            "end\n");
                        arguments.Add(expirationMillisecondsAsWholeNumber.ToString());
                        iArgument++;
                    }
                }
                else
                {
                    // if the expiration has been removed, delete it.
                    luaBuilder.Append(
                        "if redis.call(\"PERSIST\", KEYS[1]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return 0\n" +
                        "end\n");
                }
                // clear the dirty flag
                _expirationTime_IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < (objectIsNew ? 1000 : 1); iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a 24-byte (144-bit) token_id
                    _id = _authServerId + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(24)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_OAUTH2CODE + REDIS_PREFIX_SEPARATOR + _id);
                luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                //if we were able to create a key, break out of this loop; otherwise, try generating new keys up to ten times.
                if (luaResult == 1)
                {
                    break;
                }
                else if (luaResult == RESULT_KEY_CONFLICT)
                {
                    // key name conflict; try again
                }
                else if (luaResult == RESULT_DATA_CORRUPTION)
                {
                    // data corruption
                    throw new Exception("Critical Redis error!");
                }
                else
                {
                    // unknown error
                    throw new Exception("Critical Redis error!");
                }
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }
        public static async Task <OAuth2Client> LoadClientAsync(string clientId, LoadClientOptions options)
        {
            if ((options & LoadClientOptions.LocalClients) == LoadClientOptions.LocalClients)
            {
                if (_redisClient == null)
                {
                    _redisClient = await Singletons.GetRedisClientAsync();
                }

                string fullyQualifiedClientKey = REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + clientId;
                bool   localClientExists       = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedClientKey }) > 0);
                if (localClientExists)
                {
                    Dictionary <string, string> clientDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedClientKey);

                    string clientIsCachedAsString = clientDictionary.ContainsKey("cached") ? clientDictionary["cached"] : null;
                    bool   clientIsCached         = (clientIsCachedAsString != null && clientIsCachedAsString != "0");

                    string timeCreatedAsString           = clientDictionary.ContainsKey("time-created") ? clientDictionary["time-created"] : null;
                    Int64? timeCreatedInUnixMicroseconds = null;
                    Int64  timeCreatedAsInt64;
                    if (timeCreatedAsString != null && Int64.TryParse(timeCreatedAsString, out timeCreatedAsInt64))
                    {
                        timeCreatedInUnixMicroseconds = timeCreatedAsInt64;
                    }

                    string timeUpdatedAsString           = clientDictionary.ContainsKey("time-updated") ? clientDictionary["time-updated"] : null;
                    Int64? timeUpdatedInUnixMicroseconds = null;
                    Int64  timeUpdatedAsInt64;
                    if (timeUpdatedAsString != null && Int64.TryParse(timeUpdatedAsString, out timeUpdatedAsInt64))
                    {
                        timeUpdatedInUnixMicroseconds = timeUpdatedAsInt64;
                    }

                    OAuth2Client resultClient = new OAuth2Client();
                    resultClient._id = clientId;
                    ParsingHelper.ServerDetails?loginServerDetails = ParsingHelper.ExtractServerDetailsFromAccountServerIdIdentifier(clientId);
                    if (loginServerDetails == null)
                    {
                        throw new Exception();
                    }
                    resultClient._loginServerDetails = loginServerDetails.Value;
                    //
                    resultClient._accountId = clientDictionary.ContainsKey("account-id") ? clientDictionary["account-id"] : null;
                    //
                    if (clientDictionary.ContainsKey("issued-at"))
                    {
                        long issuedAtAsLong;
                        if (long.TryParse(clientDictionary["issued-at"], out issuedAtAsLong))
                        {
                            resultClient._issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtAsLong);
                        }
                    }
                    //
                    if (clientDictionary.ContainsKey("secret-hash"))
                    {
                        // load the base64-encoded binary hash of the client secret
                        resultClient._secret         = clientDictionary["secret-hash"];
                        resultClient._secretIsHashed = true;
                    }
                    else
                    {
                        resultClient._secret         = null;
                        resultClient._secretIsHashed = false;
                    }
                    //
                    if (resultClient._secret != null)
                    {
                        if (clientDictionary.ContainsKey("expires-at"))
                        {
                            long expiresAtAsLong;
                            if (long.TryParse(clientDictionary["expires-at"], out expiresAtAsLong))
                            {
                                resultClient._expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresAtAsLong);
                            }
                        }
                    }
                    //
                    resultClient._softwareId = clientDictionary.ContainsKey("software-id") ? clientDictionary["software-id"] : null;
                    //
                    resultClient._softwareVersion = clientDictionary.ContainsKey("software-version") ? clientDictionary["software-version"] : null;
                    //
                    if (!clientDictionary.ContainsKey("token-endpoint-auth-method"))
                    {
                        // this field is required; return null if it is not present.
                        return(null);
                    }
                    resultClient._tokenEndpointAuthMethod = OAuth2Convert.ConvertStringToTokenEndpointAuthMethod(clientDictionary["token-endpoint-auth-method"]).Value;
                    //
                    resultClient._redirectUris = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS).ConfigureAwait(false);

                    //
                    List <string> grantTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES).ConfigureAwait(false);

                    if (grantTypesAsStrings != null)
                    {
                        resultClient._grantTypes = new ListWithDirtyFlag <OAuth2GrantType>();
                        foreach (string grantTypeAsString in grantTypesAsStrings)
                        {
                            resultClient._grantTypes.Add(OAuth2Convert.ConvertStringToGrantType(grantTypeAsString).Value);
                        }
                    }
                    //
                    List <string> responseTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES).ConfigureAwait(false);

                    if (responseTypesAsStrings != null)
                    {
                        resultClient._responseTypes = new ListWithDirtyFlag <OAuth2ResponseType>();
                        foreach (string responseTypeAsString in responseTypesAsStrings)
                        {
                            resultClient._responseTypes.Add(OAuth2Convert.ConvertStringToResponseType(responseTypeAsString).Value);
                        }
                    }
                    resultClient._scopes = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES).ConfigureAwait(false);

                    //
                    if (clientDictionary.ContainsKey("registration-token-hash"))
                    {
                        // load the base64-encoded binary hash of the client registration token
                        resultClient._registrationToken         = clientDictionary["registration-token-hash"];
                        resultClient._registrationTokenIsHashed = true;
                    }
                    else
                    {
                        resultClient._registrationToken         = null;
                        resultClient._registrationTokenIsHashed = false;
                    }
                    resultClient._isCached = clientIsCached;
                    resultClient._timeCreatedInUnixMicroseconds = timeCreatedInUnixMicroseconds;
                    resultClient._timeUpdatedInUnixMicroseconds = timeUpdatedInUnixMicroseconds;

                    return(resultClient);
                }
            }

            // client could not be found
            return(null);
        }
        public async Task SaveClientAsync()
        {
            // we only support saving a local client (i.e. not updating a remote client)
            if (_isCached)
            {
                throw new InvalidOperationException();
            }

            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_timeCreatedInUnixMicroseconds == null);

            int RESULT_KEY_CONFLICT       = -1;
            int RESULT_DATA_CORRUPTION    = -2;
            int RESULT_UPDATED_SINCE_LOAD = -3;

            // get current server time
            long newTimeUpdatedInUnixMicroseconds = await _redisClient.TimeAsync();

            if (newTimeUpdatedInUnixMicroseconds < 0)
            {
                throw new Exception("Critical Redis error!");
            }
            if (newTimeUpdatedInUnixMicroseconds < _timeUpdatedInUnixMicroseconds)
            {
                throw new Exception("Critical Redis error!");
            }

            if (objectIsNew)
            {
                // assign clientId, clientSecret, issuedAt time and clientRefreshToken
                // for non-implicit grant types: generate a clientSecret
                if ((_grantTypes.Contains(OAuth2GrantType.AuthorizationCode) && _tokenEndpointAuthMethod != OAuth2TokenEndpointAuthMethod.None) ||
                    _grantTypes.Contains(OAuth2GrantType.ClientCredentials) ||
                    _grantTypes.Contains(OAuth2GrantType.Password))
                {
                    _secret            = new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32));
                    _secret_IsDirty    = true;
                    _expiresAt         = null;
                    _expiresAt_IsDirty = true;
                }
                _issuedAt = DateTimeOffset.UtcNow;
                // create client registration token (32-byte == 192-bit)
                /* NOTE: if we ever want to look up the registration token in the #oauth2tokens collections, we will need to start making sure the token is unique-for-server here */
                _registrationToken         = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                _registrationToken_IsDirty = true;
            }

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            if (objectIsNew)
            {
                // for new clients: if a client with this client-id already exists, return 0...and we will try again.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" +
                    "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                    "end\n");
            }
            else
            {
                // for updated: make sure that the "time-created" timestamp has not changed (i.e. that a new key has not replaced the old key)
                luaBuilder.Append("local time_created = redis.call(\"HGET\", KEYS[1], \"time-created\")\n");
                luaBuilder.Append("if time_created ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeCreatedInUnixMicroseconds.ToString());
                iArgument++;

                // for updates: make sure that our old "time-updated" timestamp has not changed
                luaBuilder.Append("local old_time_updated = redis.call(\"HGET\", KEYS[1], \"time-updated\")\n");
                luaBuilder.Append("if old_time_updated ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_UPDATED_SINCE_LOAD.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            if (objectIsNew)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"time-created\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            luaBuilder.Append(
                "if redis.call(\"HSET\", KEYS[1], \"time-updated\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                "end\n");
            arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
            iArgument++;
            //
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"account-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_accountId);
                    iArgument++;
                }
                else
                {
                    // if the account-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"account-id\")\n");
                }
                // clear the dirty flag
                _accountId_IsDirty = false;
            }
            if (_secret_IsDirty)
            {
                if (_secret != null)
                {
                    // if there is a secret assigned to this client, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"secret-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(Convert.ToBase64String(HashClientSecret(Encoding.UTF8.GetBytes(_secret))));
                    iArgument++;
                }
                else
                {
                    // if the secret has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"secret-hash\")\n");
                }
                // clear the dirty flag
                _secret_IsDirty = false;
            }
            if (objectIsNew)
            {
                // set the issued-at time
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"issued-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(_issuedAt.Value.ToUnixTimeSeconds().ToString());
                iArgument++;
            }
            // set the expires-at time
            if (_expiresAt_IsDirty)
            {
                if (_expiresAt != null)
                {
                    long expiresAtAsLong = _expiresAt.Value.ToUnixTimeSeconds();

                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"expires-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(expiresAtAsLong.ToString());
                    iArgument++;
                }
                else
                {
                    // if the expiration has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"expires-at\")\n");
                }
                // clear the dirty flag
                _expiresAt_IsDirty = false;
            }
            if (_softwareId_IsDirty)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"software-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(_softwareId);
                iArgument++;

                // clear the dirty flag
                _softwareId_IsDirty = false;
            }
            if (_softwareVersion_IsDirty)
            {
                if (_softwareVersion != null)
                {
                    // set the softwareVersion
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"software-version\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_softwareVersion);
                    iArgument++;
                }
                else
                {
                    // if the software-version has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"software-version\")\n");
                }
                // clear the dirty flag
                _softwareVersion_IsDirty = false;
            }
            // set the tokenEndpointAuthMethod
            if (_tokenEndpointAuthMethod_IsDirty)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"token-endpoint-auth-method\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(OAuth2Convert.ConvertTokenEndpointAuthMethodToString(_tokenEndpointAuthMethod));
                iArgument++;

                // clear the dirty flag
                _tokenEndpointAuthMethod_IsDirty = false;
            }
            //populate the set of redirect-uris
            if (_redirectUris.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[2])\n");
                foreach (string redirectUri in _redirectUris)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[2], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(redirectUri);
                    iArgument++;
                }

                // clear the dirty flag
                _redirectUris.IsDirty = false;
            }
            // populate the set of grant-types
            if (_grantTypes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[3])\n");
                foreach (OAuth2GrantType grantType in _grantTypes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[3], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(OAuth2Convert.ConvertGrantTypeToString(grantType));
                    iArgument++;
                }

                // clear the dirty flag
                _grantTypes.IsDirty = false;
            }
            // populate the set of response-types
            if (_responseTypes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[4])\n");
                foreach (OAuth2ResponseType responseType in _responseTypes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[4], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[4])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(OAuth2Convert.ConvertResponseTypeToString(responseType));
                    iArgument++;
                }

                // clear the dirty flag
                _responseTypes.IsDirty = false;
            }
            // populate the set of scopes
            if (_scopes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[5])\n");
                foreach (string scope in _scopes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[5], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[4])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[5])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(scope);
                    iArgument++;
                }

                // clear the dirty flag
                _scopes.IsDirty = false;
            }
            //
            if (_registrationToken_IsDirty)
            {
                if (_registrationToken != null)
                {
                    // if there is a registration token assigned to this client, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"registration-token-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(Convert.ToBase64String(HashClientRegistrationToken(Encoding.UTF8.GetBytes(_registrationToken))));
                    iArgument++;
                }
                else
                {
                    // if the registration token has been removed, delete it.
                    // NOTE: this operation is technically supported by spec--but removing the ability for a client to manage a token (usually because of third-party meddling) can have some unfortunate consequences as well
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"registration-token-hash\")\n");
                }
                // clear the dirty flag
                _registrationToken_IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < 1000; iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a 32-byte (192-bit) client_id
                    _id = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES);
                luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                // NOTE: the result will contain a negative integer (error) or one (success)
                // if we were able to create a key, break out of this loop; otherwise, try generating new keys up to ten times.
                if (luaResult == 1)
                {
                    // save our "time-updated" timestamp
                    _timeUpdatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                    if (objectIsNew)
                    {
                        // save our "time-created" timestamp
                        _timeCreatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                        if (_accountId == null)
                        {
                            // if the client belongs to the entire system (and not to an account), add it to the root client collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id });
                        }
                        else
                        {
                            // if the client belongs to the account (and not to the user), add it to the account's client collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id });
                        }
                    }
                    break;
                }
                else if (luaResult == RESULT_KEY_CONFLICT)
                {
                    // key name conflict; try again
                }
                else if (luaResult == RESULT_DATA_CORRUPTION)
                {
                    // data corruption
                    throw new Exception("Critical Redis error!");
                }
                else if (luaResult == RESULT_UPDATED_SINCE_LOAD)
                {
                    // token was updated since we loaded it; we need to reload the token, make the changes again, and then attempt to save it again
                    throw new Exception("Critical Redis error!");
                }
                else
                {
                    // unknown error
                    throw new Exception("Critical Redis error!");
                }
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }