public async Task <SpCallResult> Invoke(SpCall spCall, InvokeOptions spInvokeParams, bool isSystem = false)
        {
            if (spCall == null)
            {
                throw new ArgumentNullException(nameof(spCall));
            }

            // Check duplicate request
            var spInfo = await FindSpInfo($"{spCall.Method}");

            if (spInfo != null && spInfo.ExtendedProps.DataAccessMode == SpDataAccessMode.Write)
            {
                await CheckDuplicateRequest(spInvokeParams.ApiInvokeOptions.RequestId, spInfo.ExtendedProps.CommandTimeout);
            }

            // Call main invoke
            var spi = new InvokeOptionsInternal {
                SpInvokeParams = spInvokeParams, IsSystem = isSystem
            };

            if (isSystem)
            {
                spi.SpInvokeParams.AuthUserId = AppAuthUserId;
            }

            return(await Invoke(spCall, spi));
        }
        public async Task <SpCallResult[]> Invoke(SpCall[] spCalls, InvokeOptions spInvokeParams)
        {
            //Check DuplicateRequest if spCalls contian at least one write
            foreach (var spCall in spCalls)
            {
                var spInfo = await FindSpInfo($"{spCall.Method}");

                if (spInfo != null && spInfo.ExtendedProps.DataAccessMode == SpDataAccessMode.Write)
                {
                    await CheckDuplicateRequest(spInvokeParams.ApiInvokeOptions.RequestId, 3600 * 2);

                    break;
                }
            }

            var spi = new InvokeOptionsInternal
            {
                SpInvokeParams = spInvokeParams,
                IsBatch        = true
            };

            var spCallResults = new List <SpCallResult>();
            var tasks         = new List <Task <SpCallResult> >();

            foreach (var spCall in spCalls)
            {
                tasks.Add(Invoke(spCall, spi));
            }

            try
            {
                await Task.WhenAll(tasks.ToArray());
            }
            catch
            {
                // catch await single exception
            }

            foreach (var item in tasks)
            {
                if (item.IsCompleted)
                {
                    spCallResults.Add(item.Result);
                }
                else
                {
                    spCallResults.Add(new SpCallResult {
                        { "error", DirectSpExceptionAdapter.Convert(item.Exception.InnerException, _captchaController).SpCallError }
                    });
                }
            }

            return(spCallResults.ToArray());
        }
        private async Task <SpCallResult> Invoke(SpCall spCall, InvokeOptionsInternal spi)
        {
            try
            {
                return(await InvokeCore(spCall, spi));
            }
            catch (SpInvokerAppVersionException)
            {
                await RefreshApi();

                return(await Invoke(spCall, spi));
            }
            catch (DirectSpException spException) //catch any read-only errors
            {
                throw spException.SpCallError.ErrorId == 3906 ? new SpMaintenanceReadOnlyException(spCall.Method) : spException;
            }
        }
        private bool IsReadScale(SpInfo spInfo, UserSession userSession, InvokeOptionsInternal spi, out bool isWriteMode)
        {
            //Select ReadOnly Or Write Connection
            var dataAccessMode = spInfo.ExtendedProps != null ? spInfo.ExtendedProps.DataAccessMode : SpDataAccessMode.Write;

            //Write procedures cannot be called in ForceReadOnly anyway
            if (spi.IsForceReadOnly && dataAccessMode == SpDataAccessMode.Write)
            {
                throw new SpMaintenanceReadOnlyException(spInfo.ProcedureName);
            }

            //Set write request
            isWriteMode = !spi.IsForceReadOnly && dataAccessMode == SpDataAccessMode.Write;

            // Find connection string
            var isReadScale = spi.IsForceReadOnly || dataAccessMode == SpDataAccessMode.ReadSnapshot ||
                              (dataAccessMode == SpDataAccessMode.Read && userSession.LastWriteTime.AddSeconds(_readonlyConnectionSyncInterval) < DateTime.Now);

            return(isReadScale);
        }
        private async Task <SpCallResult> InvokeCore(SpCall spCall, InvokeOptionsInternal spi)
        {
            try
            {
                // Check captcha
                await CheckCaptcha(spCall, spi);

                // Call core
                var result = await InvokeCore2(spCall, spi);

                // Update result
                await UpdateRecodsetDownloadUri(spCall, spi, result);

                return(result);
            }
            catch (Exception ex)
            {
                throw DirectSpExceptionAdapter.Convert(ex, _captchaController);
            }
        }
        private async Task <SpCallResult> InvokeCore2(SpCall spCall, InvokeOptionsInternal spi)
        {
            //fix isLocal for system request and loopback
            if (spi.IsSystem)
            {
                spi.SpInvokeParams.IsLocalRequest = true;
            }

            // retrieve user session
            var invokeParams  = spi.SpInvokeParams;
            var invokeOptions = spi.SpInvokeParams.ApiInvokeOptions;
            var userSession   = _sessionManager.GetUserSession(invokeParams.AuthUserId, invokeParams.Audience);

            //Verify user request limit
            VerifyUserRequestLimit(userSession);

            //find api
            var spName = spCall.Method;

            if (spCall.Method == "System_api")
            {
                return(await Help());
            }

            var spInfo = await FindSpInfo(spName);

            if (spInfo == null)
            {
                var ex = new DirectSpException($"Could not find the API: {spName}");
                _logger?.LogWarning(ex.Message, ex);//Log exception
                throw ex;
            }

            //check IsCaptcha by meta-data
            if ((spInfo.ExtendedProps.CaptchaMode == SpCaptchaMode.Always || spInfo.ExtendedProps.CaptchaMode == SpCaptchaMode.Auto) && !spi.IsCaptcha)
            {
                throw new InvalidCaptchaException(await _captchaController.Create(), spInfo.ProcedureName);
            }

            //check IsBatchAllowed by meta-data
            if (!spInfo.ExtendedProps.IsBatchAllowed && spi.IsBatch)
            {
                throw new SpBatchIsNotAllowedException(spInfo.ProcedureName);
            }

            //Create DirectSpContext
            var context = new DirectSpContext()
            {
                IsBatch          = spi.IsBatch,
                IsCaptcha        = spi.IsCaptcha,
                RecordIndex      = invokeOptions.RecordIndex,
                RecordCount      = invokeOptions.RecordCount,
                AppVersion       = AppVersion,
                IsReadonlyIntent = spInfo.ExtendedProps.DataAccessMode == SpDataAccessMode.Read || spInfo.ExtendedProps.DataAccessMode == SpDataAccessMode.ReadSnapshot,
                RequestRemoteIp  = spi.SpInvokeParams.RequestRemoteIp?.ToString(),
                IsLocalRequest   = spi.SpInvokeParams.IsLocalRequest,
                UserAgent        = spi.SpInvokeParams.UserAgent,
                AppName          = AppName,
                Audience         = invokeParams.Audience,
                AuthUserId       = invokeParams.AuthUserId,
                ClientVersion    = null, //not implemented yet
                AgentContext     = userSession.AgentContext
            };

            //Get Connection String caring about ReadScale
            var isReadScale = IsReadScale(spInfo, userSession, spi, out bool isWriteMode);

            //create SqlParameters
            var spCallResults = new SpCallResult();
            var paramValues   = new Dictionary <string, object>();

            //set other caller params
            var spCallParams = spCall.Params ?? new Dictionary <string, object>();

            foreach (var spCallparam in spCallParams)
            {
                var paramName  = spCallparam.Key;
                var paramValue = spCallparam.Value;

                //find sqlParam for callerParam
                var spParam = spInfo.Params.FirstOrDefault(x => x.ParamName.Equals($"{paramName}", StringComparison.OrdinalIgnoreCase));
                if (spParam == null)
                {
                    throw new ArgumentException($"parameter '{paramName}' does not exists!");
                }
                spInfo.ExtendedProps.Params.TryGetValue(spParam.ParamName, out SpParamInfoEx spParamEx);

                //make sure Context has not been set by the caller
                if (paramName.Equals("Context", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException($"You can not set '{paramName}' parameter!");
                }

                // extract data from actual signed data
                var value = CheckJwt(paramValue, spParam, spParamEx);

                // convert data for db
                paramValue = ConvertDataForResource(value, spParam, spParamEx, invokeOptions);

                // add parameter
                paramValues.Add(paramName, paramValue);
            }

            // set all output value which have not been set yet
            foreach (var spParam in spInfo.Params)
            {
                if (spParam.IsOutput)
                {
                    if (string.Equals(spParam.ParamName, "Recordset", StringComparison.OrdinalIgnoreCase))
                    {
                        throw new DirectSpException($"{spInfo.ProcedureName} contains {spParam.ParamName} as a output parameter which is not supported!");
                    }

                    if (!paramValues.ContainsKey(spParam.ParamName))
                    {
                        paramValues.Add(spParam.ParamName, Undefined.Value);
                    }
                }
            }

            // execute the command
            var commandResult = await _commandProvider.Execute(spInfo, context, paramValues, isReadScale);

            // store agentContext
            userSession.AgentContext = commandResult.AgentContext;

            // fill Recordset and close dataReader BEFORE reading sqlParameters
            if (commandResult.Table != null)
            {
                ReadRecordset(spCallResults, commandResult.Table, spInfo, invokeOptions);
            }

            // build return params
            foreach (var outParam in commandResult.OutParams)
            {
                var outParamName  = outParam.Key;
                var outParamValue = outParam.Value;
                var spParam       = spInfo.Params.FirstOrDefault(x => x.ParamName.Equals($"{outParamName}", StringComparison.OrdinalIgnoreCase));
                spInfo.ExtendedProps.Params.TryGetValue(outParamName, out SpParamInfoEx spParamEx);

                // Sign text if is need
                if (spParamEx?.SignType == SpSignType.JwtByCertThumb)
                {
                    outParamValue = _tokenSigner.Sign(outParamValue.ToString());
                }

                // convert data form result
                var value = ConvertDataFromResource(outParamValue, invokeOptions);
                spCallResults.Add(outParamName, value);

                // add Alternative Calendar
                if (_alternativeCalendar.IsDateTime(spParam.SystemTypeName.ToString()))
                {
                    spCallResults.Add(_alternativeCalendar.GetFieldName(outParamName), _alternativeCalendar.FormatDateTime(value, spParam.SystemTypeName));
                }
            }

            userSession.SetWriteMode(isWriteMode);
            return(spCallResults);
        }