Looking for coding best practices for methods for recurring tasks in ASP.NET Core

I’m looking for some guidance, suggestions, “best practices”, etc. for methods to be scheduled as background or recurring jobs in Hangfire,

I have a small-scale Hangfire project at work, currently running several recurring tasks in production, and looking to expand this to more tasks and a larger variety of tasks.

This is an ASP.NET Core 8.0 project, currently using Hangfire v1.8.17 (Hangfire.AspNetCore and Hangfire.SqlServer), connecting to a SQL Server 2022 database. This is also using “IdentityStream.Hangfire.Console” and “Microsoft.Data.SqlClient” (v5.2.2).

Shown below is the current code for one of my methods, intended to be scheduled with a stored procedure name, to be executed as shown. I ha ve been through several iterations of this type of code, learning a bit more each time. If you have any suggestions for improvements or changes or alternative approaches, please reply with suggestions.

Notes:

  • The code below shows the database connection string being passed as a plain-text string to this method, I removed the configuration object code that I am using related to the connection string below, just to simplify the code sample in this question.
  • The “PerformContext taskContext” parameter is for “IdentityStream.Hangfire.Console”.
  • I’m not showing my error-handling below, apart from the “OperationCanceledException” used for console messages.
  • The first parameter (“string taskName”) is not used inside the method; it is only used for the DisplayName attribute - the “DisplayName” attribute allows the Hangfire dashboard to display more descriptive task names (rather than “ExecuteSP.Execute” for all the different tasks using this method.

My questions:

Is this the best approach for this type of code?

Is “public static async Task” the most appropriate method type for this purpose? Is making it “async” helpful?

Is “token.ThrowIfCancellationRequested()” the appropriate use of the CancellationToken, or does this require additional CancellationToken code? (I’m aware that it doesn’t really achieve much in this specific case where it’s just calling a single stored procedure, but is this the appropriate call if this was inside a loop in a different method?)

Any other suggestions for changes or improvements to a method like this?

// ===================================================================================
using Hangfire.Console;
using Hangfire.Server;
using Microsoft.Data.SqlClient;
using System.ComponentModel;
using System.Data;

namespace MyProject.Code
{
    public class ExecuteSP
    {
        // Call a SQL Server stored procedure with NO parameters (and no return output)

        // Set DisplayName for Hangfire dashboard
        // Note: the {0} argument (taskName) is not used inside the method, only used for this display
        [DisplayName("{0} (SP: {2})")]
        public static async Task Execute(string taskName, string? dbConnection, string spName, PerformContext taskContext, CancellationToken token)
        {
            taskContext.WriteLine($"Starting execution of stored procedure: {spName}");

            try
            {
                using (var cnx = new SqlConnection(dbConnection))
                {
                    await cnx.OpenAsync(token);

                    using (var cmd = new SqlCommand(spName, cnx))
                    {
                        cmd.CommandType = CommandType.StoredProcedure;
                        cmd.CommandTimeout = 3600; // 1 hour

                        token.ThrowIfCancellationRequested();

                        await cmd.ExecuteNonQueryAsync(token);
                    }
                }
                taskContext.WriteLine($"Successfully executed stored procedure: {spName}");
            }
            catch (OperationCanceledException)
            {
                taskContext.SetTextColor(ConsoleTextColor.Red);
                taskContext.WriteLine("Task was cancelled.");
                taskContext.ResetTextColor();
            }
        }
    }
}
// ===================================================================================

The method above would be scheduled using a call similar to this:
RecurringJob.AddOrUpdate(“MyTaskName”, () => ExecuteSP.Execute(“ThisTaskName”, “conn-string”, “dbo.SPname”, null!, CancellationToken.None), Cron.Hourly(), new RecurringJobOptions { TimeZone = TimeZoneInfo.Local });

Without knowing more about what the task is, we should ask if it is best at this point to fire off the job or to return a task and let the calling code fire off the job. If the calling code creates a Thread for the call, and then this code creates another thread for running this code. The result is wasted recourses and a slight lag in time. Not something that this type of application would be able to actually see in practice. But I would think that the best practice would be for the code here to return a Task.