Using FluentScheduler with C#

Karen Payne - Oct 22 '23 - - Dev Community

Learn how to create a background service using NuGet package FluentScheduler in a Windows Forms project without a need for a service running. FluentScheduler is not as robust as the popular Quartz.NET library but for small task FluentScheduler works great.

Only downside to FluentScheduler is the lack of documentation which can turn away novice developers. All code samples are presented with console projects using SeriLog to log information within jobs.

Since most samples are done in console projects, we will walk through running two Windows Forms projects where one adds data and the other receives data in real time from the first application and alters the data. Code for the most part has been kept simple for ease of learning.

The code sample is to mimic taking an order in one Windows Form application and report the new order in another Windows Form application. Think of the first application as a waitress taking an order while the second application is for the cook to see and process.

To keep code simple there are no order details, just an order with a primary key, order date and time. All data is exposed in a CheckedListBox so that in the second application by checking one or more items and clicking a button the items are marked as being processed and then removed from the CheckedListBox.

Dependency Injection

Currently, the library supports dependency injection of jobs (via IJobFactory). However, you shouldn't use it, it's a bad idea on its way to be deprecated.

Database operations

  • A SQL-Server localdb database is used.
  • To start off create the database with the script under the project OrdersLibrary/Scripts (which has instructions)
  • All data operations are done in OrdersLibrary library using Microsoft Entity Framework Core (EF Core).
  • Both Windows Forms projects use OrdersLibrary library.
  • Classes for EF Core were created with a Visual Studio extension EF Power Tools

Season deverlopers

  • Run data script in OrdersLibrary\Scripts
  • Run OrderTakerApp
  • Run OrderProcessingApp
  • Study code

Step through code on the processing side

JobRegistry class constructor sets up a job to run which specifies a methods in the Schedule of the base Registry.

Schedule(PerformWork)
    .WithName(jobName)
    .ToRunEvery(interval)
    .Seconds();
Enter fullscreen mode Exit fullscreen mode
  • PerformWork is passed as a method group.
  • WithName allows code later to stop this job in JobController
  • ToRunEvery accepts a value to run every n seconds. In this case, when triggered 10 seconds will passed before PerformWork is triggered

Suppose PerformWork should run immediately then every 10 seconds, use the following where ToRunNow indicates to run immediately and AndEvery, in this case every 10 seconds.

Schedule(PerformWork)
    .WithName(jobName)
    .ToRunNow()
    .AndEvery(interval)
    .Seconds();
Enter fullscreen mode Exit fullscreen mode

PerformWork method gets orders not processed followed by invoking ReportNewOrders event. The form listens via

JobRegistry.ReportNewOrders += JobRegistry_ReportNewOrders;
Enter fullscreen mode Exit fullscreen mode

Which and this is important in the event we must call Invoke done in the language extension InvokeIfRequired as data is coming from another thread. If we did not use Invoke, no run time errors but the CheckedListBox would never show new orders coming in.

private void JobRegistry_ReportNewOrders(List<Orders> list)
{
    this.InvokeIfRequired(form => UpdateCheckedListBox(list));
}
Enter fullscreen mode Exit fullscreen mode

Form Code

From the constructor, orders are read into a BindingList<Orders> which feeds into a BindingSource and the BindingSource becomes the DataSource for the CheckedListBox.

Next the job is triggered via JobController.Start(); followed by subscribing to ReportNewOrders event of JobRegistry which in turn allows real time updates from the database table to the CheckedListBox.

Next, we subscribe to form closing event to stop the job else its possible to get a runtime exception if an object is not set to an instance of an object in the process.

Processing an order

Check an item/order, press the process button. This in turn gets the checked items (more than one can be checked and processed but in real life it would be one order at a time), sends the orders to the database class which updates the orders followed by ensuring no items are checked (ran into this once or twice) then sets the selected item as the last item/order.

Wait a minute, how about processing an order when checking an order in the CheckedListBox?

Yes we can but must call BeginInvoke that without this call when asking for checked orders we get a count of 0, had to Google this and it turns out to be an issue with the actual CheckedListBox.

So let's take the code from the process button and place the code into a new method.

private void ProcessCurrentOrder()
{

    List<Orders> checkedOrders = OrdersCheckListBox.CheckedList<Orders>();
    Log.Information(checkedOrders.Count.ToString());
    if (checkedOrders.Count <= 0) return;

    DataOperations.ProcessOrders(checkedOrders);

    Log.Information("After processed orders in form");

    OrdersCheckListBox.DataSource = DataOperations.GetNewOrders();
    CheckedListBoxCleanUp();

}
Enter fullscreen mode Exit fullscreen mode

For the CheckedListBox, subscribe to ItemCheck event.

private void OrdersCheckListBox_ItemCheck(object? sender, ItemCheckEventArgs e)
{

    BeginInvoke(() =>
    {
        if (e.NewValue == CheckState.Checked)
        {
            ProcessCurrentOrder();
        }

    });

}
Enter fullscreen mode Exit fullscreen mode

And now the Button click event uses the same code as the CheckedListBox.

private void ProcessButton_Click(object sender, EventArgs e)
{
    ProcessCurrentOrder();
}
Enter fullscreen mode Exit fullscreen mode

Step through code on adding orders

The purpose of this project is to setup one or more orders for the OrderProcessingApp to work with.

There are two buttons.

The first to add a new order with the current date/time.

public static async Task<Orders> AddNewOrder()
{
    await using var context = new Context();
    Orders order = new Orders()
    {
        OrderDate = new DateOnly(Now.Year, Now.Month, Now.Day),
        OrderTime = new TimeOnly(Now.Hour, Now.Minute, Now.Second),
        OrderIsNew = true
    };

    context.Add(order);
    await context.SaveChangesAsync();
    return order;
}
Enter fullscreen mode Exit fullscreen mode

The second to reset all orders for the current day using EF Core ExecuteUpdate method.

public static void UpdateTodayOrders()
{
    var today = TodayDate();
    using var context = new Context();
    context.Orders.AsNoTracking()
        .Where(order => order.OrderDate!.Value == today)
        .ExecuteUpdate(s => s
            .SetProperty(order => order.OrderIsNew,
                order => true));
}
Enter fullscreen mode Exit fullscreen mode

Alternate

Consider working directly with data in SSMS by manually adding records and resetting records.

Add records by right clicking the table.

Reset records

UPDATE dbo.Orders
SET OrderIsNew = 1
WHERE OrderDate = '2023-10-22';
Enter fullscreen mode Exit fullscreen mode

Summary

In the article, code has been presented to get started with FluentScheduler library which has been kept relatively simple rather than complex for ease of learning.

Take time to first run the projects then go back and study the code to understand how the scheduler works and interacts with the user interface.

Since the time span for the scheduler is at ten seconds debugging will not work, instead depend on SeriLog for logging what needs to be inspected.

If Windows Forms is not preferred there is always console and ASP.NET Core options while there are other libraries to explore too if this library does not suit your needs.

Source code

Clone the following GitHub repository and open the solution with Microsoft VS2022 or later.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .