DEV Community

Oleksandr Viktor
Oleksandr Viktor

Posted on

Build a Minute-Based Job Scheduler in .NET 10 with WJb package

Here’s a polished DEV.to-ready article, based on your snippet and the console output you shared. I kept it concise and practical, with copy‑paste sections, clear explanations, and a troubleshooting guide.


Build a Minute-Based Job Scheduler in .NET 10 with WJb

Output when running:

Scheduler running in start of each minute. Await please...
DummyAction.InitAsync. jobMore={"cron":"*/1 * * * *","priority":2}
DummyAction.ExecAsync: Hello from DummyAction! at 06/12/2025 23:48:00
DummyAction.InitAsync. jobMore={"cron":"*/1 * * * *","priority":2}
DummyAction.ExecAsync: Hello from DummyAction! at 06/12/2025 23:49:00

If you’ve ever needed a lightweight, cron‑style scheduler in a .NET app—without pulling in a full-blown orchestrator—this post shows how to wire up a per-minute job scheduler using the WJb package, modern hosting, DI, and logging in .NET 10.

We’ll build a simple hosted app that:

  • Spins up a JobScheduler (cron-like timing).
  • Hands work off to a JobProcessor (execution pipeline).
  • Runs a demo DummyAction once per minute using */1 * * * *.

Why this approach?

  • ✔️ Minimal ceremony: Hosted service + DI + console logs.
  • ✔️ Extensible: Add more actions, priorities, payloads.
  • ✔️ Predictable timing: Cron expression support.
  • ✔️ Separation of concerns: Scheduler decides when, processor decides how, action decides what.

Project Setup

csproj (SDK-style):

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
    <PackageReference Include="WJb" Version="0.8.0-beta1" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Note: The package name here is WJb (0.8.0-beta1). If you’re using a different feed or package id (e.g., UkrGuru.WJb), adjust accordingly.


The Full Program

This single-file example wires up DI, logging, the scheduler, and a demo action that prints a message at the start of each minute.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Text.Json.Nodes;
using WJb;
using WJb.Helpers;

public class Program
{
    static async Task Main(string[] args)
    {
        using var host = Host.CreateDefaultBuilder(args)
            .ConfigureServices(services =>
            {
                // Processor settings for JobProcessor from package (reads IOptions<Dictionary<string, object>>)
                services.Configure<Dictionary<string, object>>(cfg =>
                {
                    cfg["MaxParallelJobs"] = 2;
                });

                services.AddTransient<DummyAction>();

                // --- 1) Action configuration ---
                // Map the action code "Dummy" for scheduler use.
                var actionConfig = new Dictionary<string, ActionItem>(StringComparer.OrdinalIgnoreCase)
                {
                    ["Dummy"] = new ActionItem(
                        Type: typeof(DummyAction).AssemblyQualifiedName!, // robust type resolution
                        More: new JsonObject
                        {
                            ["cron"] = "*/1 * * * *",              // runs at the start of each minute
                            ["priority"] = (int)Priority.Normal
                        })
                };

                services.AddSingleton(actionConfig);

                // Register IActionFactory (implementation provided by the package or your own)
                services.AddSingleton<IActionFactory, ActionFactory>();

                // Register JobProcessor ONCE; expose as IJobProcessor & hosted service
                services.AddSingleton<JobProcessor>();
                services.AddSingleton<IJobProcessor>(sp => sp.GetRequiredService<JobProcessor>());
                services.AddHostedService(sp => sp.GetRequiredService<JobProcessor>());

                // Register JobScheduler hosted service
                services.AddHostedService<JobScheduler>();
            })
            .ConfigureLogging(logging =>
            {
                logging.ClearProviders();
                logging.AddConsole();
                logging.SetMinimumLevel(LogLevel.Error); // reduce noise
            })
            .Build();

        Console.WriteLine("Scheduler running in start of each minute. Await please...");

        await host.RunAsync();
    }
}

// ------------------------------
// Dummy action used by scheduler
// ------------------------------
public sealed class DummyAction(ILogger<DummyAction> logger) : IAction
{
    private readonly ILogger<DummyAction> _logger = logger;
    private JsonObject _jobMore = new();

    public Task InitAsync(JsonObject jobMore, CancellationToken stoppingToken)
    {
        _jobMore = jobMore ?? new JsonObject();
        Console.WriteLine("DummyAction.InitAsync. jobMore={0}", _jobMore.ToJsonString());
        return Task.CompletedTask;
    }

    public Task ExecAsync(CancellationToken stoppingToken)
    {
        var message = _jobMore.GetString("message") ?? "Hello from DummyAction!";
        Console.WriteLine("DummyAction.ExecAsync: {0} at {1}", message, DateTime.Now);
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

How it Works (Step by Step)

  1. Hosted App

    Host.CreateDefaultBuilder spins up DI, config, logging, and runs hosted services until shutdown.

  2. Action Registry (ActionItem)

    We register an action code "Dummy" mapped to DummyAction with More metadata:

    • cron: "*/1 * * * *" — run at the start of every minute.
    • priority: 2 — this demo uses Priority.Normal.
  3. Scheduler (JobScheduler)

    A hosted service that checks each configured action’s cron schedule and enqueues jobs when due.

  4. Processor (JobProcessor)

    Another hosted service that picks up enqueued jobs and executes their actions (with respect to MaxParallelJobs).

  5. Action Lifecycle (IAction)

    • InitAsync receives the jobMore payload (cron, priority, plus any custom fields like message).
    • ExecAsync performs the job’s core work (here, a console message).

Running It

Run the app from your terminal:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Expected console output (every minute):

Scheduler running in start of each minute. Await please...
DummyAction.InitAsync. jobMore={"cron":"*/1 * * * *","priority":2}
DummyAction.ExecAsync: Hello from DummyAction! at 06/12/2025 23:48:00
DummyAction.InitAsync. jobMore={"cron":"*/1 * * * *","priority":2}
DummyAction.ExecAsync: Hello from DummyAction! at 06/12/2025 23:49:00
...
Enter fullscreen mode Exit fullscreen mode

Timing note: The cron */1 * * * * triggers near the top of the minute. Depending on the scheduler’s internal tick/rounding, you might see execution within the first few seconds of the minute.


Customizing the Action

Want to pass a custom message?

Just add it to More:

["Dummy"] = new ActionItem(
    Type: typeof(DummyAction).AssemblyQualifiedName!,
    More: new JsonObject
    {
        ["cron"] = "*/1 * * * *",
        ["priority"] = (int)Priority.Normal,
        ["message"] = "Minute tick ✅"
    })
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

  • No output each minute

    • Ensure the app is still running and not exiting (hosted services need the host alive).
    • Check your system time and timezone; cron matching is time-based.
    • Set logging to Information if you need more granularity:

      logging.SetMinimumLevel(LogLevel.Information);
      
  • Action not resolving

    • Verify typeof(DummyAction).AssemblyQualifiedName is not null and that the type is public.
    • Ensure you registered IActionFactory and DI knows how to construct the action.
  • Parallel execution concerns

    • Use cfg["MaxParallelJobs"] to control concurrency.
    • If your action touches shared resources, implement appropriate locks or idempotency guards.

Production Tips

  • Persisted Queues: For durability, integrate a backing store (DB) for jobs and their state.
  • Retries & Dead-letter: Add failure handling, exponential backoff, and DLQ semantics.
  • Metrics & Health: Expose Prometheus counters or IHealthCheck for scheduler/processor.
  • Graceful Shutdown: Respect CancellationToken in ExecAsync to stop cleanly.

Wrap-up

With a small amount of code, you can build a reliable, minute-based scheduler in .NET 10 using WJb. It cleanly separates responsibilities and remains flexible for real workloads—cron windows, priorities, parallel execution, and custom payloads are all first-class.

Top comments (0)