Skip to content

Commit

Permalink
feat!: Abstraction for JobExecutionContext
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Jul 21, 2024
1 parent d81d78f commit 1352e0c
Show file tree
Hide file tree
Showing 36 changed files with 130 additions and 95 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ All notable changes to **NCronJob** will be documented in this file. The project

This is a new major version! A bit of cleanup! Check the `v3` migration guide for more information.

### Changed
- Created abstraction around `JobExecutionContext` to allow for easier testing and mocking. Will be now `IJobExecutionContext`.

### Removed
- Removed `enableSecondPrecision` as it is now inferred automatically.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public class PrintHelloWorld : IJob
this.logger = logger;
}

public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
logger.LogInformation("Hello World");
logger.LogInformation("Parameter: {Parameter}", context.Parameter);
Expand Down
2 changes: 1 addition & 1 deletion docs/features/concurrency-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class ConcurrentJob : IJob
this.logger = logger;
}

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
logger.LogInformation($"ConcurrentJob with Id {context.Id} is running.");
// Simulate some work by delaying
Expand Down
2 changes: 1 addition & 1 deletion docs/features/define-and-schedule-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class MyCronJob : IJob
_logger = logger;
}

public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
_logger.LogInformation("MyCronJob is executing!");

Expand Down
2 changes: 1 addition & 1 deletion docs/features/instant-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ So also CRON jobs can be triggered instantly.
```csharp
public class MyJob : IJob
{
public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ParameterDto dto = (ParameterDto)context.Parameter;
// Do something
Expand Down
4 changes: 2 additions & 2 deletions docs/features/minimal-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ builder.Services.AddNCronJob((ILogger<Program> logger, TimeProvider timeProvider
Also the `JobExecutionContext` and `CancellationToken` can be resolved from the DI container:

```csharp
builder.Services.AddNCronJob((JobExecutionContext context, CancellationToken token) =>
builder.Services.AddNCronJob((IJobExecutionContext context, CancellationToken token) =>
{

}, "*/5 * * * * *");
Expand Down Expand Up @@ -56,7 +56,7 @@ builder.Services.AddNCronJob([RetryPolicy(retryCount: 3)] () => { }, "0 * * * *"
To know in which attempt the job currently is, the `JobExecutionContext` can be used:

```csharp
builder.Services.AddNCronJob([RetryPolicy(retryCount: 3)] (JobExecutionContext context) =>
builder.Services.AddNCronJob([RetryPolicy(retryCount: 3)] (IJobExecutionContext context) =>
{
if(context.Attempts == 1)
{
Expand Down
10 changes: 5 additions & 5 deletions docs/features/model-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The `JobExecutionContext` object passed to the dependent job contains the output
```csharp
public class JobA : IJob
{
public Task ExecuteAsync(JobExecutionContext context)
public Task ExecuteAsync(IJobExecutionContext context)
{
context.Output = "Hello World";
return Task.CompletedTask;
Expand All @@ -43,7 +43,7 @@ public class JobA : IJob

public class JobB : IJob
{
public Task ExecuteAsync(JobExecutionContext context)
public Task ExecuteAsync(IJobExecutionContext context)
{
var parentOutput = context.ParentOutput; // "Hello World"
return Task.CompletedTask;
Expand All @@ -70,7 +70,7 @@ To actively cancel dependent jobs, the `JobExecutionContext` object passed offer
```csharp
public class JobA : IJob
{
public Task ExecuteAsync(JobExecutionContext context)
public Task ExecuteAsync(IJobExecutionContext context)
{
context.SkipChildren();
return Task.CompletedTask;
Expand All @@ -79,7 +79,7 @@ public class JobA : IJob

public class JobB : IJob
{
public Task ExecuteAsync(JobExecutionContext context)
public Task ExecuteAsync(IJobExecutionContext context)
{
// This job will not run
return Task.CompletedTask;
Expand Down Expand Up @@ -110,7 +110,7 @@ If you pass in a `JobExecutionContext` to the dependent job, you can access the
builder.Services.AddNCronJob(options =>
{
options.AddJob<ImportDataJob>().ExecuteWhen(
success: s => s.RunJob(async (JobExecutionContext context, ITransfomerService transformerService) =>
success: s => s.RunJob(async (IJobExecutionContext context, ITransfomerService transformerService) =>
{
var parentOutput = (MyDataModel)context.ParentOutput;
await transformerService.TransformDataAsync(parentOutput);
Expand Down
2 changes: 1 addition & 1 deletion docs/features/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class MyJobNotificationHandler : IJobNotificationHandler<MyJob>
this.logger = logger;
}

public Task HandleAsync(JobExecutionContext context, Exception? exception, CancellationToken token)
public Task HandleAsync(IJobExecutionContext context, Exception? exception, CancellationToken token)
{
if (exception is not null)
{
Expand Down
4 changes: 2 additions & 2 deletions docs/features/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ In the `ReportJob` you can now access the parameter via the `JobExecutionContext
```csharp
public class ReportJob : IJob
{
public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var parameter = context.Parameter;
// Do something with the parameter
Expand Down Expand Up @@ -59,7 +59,7 @@ Services.AddNCronJob(b =>

public class MyJob : IJob
{
public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var myParam = (MyParameter)context.Parameter;
myParam.Counter++; // This will be incremented with each job run
Expand Down
6 changes: 3 additions & 3 deletions docs/features/retry-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Here are examples of how to use the built-in retry policies:
[RetryPolicy(retryCount: 4)]
public class RetryJob(ILogger<RetryJob> logger) : IJob
{
public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var attemptCount = context.Attempts;

Expand All @@ -40,7 +40,7 @@ public class RetryJob(ILogger<RetryJob> logger) : IJob
[RetryPolicy(4, PolicyType.FixedInterval)]
public class FixedIntervalRetryJob(ILogger<FixedIntervalRetryJob> logger) : IJob
{
public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var attemptCount = context.Attempts;

Expand All @@ -64,7 +64,7 @@ You can also create custom retry policies by implementing the `IPolicyCreator` i
[RetryPolicy<MyCustomPolicyCreator>(retryCount:4, delayFactor:1)]
public class CustomPolicyJob(ILogger<CustomPolicyJob> logger) : IJob
{
public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var attemptCount = context.Attempts;

Expand Down
4 changes: 2 additions & 2 deletions docs/features/startup-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Startup jobs are defined like regular CRON jobs and inherit from `IJob`. The onl
```csharp
public class MyStartupJob : IJob
{
public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
// Perform startup task
return Task.CompletedTask;
Expand Down Expand Up @@ -43,7 +43,7 @@ public class InitialDataLoader : IJob
_dataService = dataService;
}

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
await _dataService.LoadInitialDataAsync();
}
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class PrintHelloWorld : IJob
this.logger = logger;
}

public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
logger.LogInformation("Hello World");
logger.LogInformation("Parameter: {Parameter}", context.Parameter);
Expand Down
14 changes: 13 additions & 1 deletion docs/migration/v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@ Inside `WithCronExpression` was an optional parameter that if set to `true` the
builder.Services.AddNCronJob(
n => n.AddJob<SimpleJob>(
p => p.WithCronExpression("* * * * * *")));
```
```

### `JobExecutionContext` is now `IJobExecutionContext`
The `JobExecutionContext` class isn't used anymore. It was replaced by an interface `IJobExecutionContext`. This change was made to allow for easier testing and mocking of the context. Until now you always had to pass in a valid instance which was a bit cumbersome. Now you can mock the interface (or provide a fake) and pass it in.

For the migration, you just have to change the type of the parameter in your job from `JobExecutionContext` to `IJobExecutionContext`:

```diff
- public Task RunAsync(JobExecutionContext context, CancellationToken token)
+ public Task RunAsync(IJobExecutionContext context, CancellationToken token)
```

All the public members are still the same as before.
2 changes: 1 addition & 1 deletion sample/NCronJobSample/HelloWorldJobHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class HelloWorldJobHandler : IJobNotificationHandler<PrintHelloWo

public HelloWorldJobHandler(ILogger<HelloWorldJobHandler> logger) => this.logger = logger;

public Task HandleAsync(JobExecutionContext context, Exception? exception, CancellationToken cancellationToken)
public Task HandleAsync(IJobExecutionContext context, Exception? exception, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/NCronJobSample/Jobs/ConcurrentTaskExecutorJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class ConcurrentTaskExecutorJob : IJob

public ConcurrentTaskExecutorJob(ILogger<ConcurrentTaskExecutorJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/NCronJobSample/Jobs/MultiInstanceJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public partial class MultiInstanceJob : IJob

public MultiInstanceJob(ILogger<MultiInstanceJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
4 changes: 2 additions & 2 deletions sample/NCronJobSample/Jobs/PrintHelloWorldJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class PrintHelloWorldJob : IJob

public PrintHelloWorldJob(ILogger<PrintHelloWorldJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand All @@ -31,7 +31,7 @@ public partial class DataProcessingJob : IJob

public DataProcessingJob(ILogger<DataProcessingJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/NCronJobSample/Jobs/TestCancellationJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public partial class TestCancellationJob : IJob

public TestCancellationJob(ILogger<TestCancellationJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/NCronJobSample/Jobs/TestRetryJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class TestRetryJob(ILogger<TestRetryJob> logger, int maxFailuresBeforeSuc
/// <summary>
/// Runs the job, simulating failures based on a retry count. Will fail 3 times and then succeed.
/// </summary>
public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/RunOnceSample/Jobs/RunAtStartJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class RunAtStartJob : IJob

public RunAtStartJob(ILogger<RunAtStartJob> logger) => this.logger = logger;

public async Task RunAsync(JobExecutionContext context, CancellationToken token)
public async Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
2 changes: 1 addition & 1 deletion sample/RunOnceSample/Jobs/RunAtStartJobHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class RunAtStartJobHandler : IJobNotificationHandler<RunAtStartJo

public RunAtStartJobHandler(ILogger<RunAtStartJobHandler> logger) => this.logger = logger;

public Task HandleAsync(JobExecutionContext context, Exception? exception, CancellationToken cancellationToken)
public Task HandleAsync(IJobExecutionContext context, Exception? exception, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);

Expand Down
4 changes: 2 additions & 2 deletions src/NCronJob/Configuration/DynamicJobFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public DynamicJobFactory(IServiceProvider serviceProvider, Delegate jobAction)

Func<IServiceProvider, object>?[] BuildServiceResolvers() =>
parameters.Select(p =>
p.ParameterType == typeof(JobExecutionContext) || p.ParameterType == typeof(CancellationToken)
p.ParameterType == typeof(IJobExecutionContext) || p.ParameterType == typeof(CancellationToken)
? null
: new Func<IServiceProvider, object>(sp => sp.GetRequiredService(p.ParameterType))
).ToArray();
Expand Down Expand Up @@ -55,7 +55,7 @@ private static Func<object[], Task> BuildInvoker(Delegate jobDelegate)
throw new InvalidOperationException("The job action must return a Task or void type.");
}

public Task RunAsync(JobExecutionContext context, CancellationToken token)
public Task RunAsync(IJobExecutionContext context, CancellationToken token)
{
var arguments = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
Expand Down
2 changes: 1 addition & 1 deletion src/NCronJob/IJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ public interface IJob
/// </summary>
/// <param name="context">The context of the job execution.</param>
/// <param name="token">A cancellation token that can be used to cancel the job.</param>
Task RunAsync(JobExecutionContext context, CancellationToken token);
Task RunAsync(IJobExecutionContext context, CancellationToken token);
}
45 changes: 45 additions & 0 deletions src/NCronJob/IJobExecutionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace NCronJob;

/// <summary>
/// Represents the context of a job execution.
/// </summary>
public interface IJobExecutionContext
{
/// <summary>
/// The Job Instance Identifier, generated once upon creation of the context.
/// </summary>
Guid Id { get; }

/// <summary>
/// The output of a job that can be read by the <see cref="IJobNotificationHandler{TJob}"/>.
/// </summary>
object? Output { get; set; }

/// <summary>
/// The attempts made to execute the job for one run. Will be incremented when a retry is triggered.
/// Retries will only occur when <see cref="RetryPolicyAttribute{T}"/> is set on the Job.
/// </summary>
int Attempts { get; }

/// <summary>The passed in parameters to a job.</summary>
object? Parameter { get; }

/// <summary>
/// The correlation identifier of the job run. The <see cref="CorrelationId"/> stays the same for jobs and their dependencies.
/// </summary>
Guid CorrelationId { get; }

/// <summary>
/// The output of the parent job. Is always <c>null</c> if the job was not started due to a dependency.
/// </summary>
object? ParentOutput { get; }

/// <summary>
/// Prohibits the execution of dependent jobs.
/// </summary>
/// <remarks>
/// Calling <see cref="JobExecutionContext.SkipChildren"/> has no effects when no dependent jobs are defined
/// via <see cref="INotificationStage{TJob}.ExecuteWhen"/>.
/// </remarks>
void SkipChildren();
}
2 changes: 1 addition & 1 deletion src/NCronJob/IJobNotificationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface IJobNotificationHandler
/// <remarks>
/// The method will be invoked with the same scope as the job itself.
/// </remarks>
Task HandleAsync(JobExecutionContext context, Exception? exception, CancellationToken cancellationToken);
Task HandleAsync(IJobExecutionContext context, Exception? exception, CancellationToken cancellationToken);
}

/// <summary>
Expand Down
Loading

0 comments on commit 1352e0c

Please sign in to comment.