DevelApp.PluginJobRunner.Model
1.0.0
dotnet add package DevelApp.PluginJobRunner.Model --version 1.0.0
NuGet\Install-Package DevelApp.PluginJobRunner.Model -Version 1.0.0
<PackageReference Include="DevelApp.PluginJobRunner.Model" Version="1.0.0" />
<PackageVersion Include="DevelApp.PluginJobRunner.Model" Version="1.0.0" />
<PackageReference Include="DevelApp.PluginJobRunner.Model" />
paket add DevelApp.PluginJobRunner.Model --version 1.0.0
#r "nuget: DevelApp.PluginJobRunner.Model, 1.0.0"
#:package DevelApp.PluginJobRunner.Model@1.0.0
#addin nuget:?package=DevelApp.PluginJobRunner.Model&version=1.0.0
#tool nuget:?package=DevelApp.PluginJobRunner.Model&version=1.0.0
PluginJobRunner
Sample project for RuntimePluggableClassFactory. The short description og RuntimePluggableClassFactory is to make it easier to work on plugin executable frameworks. It is a part of my overall aim that developers mainly work on what gives the business value mainly user experience, business rules, integration, orchistration and data avoiding as much plumming code as possible. Plumming code in my experience accounts for 25-90 % of a developers work and typically gives almost 0 % value for the business.
Project setup
- PluginInterface: Carries the general IJobExecutor and IJobExecutionContext used to make the individual JobExecutors
- JobExecutorModel: Includes the domain model (and because dual database access also the ef database context and migrations)
- JobExecutor: Project for containing the individual JobExecutor implementations (here only the MergeSortArray)
- JobExecutorTest: Project for containing the unit tests for the JobExecutor project
- JobRunner: OpenApi server responsible for scheduling and running the jobs
Design
To make it easier to test the functionality of the individual job function it has been made as a plugin. To facilitate easier use of plugins I have chosen to use a library I have developed for this called RuntimePluggableClassFactory.
The JobRunner is responsible for checking regularly if there has come new jobs in the database, load the JobExecutor and execute the job based on the data. It is not possible to determine if a nano-service system better made on a actor or virtual actor system or a microservice system and as it is not included in the requirements. I have decided to use a WebApi as a microservice for the JobRunner. I will use the Quartz.Net as a hosted service Quartz.Net serving as the timer service. In a production environment this would be inside the secured zone accessable from the DMZ where the Frontend runs.
The Quartz setup and use is modified from Creating a Quartz.NET hosted service with ASP.NET Core by Andrew Lock as his version allows for constructor dependency injection for jobs. His version is explains having the jobs created in the startup which is ok for some jobs but again have the problem that changing a job recycles the application pool. For a configurable job runner that is not acceptable. The problem is mainly a WebApi problem as the service is configured to wait for the jobs finish the run. Use of the [DisallowConcurrentExecution] might cause a problem but also solves a problem on a jobrunner.
The JobRunner Startup on development is set to create the database if missing run migrations on the database
There should be a frontend WebApi server for relaying the Api from the JobRunner service. The Frontend server in a production environment be a frontend server in a DMZ zone. As it would have somewhat the same as is done in the JobRunner in terms of the Api and just asa proxy it is seen as trivial and not included.
Implementation notes
Job processing will be handed off to a job scheduler, in this case a timed version Quartz.Net as it can be configured to run in memory and persisted in database and be used for much more of scheduling.
Logging will be done using Serilog as it is rather configurable and easy to work with. It logs to file in the JobRunner project in the log folder. File logging will not occur if security is set to not allow serilog to create the log folder. Logging levels are different on production and development and controlled in the appsettings.json file and appsettings.Development.json file. Operationswise a dynamic setting of the loglevels would be nice to have as a normal change in the configurations will make a .net server recycle the application pool desturbing execution for normal users.
Unit testing needs to cover business rules or key components. This rule only has one key component which is the sorting of the array. It is possible to make an integration test of putting data into the scheduler but that will not be included.
Unit testing is done using XUnit (base testing), FluentAssertions (failing test readability), AutoFixture (easier unit testing setup), Moq (Mocking framework without shims support)
How to use
Startup in Visual Studio
Press F5 in Visual Studio when solution has started up. Visual studio will startup with Swagger. Under the startup it shows the precise ports used as they might be different depending on the system running it. Default SSL setting on the WebApi needs a selfsigned SSL certificate that Visual Studio can generate automatically.
To use the Api from the OpenApi UI you need to authorize using the green button in the upper right corner with the ApiKey (defined in appsettings which is "Secret"). Click Close when you have entered the key. At this time it is possible to use all the functions of the Api
Migrations
Migrations are created with dotnet ef migrations add InitialCreate -s ../JobRunner -o data -v in the JobExecutorModel project
Migrations are done with dotnet ef database update -s ../JobRunner in the JobExecutorModel project
Packaging and releases
The solution ships as both NuGet packages and an executable:
- DevelApp.PluginJobRunner.Interface (PluginInterface) - the IJobExecutor contract that job plugin projects implement
- DevelApp.PluginJobRunner.Model (JobExecutorModel) - the EF Core job model and JobDbContext
- DevelApp.PluginJobRunner.Executors (JobExecutor) - the sample MergeSortArray executor plugin
Publishing follows the org-standard pipeline (see RuntimePluggableClassFactory). Every push to main packs and signs the packages (Certum cloud code signing) and publishes them to the GitHub Packages feed; stable versions are also published to NuGet.org. The JobRunner itself is an ASP.NET Core web app and is not packed; each GitHub Release also carries self-contained JobRunner binaries (win-x64 zip and linux-x64 tar.gz, with the sample plugins pre-deployed into Plugins) and a container image is pushed to ghcr.io/develapp-ai/pluginjobrunner.
Consuming the packages from the org feed requires authentication with a GitHub PAT with read:packages:
dotnet nuget add source --username <github-user> --password <token> --store-password-in-clear-text --name github-packages https://nuget.pkg.github.com/DevelApp-ai/index.json
Running the container needs the database connection string:
docker run -e "ConnectionStrings__JobDatabase=Server=...;Database=JobDatabase;..." -p 8080:8080 ghcr.io/develapp-ai/pluginjobrunner:latest
Defects and todos
- Use of JsonschemaBuilder for validating input data for job and providing typed support for both input and output. JsonSchemaBuilder might need an upgrade
- ApiKey is read from configuration file
- WebApi and JobRunner writes out unneeded headers "server: Microsoft-IIS/10.0", "x-firefox-spdy: h2", "x-powered-by: ASP.NET" which should be limited to development or removed all together
- jobData and returnedJobData in IJobExecutor is a string instead of inforced as Json. Is JsonValue from Manatee.Json useful ?
- IJobExecutionContext only exists because of mocking framework used for testing does not support shims so mocking framework should be changed
RuntimePluggableClassFactory has two important implementation limitations (for my use it is defects):RESOLVED in v2.1.0All dlls in the plugin library and their dependencies are loaded into memory and not filtered on which are approvedRESOLVED: Now uses IPluginSecurityValidator with multi-level validationPlugin library is only loaded at startup so the application needs to be restarted to add or remove functionalityRESOLVED: v2.0.0 added UnloadPlugin(), UnloadAllPlugins(), and PluginWatcher for hot-swapping
Failing JobExecutor should not be killing the whole application. Stability should be investigated moreRESOLVED: JobExecutorFactory now uses error event handlers (PluginLoadingFailed, SecurityValidationFailed, PluginInstantiationFailed) for graceful degradation without crashes- More consistent naming
- IJobExecutionContext is not implemented and is only a stub
- ApiKey security in own Nuget as used often eventhough it is fairly simple code
- Model should have the Job split into two so that definition JobExecutor and JobData is not in the same place as the execution data JobExecutionDuration but as Quartz.Net data model will take over when jobs gets persisted it is not in focus.
- There is no support for calling a specific version of the JobExecutor and RuntimePluggableClassFactory will always serve the newest version
Things to think about
- Consider if the domain model and the database model should be separated ? Especially when Quartz.Net data model takes over a simplified version seems to be nice for the domain
- Can the webapi be made into executable framwork using plugins for controllers and model ? A form of dynamic controller serving for plugins microfrontend style ?
- JobSchedule.JobType with its Type type is clashing with the naming used in RuntimePluggableClassFactory as it can be different that the class name. Should the RuntimePluggableClassFactory be redone to use the Type type?
- Could (MediatR)[https://www.kiltandcode.com/2021/02/15/using-mediatr-request-handlers-in-aspnet-core-to-decouple-code/] be used to generalize dependency handling in the individual jobs as the context
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. 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. |
-
net9.0
- Microsoft.EntityFrameworkCore.SqlServer (>= 9.0.10)
- serilog.aspnetcore (>= 9.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DevelApp.PluginJobRunner.Model:
| Package | Downloads |
|---|---|
|
DevelApp.PluginJobRunner.Executors
Sample job executor plugins for PluginJobRunner (MergeSortArray), demonstrating the IJobExecutor contract on RuntimePluggableClassFactory. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 44 | 11/12/2026 |