Aicrosoft.Scheduling 8.0.0-beta.260130.2

This is a prerelease version of Aicrosoft.Scheduling.
dotnet add package Aicrosoft.Scheduling --version 8.0.0-beta.260130.2
                    
NuGet\Install-Package Aicrosoft.Scheduling -Version 8.0.0-beta.260130.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Aicrosoft.Scheduling" Version="8.0.0-beta.260130.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Aicrosoft.Scheduling" Version="8.0.0-beta.260130.2" />
                    
Directory.Packages.props
<PackageReference Include="Aicrosoft.Scheduling" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Aicrosoft.Scheduling --version 8.0.0-beta.260130.2
                    
#r "nuget: Aicrosoft.Scheduling, 8.0.0-beta.260130.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Aicrosoft.Scheduling@8.0.0-beta.260130.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Aicrosoft.Scheduling&version=8.0.0-beta.260130.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Aicrosoft.Scheduling&version=8.0.0-beta.260130.2&prerelease
                    
Install as a Cake Tool

JobsFactory

   __        _            ___          _                   
   \ \  ___ | |__  ___   / __\_ _  ___| |_ ___  _ __ _   _ 
    \ \/ _ \| '_ \/ __| / _\/ _` |/ __| __/ _ \| '__| | | |
 /\_/ / (_) | |_) \__ \/ / | (_| | (__| || (_) | |  | |_| |
 \___/ \___/|_.__/|___/\/   \__,_|\___|\__\___/|_|   \__, |
                                                     |___/ 

Keywords: jobsFactory, jobs, task, job, routine, schedule, setup, startup, interval, cronexpression, jobagent

JobsFactory is a task scheduling framework that supports multiple types of jobs(tasks) and is easily extensible. It is written in dotnet8. It is an upgrade and refactoring of my earlier open-source projects BeesTask.

An implementation example : JobAgent

Advantages

  • Multiple trigger modes are supported:
    • Setup trigger – runs when the system or component is upgraded.
    • Interval trigger – runs on a fixed time interval.
    • Cron trigger – runs based on a cron Expression.
    • Event trigger – runs when specific events occur.
  • Simply inherit a few classes, set the properties, and override the necessary methods to implement your custom business logic.
  • The system is implemented as a plugin based on a .NET library, featuring a clean and well-structured design that’s easy to manage and use.

Applicable Scenarios

  • Periodically triggering a task to perform certain operations, such as processing large amounts of data;
  • Triggering at specific times to perform operations that need to be executed at certain times, such as on the first day of each month;
  • Triggering based on events to perform subsequent operations, such as automatic directory updates and transfers;

Common Usage Examples

  • DDNS updates can be executed periodically or triggered by IP update events from routers;
  • Monitoring a directory and synchronizing its contents to other locations when changes occur;
  • Monitoring events in the database and executing specific business logic based on the content of the events;

Supported Task Types

Before using this framework, please determine whether it is suitable for your needs based on the supported job types.
| Value | Type | Example Value | Description | | ----------- | :----------------------- | :----------------- | :---------------------------------------------------------------------------------- | | Setup | Installation and Upgrade | "" | Runs after startup | | Startup | Startup | "2000" | Runs before all tasks after Setup, with a delay of 2 seconds. May run continuously. | | Event | Event Trigger | "DirChangeEventer" | Triggered when the specified directory changes | | Interval | Interval Execution | "00:00:20" | Runs every 20 seconds | | Schedulable | Cron Expression | "* 0/10 * * * ?" | Cron expression: Executes every 10 minutes |

NOTE: Job type extensions are not supported at the moment.



Quick Start (Simple Example)

Implement a Job

Code Job Description

public class Interval10Sec : Job
{
    public Interval10Sec()
    {
        Trigger = "00:00:10";
    }
    public override string? WorkerName => nameof(SimpleWorker);
}

Configuration Job Description

{
 "Jobs": {
   // SampleJobs is the module name, used to distinguish Jobs with the same Job name in multiple plugin modules.
   "SampleJobs": [
     {
       "name": "Interval10Sec",
       "trigger": "00:00:10",
       "workerName": "SimpleWorker"
     },
   ]
 }
}

Implement a Business Worker for a Job

Worker is Executing while Job Trigger (Job.WorkerName)

By using the following code, the SimpleWorker.ExecuteAsync method will be executed every 10 seconds.

[KeyedName]
public sealed class SimpleWorker(IServiceProvider serviceProvider) : Worker<JobContext>(serviceProvider)
{
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(500, cancellationToken); //mock execute 0.5 second.
        await ExecuteCallbackAsync(true);// work finish callback. It's not necessary.
    }
   
    protected override async Task ExecuteCallbackAsync<TResult>(TResult? result) where TResult : default
    {
        if (result is true)
            Logger.LogDebug($"Worker are success.");
        await Task.CompletedTask;
    }
}

Note the handling of the CancellationToken signal, which will receive a cancellation signal when the application stops.

Key Components in the Framework

To better utilize the JobFactory framework and to extend and customize it more deeply, it is essential to understand the relevant components within the framework.

You can visit Architecture UML to view the UML class diagram.

Job - Task Configuration

  • A Job is a description of a task, through which you can set its job type and the business logic unit to be executed.
  • There are two ways to configure it: typically in the application configuration appsettings.json or in the configuration of a plugin module moduleAssemblyName.json. Alternatively, you can achieve the same effect by inheriting from the Job class directly in code.
  • Once the type of the Job's Trigger is set, do not change it.

Configuration in appsettings.json

{
 "Jobs": {
   // SampleJobs is the module name, used to distinguish Jobs with the same Job name in multiple plugin modules.
   "SampleJobs": [
     {
       "name": "Interval10Sec",
       "trigger": "00:00:10",
       "workerName": "SimpleWorker"
     },
   ]
 }
}

Code Implementation

public class Interval10Sec : Job
{
    public Interval10Sec()
    {
        Trigger = "00:00:10";
    }
    public override string? WorkerName => nameof(SimpleWorker);
}

JobContext - State and Context During Task Execution

  • JobContext is the context and state corresponding to each Job, which will be persisted to the default /states/ directory after the first run.
  • When the task runs again, it will read the last persisted state and restore it as the current JobContext.
  • Each JobContext must implement the corresponding JobFactory to create the context.
  • You can also use it directly without creating a custom JobContext. You can set and retrieve different working states through the Data property.

Custom CustomJobContext

public sealed class CustomJobContext : JobContext
{
    public bool Done
    {
        get; set;
    }
}

public sealed class CustomJobContextFactory(IServiceProvider serviceProvider)
    : JobContextServiceBase(serviceProvider), IJobContextFactory<CustomJobContext>, ITransient
{
    public override CustomJobContext LoadOrCreate(IJob job)
    {
        var ctx = LoadOrCreateNew(job, Create);
        return ctx;
    }

    protected override CustomJobContext Create() => new();
}

Worker - The Business Logic Execution Unit

  • Workers are registered to the DI container using their class name via KeyedName, or you can specify a string name. If no name is specified, the full class name will be registered in the DI container.
  • When implementing your own Worker business class, you need to point the WorkerName of the Job to it.
  • Each Worker requires a corresponding JobContext to determine the context type.

Simple Implementation

[KeyedName]
public sealed class SimpleWorker(IServiceProvider serviceProvider) : Worker<JobContext>(serviceProvider)
{
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(500, cancellationToken); //mock execute 0.5 second.
        await ExecuteCallbackAsync(true);// work finish callback. It's not necessary.
    }
   
    protected override async Task ExecuteCallbackAsync<TResult>(TResult? result) where TResult : default
    {
        if (result is true)
            Logger.LogDebug($"Worker are success.");

        await Task.CompletedTask;
    }
}

Event - Customizing the Triggering Event for Event-Driven Jobs

  • This is the event trigger for the Job's triggering type.
  • Each event is different, so it needs to be implemented based on specific business requirements.
  • The class name of the event is the name of the class that starts the event detection.

Simple Implementation

[KeyedName]
public sealed class RouterEventer(IServiceProvider serviceProvider) : Eventer<TimeJobContext>(serviceProvider)
{
    public override async Task StartAsync(TimeJobContext? jobContext, CancellationToken cancellationToken)
    {
        await base.StartAsync(jobContext, cancellationToken);
        var receiver = new UdpReceiver(ServiceProvider, cancellationToken);
        receiver.OnMessage += async (s, e) =>
        {
            var workerName = jobContext.GetWorkerName();
            var worker = ServiceProvider.GetKeyedService<IWorker<TimeJobContext>>(workerName);
            if (worker == null)
            {
                Logger.LogError($"{this} No Worker[{workerName}] was found.");
                return;
            }

            var dnsrst = jobContext!.GetData<DDNSState>() ?? new DDNSState();
            jobContext.SetData(dnsrst);
            await worker.StartAsync(jobContext, cancellationToken); //trigger the worker.
        };
        await Task.CompletedTask;
    }
}

Watcher - Each Watcher Monitors Certain Types of Jobs

  • Each Watcher monitors one or more Jobs.
  • Each Watcher corresponds to a specific JobContext.
  • It creates the corresponding JobContext for the Job when it starts.
  • It is responsible for triggering and managing the Worker of the Job.
  • For Jobs of the same Trigger type, there may be multiple Watchers, with Watchers corresponding to Triggers and specific JobContexts.
  • You can implement a custom Watcher.

Simple Implementation of an Event Watcher

public sealed class MyEventWatcher(IServiceProvider serviceProvider) : EventWatcherBase<MyJobContext>(serviceProvider)
{
}

Implementation of a Custom Watcher

public sealed class MyWatcher(IServiceProvider serviceProvider) : Watcher<MyJobContext>(serviceProvider), ITransient
{
    protected override TriggerStyle TriggerStyle => TriggerStyle.Setup;

    protected override async Task ExecuteAsync()
    {
        var ctxs = JobContexts;
        using var logDis = Logger.BeginScopeLog(out string scopedId);
        try
        {
            var runjxs = ctxs.Where(x => !x.Done).OrderBy(x => x.OrderVersion);
            foreach (var run in runjxs)
            {
                if (AppCancellationToken.IsCancellationRequested) break;
                var name = run.GetWorkerName();
                var worker = ServiceProvider.GetKeyedService<IWorker<SetupJobContext>>(name);
                if (worker == null)
                    Logger.LogError($"{this} No Worker[{name}] was found.");
                else
                    await worker.StartAsync(run, AppCancellationToken);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, $"{this} has exception:{ex.Message}");
        }
    }
}

Usage

Starting from Scratch

Create a New dotnet8 Console Project

  • Add the required package references.
NuGet\Install-Package Aicrosoft.Scheduling
NuGet\Install-Package Aicrosoft.Extensions.Hosting

Modify Program.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Aicrosoft.Logging.NLog;
using Aicrosoft.Services;

var start = Environment.TickCount64;
var logger = NLogHelper.GetLogger();
logger.DelegateDiag();
logger.CaptureGlobalException();
logger.LogTrace($"Loggger was created TimeElapsed:{Environment.TickCount64 - start} ms");
try
{
    NLogHelper.SetConfigurationVariableWhenRelease("logLevel", "Info");
    start = Environment.TickCount64;
    logger.LogTrace($"Begin Build Host Envirment ...");
    using IHost host = Host.CreateDefaultBuilder(args)
        .AddServices(typeof(BizApp).Assembly) //add Biz.Core assembly to DI
        .AddAsWindowsService("JobAgent")
        .AddJobsFactory()
        .AddPluginsService() //enable plugins support.
        .AddNLog()
        //.UseAop()
        .AddServiceDIDebug() //show DI table. for develop.
        .Build()
        ;
    ServiceLocator.ServiceProvider = host.Services;
    logger.LogTrace($"End Build. TimeElapsed:{Environment.TickCount64 - start} ms");
    await host.RunAsync();
    return 0;
}
catch (Exception ex)
{
    logger.LogError(ex, "Build and run IHost has a exception");
    return -9;
}
finally
{
    NLogHelper.Shutdown();
}

Subsequent Steps

  • Add Job configurations or code.
  • Implement the business logic Worker.
  • If extending JobContext, implement the corresponding JobContextFactory and Worker.
  • Use the existing open-source JobAgent.
  • Create a new library project and add the reference.
NuGet\Install-Package Aicrosoft.Scheduling
NuGet\Install-Package Aicrosoft.Extensions.Hosting
  • The plugin assembly is your business module, where you can fully implement complex business logic.
  • As long as the corresponding Job description is implemented, it will be called by the framework.
  • ⚠️ Add <EnableDynamicLoading>true</EnableDynamicLoading> to the project properties of the plugin assembly to copy all the package DLLs.

Plugin Configuration Class

  • It automatically adds services from the plugin assembly to the DI container.
  • It defaults to reading the configuration from AssemblyName.json.
public class JobAppSetup : PluginSetupBase
{
}

Override the ConfigureServices Method

  • If you have custom configuration instances, override the ConfigureServices method.
public class JobAppSetup : PluginSetupBase
{
    protected override void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
    {
        base.ConfigureServices(hostBuilderContext, services);
        services.Configure<DDNSOption>(hostBuilderContext.Configuration.GetSection(DDNSOption.SectionName));

        // Add custom service to DI
        services.AddSingleton<IMyBiz, MyBiz>();
    }
}

Subsequent Steps

  • Copy the files generated in the bin directory of the assembly to the Bin\Plugins\PluginName\ directory of JobAgent.

Configuration

Overview

  • The appsettings.json in JobAgent is not mandatory. Without it, all plugin modules will be loaded.
  • Different modules in the Plugins directory of JobAgent will load their correspondingly named configuration files by default.
  • If the Aicrosoft.Extensions.NLog module is used, nlog.config is the configuration for NLog.

Job Configuration

🚀 Job configuration is the core of the entire system

You can configure it in the appsettings.json under the root directory of JobAgent, or in the correspondingly named configuration under the plugin directory.

Job Configuration Example

{
  "Jobs": {
    // SampleJobs is the module name, used to distinguish Jobs with the same Job name in multiple plugin modules.
    "SampleJobs": [
      {
        "enable": false,
        "name": "Interval-Woker-Sample1",
        "trigger": "00:00:05",
        "workerName": "SampleJobs.Aicrosoft.SimpleIntervalWorker, SampleJobs"
      },
      {
        "enable": true,
        "name": "L11",
        "trigger": "* 0/10 * * * ?",
        "priority": "lowest",
        "workerName": "LoopSampleWorker"
      },
    ]
  }
}

Job Parameter Description

  • Jobs - A structure keyed by module name, with Job configurations as arrays.
  • name - The name of the Job, not mandatory.
  • enable - Whether to enable this task.
  • trigger - The triggering method, refer to Supported Job Types.
  • workerName - The name in the DI container for the business logic class to be executed after the Job is triggered.
  • timeout - Default is 30 seconds. It is the timeout for executing the specific business logic. Jobs of the Startup type are not limited by this.
  • extend - A dictionary of type Dictionary<string, string> that provides additional property extensions for this Job.
  • Subclasses of Job can implement equivalent configurations in the configuration.

Configuration for TimeWatcher

{
    "TimeWatcher": {
        "Delay": "00:00:30" 
  },
}

  • The node can be omitted and the default value will be used.
  • TimeWatcher will run after a 30-second delay by default.
  • TimeWatcher monitors Jobs of the Interval and Schedulable types.

Plugin Configuration

{
    "Plugins": {
        "PluginsRoot": "Plugins",
        "DisableAutoload": true, 
        "AssemblyNames": {
            "SampleJobs": true,
            "DDNSJob": false
        }
    }
}
  • PluginsRoot - The root directory name for plugins, default is Plugins.
  • DisableAutoload - When set to true, it will load plugins based on the AssemblyNames configuration instead of automatically loading them.
  • AssemblyNames - Specifies which plugins to load.
    • If false, it will automatically load all plugins under the PluginsRoot directory.

Common Parameter Configuration

{
 "Resources": [
   {
     "name": "NetFile",
     "type": "Http",
     "value": "http://127.0.0.1/{0:yyyyMMdd}.txt"
   },
   {
     "name": "LocalFile",
     "type": "Local",
     "value": "Data\\{0:yyyyMMdd}_{1}.txt",
     "params": {
       "encoding": "gb2312",
       "account": "admin",
       "password": "admin1234.com"
     }
   },
   {
     "name": "RankDb",
     "type": "SqlServer",
     "value": "Data Source=192.168.1.50;Initial Catalog=DbRank;User ID=sa;Password=sa;App=WST_SG2Rank V1.0"
   }
 ]
}

Obtain Configuration Information

var resources = Services.GetService<IOptions<ResourcesOption>>().Value;

Dynamic Configuration Loading

Dynamic configuration loading is not supported at the moment. If you modify the configuration, you must restart the application.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.0.0-beta.260130.2 68 1/30/2026
8.0.0-beta.260127.1 64 1/27/2026
8.0.0-beta.251110.1 378 11/10/2025