Skip to content
Open
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
29 changes: 0 additions & 29 deletions src/NCronJob/Configuration/Builder/IRuntimeJobBuilder.cs

This file was deleted.

15 changes: 12 additions & 3 deletions src/NCronJob/Configuration/Builder/JobOptionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,22 @@ public IOptionChainerBuilder RunAtStartup(bool shouldCrashOnFailure = true)
return new OptionChainerBuilder(this);
}

internal List<JobOption> GetJobOptions()
internal static ICollection<JobOption> Evaluate(Action<JobOptionBuilder>? options)
{
if (jobOptions.Count == 0)
if (options is null)
{
jobOptions.Add(new JobOption());
return [new JobOption()];
}

var builder = new JobOptionBuilder();

options(builder);

return builder.GetJobOptions();
}

private List<JobOption> GetJobOptions()
{
return jobOptions;
}
}
188 changes: 188 additions & 0 deletions src/NCronJob/Configuration/Builder/JobRegistrator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace NCronJob;

/// <summary>
/// Defines the contract to register jobs in the service collection.
/// </summary>
public interface IJobRegistrator
{
/// <summary>
/// Adds a typed job to the service collection that gets executed based on the given cron expression.
/// </summary>
/// <param name="jobType">The job type. It will be registered scoped into the container.</param>
/// <param name="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
IDependencyAndJobRegistrator AddJob(Type jobType, Action<JobOptionBuilder>? options = null);

/// <summary>
/// Adds an untypedjob 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="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
IDependencyAndJobRegistrator AddJob(Delegate jobDelegate, Action<JobOptionBuilder>? options = null);
}

/// <summary>
/// Extensions for IJobRegistrator.
/// </summary>
public static class IJobRegistratorExtensions
{
/// <summary>
/// Adds a typed job to the service collection that gets executed based on the given cron expression.
/// </summary>
/// <typeparam name="T">The job type. It will be registered scoped into the container.</typeparam>
/// <param name="jobRegistrator"> The job registrator instance.</param>
/// <param name="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
public static IDependencyAndJobRegistrator AddJob<T>(
this IJobRegistrator jobRegistrator,
Action<JobOptionBuilder>? options = null)
where T : class, IJob
{
ArgumentNullException.ThrowIfNull(jobRegistrator);

return jobRegistrator.AddJob(typeof(T), options);
}
}

/// <summary>
/// Defines the contract to register jobs and dependencies in the service collection.
/// </summary>
public interface IDependencyAndJobRegistrator : IJobRegistrator
{
/// <summary>
/// Adds a job that runs after the given job has finished.
/// </summary>
/// <param name="success">Configure a job that runs after the principal job has finished successfully.</param>
/// <param name="faulted">Configure a job that runs after the principal job has faulted. Faulted means that the parent job did throw an exception.</param>
/// <returns>The builder to register more jobs.</returns>
IDependencyAndJobRegistrator ExecuteWhen(
Action<DependencyBuilder>? success = null,
Action<DependencyBuilder>? faulted = null);
}

internal class JobRegistrator : IDependencyAndJobRegistrator
{
private readonly IServiceCollection services;
private readonly ConcurrencySettings settings;
private readonly JobDefinitionCollector jobDefinitionCollector;
private readonly IReadOnlyCollection<JobDefinition> parentJobDefinitions;

public JobRegistrator(
IServiceCollection services,
ConcurrencySettings settings,
JobDefinitionCollector jobDefinitionCollector,
IReadOnlyCollection<JobDefinition> parentJobDefinitions)
{
this.services = services;
this.settings = settings;
this.jobDefinitionCollector = jobDefinitionCollector;
this.parentJobDefinitions = parentJobDefinitions;
}

public IDependencyAndJobRegistrator AddJob(Type jobType, Action<JobOptionBuilder>? options = null)
{
ArgumentNullException.ThrowIfNull(jobType);

var jobDefinitions = AddTypedJobInternal(services, settings, jobType, options);

jobDefinitionCollector.Add(jobDefinitions);

return new JobRegistrator(services, settings, jobDefinitionCollector, jobDefinitions);
}

public IDependencyAndJobRegistrator AddJob(Delegate jobDelegate, Action<JobOptionBuilder>? options = null)
{
ArgumentNullException.ThrowIfNull(jobDelegate);

var jobDefinitions = AddUntypedJobInternal(settings, jobDelegate, options);

jobDefinitionCollector.Add(jobDefinitions);

return new JobRegistrator(services, settings, jobDefinitionCollector, jobDefinitions);
}

public IDependencyAndJobRegistrator ExecuteWhen(
Action<DependencyBuilder>? success = null,
Action<DependencyBuilder>? faulted = null)
{
ExecuteWhenHelper.AddRegistration(
jobDefinitionCollector,
parentJobDefinitions,
success,
faulted);

return this;
}

internal static List<JobDefinition> AddTypedJobInternal(
IServiceCollection services,
ConcurrencySettings settings,
Type jobType,
Action<JobOptionBuilder>? options)
{
ValidateConcurrencySetting(settings, jobType);
services.TryAddScoped(jobType);

return AddJobInternal(
(o) => JobDefinition.CreateTyped(o.Name, jobType, o.Parameter),
options);
}

internal static List<JobDefinition> AddUntypedJobInternal(
ConcurrencySettings settings,
Delegate jobDelegate,
Action<JobOptionBuilder>? options)
{
ValidateConcurrencySetting(settings, jobDelegate.Method);

return AddJobInternal(
(o) => JobDefinition.CreateUntyped(o.Name, jobDelegate),
options);
}

private static List<JobDefinition> AddJobInternal(
Func<JobOption, JobDefinition> definitionBuilder,
Action<JobOptionBuilder>? options)
{
var jobDefinitions = new List<JobDefinition>();

var jobOptions = JobOptionBuilder.Evaluate(options);

foreach (var option in jobOptions)
{
var entry = definitionBuilder(option);
entry.UpdateWith(option);

jobDefinitions.Add(entry);
}

return jobDefinitions;
}

private static void ValidateConcurrencySetting(
ConcurrencySettings settings,
object jobIdentifier)
{
var cachedJobAttributes = jobIdentifier switch
{
Type type => JobAttributeCache.GetJobExecutionAttributes(type),
MethodInfo methodInfo => JobAttributeCache.GetJobExecutionAttributes(methodInfo),
_ => throw new ArgumentException("Invalid job identifier type")

Check warning on line 177 in src/NCronJob/Configuration/Builder/JobRegistrator.cs

View check run for this annotation

Codecov / codecov/patch

src/NCronJob/Configuration/Builder/JobRegistrator.cs#L177

Added line #L177 was not covered by tests
};

var concurrencyAttribute = cachedJobAttributes.ConcurrencyPolicy;
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}).");

Check warning on line 185 in src/NCronJob/Configuration/Builder/JobRegistrator.cs

View check run for this annotation

Codecov / codecov/patch

src/NCronJob/Configuration/Builder/JobRegistrator.cs#L184-L185

Added lines #L184 - L185 were not covered by tests
}
}
}
91 changes: 28 additions & 63 deletions src/NCronJob/Configuration/Builder/NCronJobOptionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace NCronJob;
/// <summary>
/// Represents the builder for the NCronJob options.
/// </summary>
public class NCronJobOptionBuilder : IJobStage, IRuntimeJobBuilder
public class NCronJobOptionBuilder : IJobStage
{
private readonly IServiceCollection services;
private readonly ConcurrencySettings settings;
Expand Down Expand Up @@ -40,7 +40,7 @@ internal NCronJobOptionBuilder(
public IStartupStage<T> AddJob<T>(Action<JobOptionBuilder>? options = null)
where T : class, IJob
{
var jobDefinitions = AddJobInternal(typeof(T), options);
var jobDefinitions = JobRegistrator.AddTypedJobInternal(services, settings, typeof(T), options);

jobDefinitionCollector.Add(jobDefinitions);

Expand All @@ -64,7 +64,7 @@ public IStartupStage<IJob> AddJob(Type jobType, Action<JobOptionBuilder>? option
{
ArgumentNullException.ThrowIfNull(jobType);

var jobDefinitions = AddJobInternal(jobType, options);
var jobDefinitions = JobRegistrator.AddTypedJobInternal(services, settings, jobType, options);

jobDefinitionCollector.Add(jobDefinitions);

Expand All @@ -89,22 +89,36 @@ public NCronJobOptionBuilder AddJob(
ArgumentNullException.ThrowIfNull(jobDelegate);
ArgumentException.ThrowIfNullOrEmpty(cronExpression);

ValidateConcurrencySetting(jobDelegate.Method);

var jobOption = new JobOption
AddJob(jobDelegate, builder =>
{
CronExpression = cronExpression,
TimeZoneInfo = timeZoneInfo
};

var jobDefinition = JobDefinition.CreateUntyped(jobName, jobDelegate);
jobDefinition.UpdateWith(jobOption);

jobDefinitionCollector.Add(jobDefinition);
if (jobName is not null)
{
builder
.WithName(jobName)
.WithCronExpression(cronExpression, timeZoneInfo);
}
else
{
builder.WithCronExpression(cronExpression, timeZoneInfo);
}
});

return this;
}

/// <summary>
/// Adds an untypedjob 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="options">Configures the <see cref="JobOptionBuilder"/>, like the cron expression or parameters that get passed down.</param>
public IDependencyAndJobRegistrator AddJob(
Delegate jobDelegate,
Action<JobOptionBuilder>? options = null)
{
var registrator = new JobRegistrator(services, settings, jobDefinitionCollector, Array.Empty<JobDefinition>());
return registrator.AddJob(jobDelegate, options);
}

/// <summary>
/// Registers the <see cref="IExceptionHandler"/> implementation to the service collection.
/// </summary>
Expand All @@ -117,55 +131,6 @@ public NCronJobOptionBuilder AddExceptionHandler<TExceptionHandler>() where TExc
services.AddSingleton<IExceptionHandler, TExceptionHandler>();
return this;
}

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

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
{
Type type => JobAttributeCache.GetJobExecutionAttributes(type),
MethodInfo methodInfo => JobAttributeCache.GetJobExecutionAttributes(methodInfo),
_ => throw new ArgumentException("Invalid job identifier type")
};

var concurrencyAttribute = cachedJobAttributes.ConcurrencyPolicy;
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}).");
}
}

private List<JobDefinition> AddJobInternal(
Type jobType,
Action<JobOptionBuilder>? options)
{
ValidateConcurrencySetting(jobType);

var jobDefinitions = new List<JobDefinition>();

var builder = new JobOptionBuilder();
options?.Invoke(builder);

services.TryAddScoped(jobType);

var jobOptions = builder.GetJobOptions();

foreach (var option in jobOptions)
{
var entry = JobDefinition.CreateTyped(option.Name, jobType, option.Parameter);
entry.UpdateWith(option);

jobDefinitions.Add(entry);
}

return jobDefinitions;
}
}

/// <summary>
Expand Down
Loading