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
DummyActiononce 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>
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;
}
}
How it Works (Step by Step)
Hosted App
Host.CreateDefaultBuilderspins up DI, config, logging, and runs hosted services until shutdown.-
Action Registry (
ActionItem)
We register an action code"Dummy"mapped toDummyActionwithMoremetadata:-
cron: "*/1 * * * *"— run at the start of every minute. -
priority: 2— this demo usesPriority.Normal.
-
Scheduler (
JobScheduler)
A hosted service that checks each configured action’s cron schedule and enqueues jobs when due.Processor (
JobProcessor)
Another hosted service that picks up enqueued jobs and executes their actions (with respect toMaxParallelJobs).-
Action Lifecycle (
IAction)-
InitAsyncreceives thejobMorepayload (cron, priority, plus any custom fields likemessage). -
ExecAsyncperforms the job’s core work (here, a console message).
-
Running It
Run the app from your terminal:
dotnet run
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
...
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 ✅"
})
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
Informationif you need more granularity:
logging.SetMinimumLevel(LogLevel.Information);
-
Action not resolving
- Verify
typeof(DummyAction).AssemblyQualifiedNameis notnulland that the type is public. - Ensure you registered
IActionFactoryand DI knows how to construct the action.
- Verify
-
Parallel execution concerns
- Use
cfg["MaxParallelJobs"]to control concurrency. - If your action touches shared resources, implement appropriate locks or idempotency guards.
- Use
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
IHealthCheckfor scheduler/processor. - Graceful Shutdown: Respect
CancellationTokeninExecAsyncto 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)