Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 #120

Merged
merged 10 commits into from
Dec 28, 2024
Merged

v4 #120

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: .NET

on:
push:
branches: [ main ]
branches: [ main, v4 ]
pull_request:
branches: [ main ]
branches: [ main, v4 ]

env:
VSTEST_CONNECTION_TIMEOUT: 180
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ All notable changes to **NCronJob** will be documented in this file. The project

## [Unreleased]

New `v4` release with some new features and improvements. Check the [`v4` migration guide](https://docs.ncronjob.dev/migration/v4/) for more information.

### Added

- Optionally make startup jobs run early

### Changed

- `IRuntimeRegistry.AddJob` is now called `TryRegister` to better reflect its behavior.
- Explicit handling of duplicates
- Make `UseNCronJobAsync` mandatory when startup jobs have been defined

## [v3.3.8] - 2024-11-16

### Changed
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ builder.Services.AddNCronJob((ILoggerFactory factory, TimeProvider timeProvider)
var logger = factory.CreateLogger("My Anonymous Job");
logger.LogInformation("Hello World - The current date and time is {Time}", timeProvider.GetLocalNow());
}, "*/5 * * * * *");

var app = builder.Build();
await app.UseNCronJobAsync();
app.Run();
```

With this simple lambda, you can define a job that runs every 5 seconds. Pass in all dependencies, just like you would with a Minimal API.
Expand Down Expand Up @@ -155,6 +159,11 @@ builder.Services.AddNCronJob(options =>
options.AddJob<MyJob>()
.RunAtStartup();
});

var app = builder.Build();
// Here the startup jobs will be executed
await app.UseNCronJobAsync();
app.Run();
```

In this example, the job of type 'MyJob' will be executed as soon as the application starts. This is
Expand Down
8 changes: 7 additions & 1 deletion docs/advanced/dynamic-job-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ To add a job at runtime, leverage the `IRuntimeJobRegistry` interface:
```csharp
app.MapPost("/add-job", (IRuntimeJobRegistry registry) =>
{
registry.AddJob(n => n.AddJob<SampleJob>(p => p.WithCronExpression("* * * * *").WithName("MyName")));
var hasSucceeded = registry.TryRegister(n => n.AddJob<SampleJob>(p => p.WithCronExpression("* * * * *").WithName("MyName")), out Exception? exc);

if (!hasSucceeded)
{
return TypedResults.Error(exc?.Message);
}

return TypedResults.Ok();
});
```
Expand Down
4 changes: 4 additions & 0 deletions docs/features/minimal-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ In the same way, the concurrency level can be controlled (see [**Concurrency**](

```csharp
builder.Services.AddNCronJob([SupportsConcurrency(2)] () => { }, "0 * * * *");

var app = builder.Build();
await app.UseNCronJobAsync();
app.Run();
```

Now, the job can only be executed by two instances at the same time.
Expand Down
14 changes: 11 additions & 3 deletions docs/features/startup-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,28 @@ public class MyStartupJob : IJob
As with CRON jobs, they must be registered in the `AddNCronJob` method.

```csharp
Services.AddNCronJob(options =>
builder.Services.AddNCronJob(options =>
{
options.AddJob<MyStartupJob>()
.RunAtStartup(); // Configure the job to run at startup
});

var app = builder.Build();
// Execute all startup jobs
await app.UseNCronJobAsync();
app.Run();
```

The `RunAtStartup` method ensures that the job is executed as soon as the application starts. This method is useful for scenarios where certain tasks need to be performed immediately upon application launch.
The `RunAtStartup` in combination with `UseNCronJobAsync` method ensures that the job is executed as soon as the application starts. This method is useful for scenarios where certain tasks need to be performed immediately upon application launch.

Failure to call `UseNCronJobAsync` when startup jobs are defined will lead to a fatal exception during the application start.

## Example Use Case

Consider an application that needs to load initial data from a database or perform some cleanup tasks whenever it starts. You can define and configure a startup job to handle this:

### Job Definition

```csharp
public class InitialDataLoader : IJob
{
Expand Down Expand Up @@ -67,6 +75,6 @@ This setup ensures that the `InitialDataLoader` job will be executed as soon as

## Summary

Startup jobs are a powerful feature of **NCronJob** that enable you to execute critical tasks immediately upon application startup. By using the `RunAtStartup` method, you can ensure that your application performs necessary setup procedures, data loading, or cleanup tasks right at the beginning of its lifecycle.
Startup jobs are a powerful feature of **NCronJob** that enable you to execute critical tasks immediately before application startup. By using the `RunAtStartup` method, you can ensure that your application performs necessary setup procedures, data loading, or cleanup tasks right at the beginning of its lifecycle.

This feature is particularly useful for applications that require certain operations to be completed before they are fully functional. By configuring startup jobs, you can streamline your application's initialization process and improve its overall reliability and performance.
8 changes: 6 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ public class PrintHelloWorld : IJob
```

## 3. Register the service and the job
The **NCronJob** library provides one easy entry point for all its magic, the `AddNCronJob` extension method on top of the `IServiceCollection` interface.
The **NCronJob** library provides one easy entry point for all its magic, the `AddNCronJob` extension method on top of the `IServiceCollection` interface. Additionally call the `UseNCronJobAsync` method.

```csharp
Services.AddNCronJob(options =>
builder.Services.AddNCronJob(options =>
{
options.AddJob<PrintHelloWorld>(j =>
{
Expand All @@ -53,6 +53,10 @@ Services.AddNCronJob(options =>
.WithParameter("Hello World");
}));
});

var app = builder.Build();
await app.UseNCronJobAsync();
app.Run();
```

Now your `PrintHelloWorld` job will run every minute and log "Hello World" to the console. And that is all!
Expand Down
79 changes: 79 additions & 0 deletions docs/migration/v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# v4 Migration Guide

This document describes the changes made in `v4` of **NCronJob** and how to migrate from `v3`.

Version 4 of **NCronJob** introduces some breaking changes to improve the API.

## `UseNCronJob` and `UseNCronJobAsync` methods
Following the classical ASP.NET pattern, the `UseNCronJob` and `UseNCronJobAsync` methods have been introduced to the `IHost` interface. The minimal setup now looks like this:

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddNCronJob(...);

var app = builder.Build();
await app.UseNCronJobAsync();
app.Run();
```

Instead of the async method `UseNCronJobAsync`, you can also use the synchronous `UseNCronJob` method. The difference is that the async method returns a `Task` that you can await.

## `RunAtStartup` method
The `RunAtStartup` method has a bit different semantics than before. All startup jobs are now executed when `UseNCronJob` or `UseNCronJobAsync` is called. This ensures that all startup jobs are completed before any other part of the application is executed.

## `AddNCronJob(Delegate)` move to `NCronJobExtensions`
The `AddNCronJob(Delegate)` (Minimal API) was a different static class than the `AddNCronJob(Action)` version. This has been fixed by moving the `AddNCronJob(Delegate)` method to the `NCronJobExtensions` class. As most developers are not invoking those extensions via the static class, this should not affect most users. Otherwise the migration is simple:

```diff
- IServiceCollection.AddNCronJob(() => {}, "* * * * *");
+ NCronJobExtensions.AddNCronJob(services, () => {}, "* * * * *");
```

## `IRuntimeRegistry` is more restrictive
In `v3` the `IRuntimeRegistry` offered the ability to use the whole `NCronJobOptionBuilder` which led to confusion, especially if done something like this:

```csharp
runtimeRegistry.AddJob(n => n.AddJob<MyJob>(...).RunAtStartup().AddExceptionHandler<MyHandler>());
```

It didn't make sense to add a startup job during runtime. Also adding exception handlers during runtime was out of scope for this feature. Therefore the interface is more restrictive now and only allows to add jobs.

## `IRuntimeRegistry`s `AddJob` is now called `TryRegister`
The `AddJob` method of the `IRuntimeRegistry` has been renamed to `Register` to better reflect its purpose and to avoid the convoluted naming.

```diff
- runtimeRegistry.AddJob(r => r.AddJob);
+ runtimeRegistry.TryRegister(r => r.AddJob);
```

Not only that there is another overload:
```csharp
+ runtimeRegistry.TryRegister(r => r.AddJob, out Exception? exc);
```

The new `TryRegister` method returns a boolean indicating if the registration was successful and an exception if the registration failed. This can happen if the same configuration of a job is already configured (like same job type, with same cron expression and parameter).

### Chaining was removed
Additionally, the chaining of the former `Add` (now `TryRegister`) method was removed. If the first job registration was successful, but the second failed, the first job was still registered. This seemed arbitrary and was removed.

Each chain should be its own `TryRegister` call now.

## Registering duplicated jobs will lead to an exception during startup
Given the following job registration:

```csharp
builder.Services.AddNCronJob(r => r.AddJob<MyJob>(p => p
.WithCronExpression("* * * * *")
.And
.WithCronExpression("* * * * *")));
```

In `v3` and earlier the second registration would have been ignored. In `v4` this will lead to an exception. This is to prevent accidental misconfigurations. Especially because jobs, by default, are not executed in parallel without further configuration.
If you want to register the same job multiple times, you can define a custom name for the job:

```csharp
builder.Services.AddNCronJob(r => r.AddJob<MyJob>(p => p
.WithCronExpression("* * * * *")
.And
.WithCronExpression("* * * * *").WithName("MyJob")));
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nav:
- Model dependencies: features/model-dependencies.md
- Exception handler: features/exception-handler.md
- Migration:
- v4 Migration Guide: migration/v4.md
- v3 Migration Guide: migration/v3.md
- v2 Migration Guide: migration/v2.md
- Advanced:
Expand Down
2 changes: 1 addition & 1 deletion sample/MinimalSample/MinimalSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions sample/RunOnceSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@

var app = builder.Build();

await app.UseNCronJobAsync();

await app.RunAsync();
36 changes: 36 additions & 0 deletions src/NCronJob/Configuration/Builder/IRuntimeJobBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace NCronJob;

/// <summary>
/// Represents a builder for adding jobs at runtime.
/// </summary>
public interface IRuntimeJobBuilder
{
/// <summary>
/// Adds a job to the service collection that gets executed based on the given cron expression.
/// If a job with the same configuration is already registered, it will throw an exception.
/// </summary>
/// <param name="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
void AddJob<TJob>(Action<JobOptionBuilder>? options = null) where TJob : class, IJob;

/// <summary>
/// Adds a job to the service collection that gets executed based on the given cron expression.
/// If a job with the same configuration is already registered, it will throw an exception.
/// </summary>
/// <param name="jobType">The type of the job to be added.</param>
/// <param name="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
void AddJob(Type jobType, Action<JobOptionBuilder>? options = null);

/// <summary>
/// Adds a job using an asynchronous anonymous delegate to the service collection that gets executed based on the given cron expression.
/// </summary>
/// <param name="jobDelegate">The delegate that represents the job to be executed.</param>
/// <param name="cronExpression">The cron expression that defines when the job should be executed.</param>
/// <param name="timeZoneInfo">The time zone information that the cron expression should be evaluated against.
/// If not set the default time zone is UTC.
/// </param>
/// <param name="jobName">Sets the job name that can be used to identify and manipulate the job later on.</param>
void AddJob(Delegate jobDelegate,
string cronExpression,
TimeZoneInfo? timeZoneInfo = null,
string? jobName = null);
}
42 changes: 29 additions & 13 deletions src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
/// <summary>
/// Represents the builder for the NCronJob options.
/// </summary>
public class NCronJobOptionBuilder : IJobStage
public class NCronJobOptionBuilder : IJobStage, IRuntimeJobBuilder
{
private protected readonly IServiceCollection Services;
private protected readonly ConcurrencySettings Settings;
private readonly IServiceCollection services;
private readonly ConcurrencySettings settings;
private readonly JobRegistry jobRegistry;

internal NCronJobOptionBuilder(
IServiceCollection services,
ConcurrencySettings settings,
JobRegistry jobRegistry)
{
Services = services;
Settings = settings;
this.services = services;
this.settings = settings;
this.jobRegistry = jobRegistry;
}

Expand All @@ -41,7 +41,7 @@
where T : class, IJob
{
var (builder, jobDefinitions) = AddJobInternal(typeof(T), options);
return new StartupStage<T>(Services, jobDefinitions, Settings, jobRegistry, builder);
return new StartupStage<T>(services, jobDefinitions, settings, jobRegistry, builder);
}

/// <summary>
Expand All @@ -60,7 +60,7 @@
public IStartupStage<IJob> AddJob(Type jobType, Action<JobOptionBuilder>? options = null)
{
var (builder, jobDefinitions) = AddJobInternal(jobType, options);
return new StartupStage<IJob>(Services, jobDefinitions, Settings, jobRegistry, builder);
return new StartupStage<IJob>(services, jobDefinitions, settings, jobRegistry, builder);
}

/// <summary>
Expand Down Expand Up @@ -103,10 +103,17 @@
/// </remarks>
public NCronJobOptionBuilder AddExceptionHandler<TExceptionHandler>() where TExceptionHandler : class, IExceptionHandler
{
Services.AddSingleton<IExceptionHandler, TExceptionHandler>();
services.AddSingleton<IExceptionHandler, TExceptionHandler>();
return this;
}

void IRuntimeJobBuilder.AddJob<TJob>(Action<JobOptionBuilder>? options) => AddJob<TJob>(options);

void IRuntimeJobBuilder.AddJob(Type jobType, Action<JobOptionBuilder>? options) => AddJob(jobType, options);

Check warning on line 112 in src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs#L112

Added line #L112 was not covered by tests

void IRuntimeJobBuilder.AddJob(Delegate jobDelegate, string cronExpression, TimeZoneInfo? timeZoneInfo, string? jobName) =>
AddJob(jobDelegate, cronExpression, timeZoneInfo, jobName);

private void ValidateConcurrencySetting(object jobIdentifier)
{
var cachedJobAttributes = jobIdentifier switch
Expand All @@ -117,11 +124,11 @@
};

var concurrencyAttribute = cachedJobAttributes.ConcurrencyPolicy;
if (concurrencyAttribute != null && concurrencyAttribute.MaxDegreeOfParallelism > Settings.MaxDegreeOfParallelism)
if (concurrencyAttribute != null && concurrencyAttribute.MaxDegreeOfParallelism > settings.MaxDegreeOfParallelism)
{
var name = jobIdentifier is Type type ? type.Name : ((MethodInfo)jobIdentifier).Name;
throw new InvalidOperationException(
$"The MaxDegreeOfParallelism for {name} ({concurrencyAttribute.MaxDegreeOfParallelism}) cannot exceed the global limit ({Settings.MaxDegreeOfParallelism}).");
$"The MaxDegreeOfParallelism for {name} ({concurrencyAttribute.MaxDegreeOfParallelism}) cannot exceed the global limit ({settings.MaxDegreeOfParallelism}).");

Check warning on line 131 in src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs#L131

Added line #L131 was not covered by tests
}
}

Expand All @@ -147,7 +154,7 @@
var builder = new JobOptionBuilder();
options?.Invoke(builder);

Services.TryAddScoped(jobType);
services.TryAddScoped(jobType);

var jobOptions = builder.GetJobOptions();

Expand Down Expand Up @@ -313,7 +320,7 @@
/// </code>
/// </example>
IStartupStage<TJob> AddJob<TJob>(Action<JobOptionBuilder>? options = null) where TJob : class, IJob;

/// <summary>
/// Adds a job to the service collection that gets executed based on the given cron expression.
/// </summary>
Expand All @@ -328,9 +335,18 @@
where TJob : class, IJob
{
/// <summary>
/// Configures the job to run once during the application startup before any other jobs.
/// Configures the job to run once before the application itself runs.
/// </summary>
/// <returns>Returns a <see cref="INotificationStage{TJob}"/> that allows adding notifications of another job.</returns>
/// <remarks>
/// If a job is marked to run at startup, it will be executed before any `IHostedService` is started. Use the <seealso cref="NCronJobExtensions.UseNCronJob"/> method to trigger the job execution.
/// In the context of ASP.NET:
/// <code>
/// await app.UseNCronJobAsync();
/// await app.RunAsync();
/// </code>
/// All startup jobs will be executed (and awaited) before the web application is started. This is particular useful for migration and cache hydration.
/// </remarks>
INotificationStage<TJob> RunAtStartup();
}

Expand Down
Loading
Loading