/// <summary> /// Execute the specified SQL stored procedure (deriving parameters automatically, applying inputs from /// parameters and adding output parameters to return values collection) /// </summary> public override async Task <TaskResult> ExecuteTask(TaskParameters parameters) { var result = new TaskResult(); var consoleOutput = new StringBuilder(); var dateTimeNow = DateTime.Now; // Use single value throughout for consistency in macro replacements try { #region Retrieve task parameters // Retrieve and validate required parameters string connectionString = config.GetConnectionString(parameters.GetString("ConnectionString")); string storedProcedureName = parameters.GetString("StoredProcedureName"); if (string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(storedProcedureName)) { throw new ArgumentException("Missing or invalid ConnectionString and/or StoredProcedureName"); } // Retrieve optional parameters string returnValueRegex = parameters.GetString("ReturnValueRegex"); bool defaultNulls = parameters.GetBool("DefaultNulls"); var dbTimeout = parameters.Get <int>("DBTimeout", int.TryParse); #endregion #region Open database connection and execute procedure await using (var cnn = new SqlConnection(connectionString)) { await cnn.OpenAsync(); // Capture console messages into StringBuilder: cnn.InfoMessage += (object obj, SqlInfoMessageEventArgs e) => { consoleOutput.AppendLine(e.Message); }; await using (var cmd = new SqlCommand(storedProcedureName, cnn) { CommandType = CommandType.StoredProcedure }) { if (dbTimeout > 0) { cmd.CommandTimeout = (int)dbTimeout; } #region Derive and apply procedure parameters SqlCommandBuilder.DeriveParameters(cmd); // Note: synchronous (no async version available) foreach (SqlParameter sqlParameter in cmd.Parameters) { if (sqlParameter.Direction.HasFlag(ParameterDirection.Input)) { // This parameter requires input value - check for corresponding parameter: if (parameters.ContainsKey(sqlParameter.ParameterName)) { // Special case - strings are not automatically changed to Guid values, attempt explicit conversion: if (sqlParameter.SqlDbType == SqlDbType.UniqueIdentifier) { sqlParameter.Value = (object)parameters.Get <Guid>(sqlParameter.ParameterName, Guid.TryParse) ?? DBNull.Value; } else { // Apply string value to parameter (replacing null with DBNull): sqlParameter.Value = (object)parameters.GetString(sqlParameter.ParameterName, null, dateTimeNow) ?? DBNull.Value; } } // If parameter value was not set above, and either we are set to supply default NULL // values OR this is also an OUTPUT parameter, set to explicit NULL: else if (defaultNulls || sqlParameter.Direction.HasFlag(ParameterDirection.Output)) { sqlParameter.Value = DBNull.Value; } // (otherwise, value will be left unspecified; if stored procedure does not provide a default // value, execution will fail and missing parameter will be indicated by exception string) } } #endregion await cmd.ExecuteNonQueryAsync(); #region Extract output parameters into return values collection, validate return value foreach (SqlParameter sqlParameter in cmd.Parameters) { if (sqlParameter.Direction.HasFlag(ParameterDirection.Output)) { result.AddReturnValue(sqlParameter.ParameterName, sqlParameter.Value == DBNull.Value ? null : sqlParameter.Value?.ToString()); } } // If return value regex provided, check return value against it: if (!string.IsNullOrEmpty(returnValueRegex)) { string returnValue = result.ReturnValues.ContainsKey("@RETURN_VALUE") ? result.ReturnValues["@RETURN_VALUE"] : null; if (string.IsNullOrEmpty(returnValue)) { throw new Exception($"Stored procedure did not return a value (or no value retrieved)"); } else if (!Regex.IsMatch(returnValue, returnValueRegex)) { throw new Exception($"Invalid stored procedure return value ({returnValue})"); } } #endregion // If this point is reached with no exception raised, operation was successful: result.Success = true; } } #endregion } catch (Exception ex) { if (ex is AggregateException ae) // Exception caught from async task; simplify if possible { ex = TaskUtilities.General.SimplifyAggregateException(ae); } logger.LogError(ex, "Procedure execution failed"); result.AddException(ex); } // If console output was captured, log now: if (consoleOutput.Length > 0) { logger.LogDebug(consoleOutput.ToString()); } return(result); }