In this guide, I'll explain how to run a simple but functional net core application in Ubuntu on a time-based schedule. The application purpose is simple, run every 10 minutes (or whatever schedule fits you) to check which articles need to be switched from DRAFT status to PUBLISHED.

Because this application will be executed by cron service, environment variables won't be visible to this context. To overcome this behavior:

  • Use absolute paths
  • We set the ENV variables manually when declaring the cron jobs
  • Set the environment variables inside the code

I find the first way to be very helpful in most cases because of:

  • Security Save configuration files in a specific path, where you can take care for permissions
  • Other releases I know my configuration won't be overwritten by mistake in another future release
  • Clarity No messy code in cron configurations when you need to set multiple environment variables

With this idea in place lets create a console application using vs/vs code

I'm calling it PublishCron. Simple advice: try to use a meaningful and short name for your applications. You will need these names later on when creating or updating a cronjob, building or managing the state of a daemon or registering them in your web server.

This app will update a table in MySQL database from c#, so we need to install MySql.Data, and I am using Dapper as my Orm of choice:

Install-Package MySql.Data
Install-Package Dapper

For logging part I'm going to use Serilog library and its file sink:

Install-Package Serilog
Install-Package Serilog.Sinks.File

To parse the configuration file:

Install-Package Newtonsoft.Json

Next step is to generate an appsettings.json file that will hold some configurations for our app

{
  "DbConnection": "server=localhost;port=3306;SslMode=none;database=<MY_DATABASE_NAME>;uid=<MY_CUSTOM_USER>;pwd=<MY_CUSTOM_USER_PWD>;",
  "LogPath": "/var/netcore/console/PublishCron/logs/log.txt"
}

Note the logPath value, it's written in its absolute form.

Keep in mind that Linux systems care about character case, so appsettings.json is different from appSettings.json I find it useful to make a habit one form of writing no matter if it's camelCase, PascalCase or Snake_Case and stick to it. By doing so you will save yourself a lot of time.

Add a C# class to map it with the appsettings.json file content:

public class AppSettings
{
    public string DbConnection { get; set; }
    public string LogPath { get; set; }
}

Create another C# class called DbService, this one (obviously) will handle the DB task:

public class DbService
{
    private readonly string connectionString;
    public DbService(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public MySqlConnection DefaultConnection => new MySqlConnection(connectionString);

    public int PublishScheduledArticles(System.Action<string> log)
    {
        var queryString = @"UPDATE articles
                            SET articles.Scheduled = null,
                            articles.ArticleStatus = b'1',
                            articles.Date = utc_timestamp()
                            WHERE Scheduled IS NOT NULL
                                AND Scheduled < utc_timestamp();";

        try
        {
            using (var conn = DefaultConnection)
            {
                conn.Open();
                log("Connection opened");
                var markResult = conn.Execute(queryString);
                log($"Command executed the query successfully with [{markResult}] results");
                return markResult;
            }
        }
        catch (System.Exception ex)
        {
            while (ex.InnerException != null)
                ex = ex.InnerException;
            log($"ERROR: Connection failed with message:  {ex.Message} and stacktrace: {ex.StackTrace}");
            return 0;
        }
    }
}

Update the Program.cs file to match the following:

class Program
{
    static AppSettings GetAppSettings(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("appsettings.json was not found or is not accesible");
        }

        var appSettingsContent = File.ReadAllText(filePath);
        if (string.IsNullOrEmpty(appSettingsContent))
            throw new Exception("Appsettings file is empty");

        return JsonConvert.DeserializeObject<AppSettings>(appSettingsContent);
    }

    static void Main(string[] args)
    {
        try
        {
            var appSettingsFilePath = "/var/netcore/console/PublishCron/appsettings.json";
            var appSettings = GetAppSettings(appSettingsFilePath);

            Log.Logger = new LoggerConfiguration()
                                .MinimumLevel.Information()
                                .WriteTo.File(appSettings.LogPath, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
                                .CreateLogger();

            Log.Information($"Starting PublishCron");
            var dbService = new DbService(appSettings.DbConnection);
            dbService.PublishScheduledArticles(item =>
            {
                Log.Information(item);
            });

            Log.Information($"PublishCron finished. Closing & flushing logs...");
        }
        catch (Exception ex)
        {
            Log.Error(ex, $"GoogleCron app failed with error: {ex.Message}");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

Deploy a self-contained application:

dotnet publish -c Release -r ubuntu.16.04-x64

Move the deployed application files in the server and create the schedule

Now we need to place the deployed application in our Ubuntu machine. I like to use Bitvise SSH Client for this purpose as it has a very simple and intuitive interface, one login and you have ssh & sftp access.

Set our PublishCron app under /var/netcore/console, now the full path to the executable dll file is: /var/netcore/console/PublishCron/PublishCron.dll

Now the last part: Creating the cron job.

From ubuntu documentation:

A crontab file is a simple text file containing a list of commands meant to be run at specified times. It is edited using the crontab command. The commands in the crontab file (and their run times) are checked by the cron daemon, which executes them in the system background.

So by default, every user has this right and this is accomplished by using crontab command.

  • crontab -l Used to view all available schedules
  • crontab -e Is used to edit the list of tasks that will get executed in behalf of the current user (a users crontab will be checked and executed by system daemon no matter if the user is currently logged in or not).

Every line in crontab means one job to be done from cron daemon, and the structure to be followed is like this:

minute hour day month weekday where minute (0-59), hour (0-23, 0 = midnight), day (1-31), month (1-12), weekday (0-6, 0 = Sunday).

So to execute our application every 10 minutes:

  • crontab -e to enter in edit mode
  • */10 * * * * /usr/bin/dotnet /var/netcore/console/PublishCron/PublishCron.dll

When this app executes the log file should appear based in the path that you set in appsettings.json. If not present, try to check the permissions for the log folder.

To check if the application works correctly when executed from the current user context you can run dotnet PublishCron.dll command and view its result.

To verify the dotnet path you may use whereis dotnet command.

You can find the full application in Github