Skip to content
Merged
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
28 changes: 21 additions & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ root = true

[*]
charset = utf-8
insert_final_newline = true

# C# files
[*.cs]
Expand All @@ -15,8 +16,9 @@ indent_style = space
tab_width = 4

# New line preferences
end_of_line = lf
insert_final_newline = false
insert_final_newline = true
trim_trailing_whitespace = true


#### .NET Coding Conventions ####

Expand All @@ -37,7 +39,7 @@ dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent

# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning

# Expression-level preferences
csharp_style_deconstructed_variable_declaration = true:suggestion
Expand Down Expand Up @@ -65,9 +67,9 @@ dotnet_code_quality_unused_parameters = all:suggestion
#### C# Coding Conventions ####

# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = true:silent
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent

# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
Expand All @@ -87,7 +89,7 @@ csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion

# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async

# Code-block preferences
csharp_prefer_braces = true:silent
Expand All @@ -100,6 +102,12 @@ csharp_style_prefer_range_operator = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent

# C# 10
csharp_style_namespace_declarations = file_scoped:error
csharp_style_prefer_primary_constructors = true
dotnet_diagnostic.IDE0290.severity = error


#### C# Formatting Rules ####

# New line preferences
Expand Down Expand Up @@ -146,3 +154,9 @@ csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true


[*.csproj]
indent_size = 2
indent_style = space
tab_width = 2
6 changes: 3 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- run: dotnet --info
- name: Restore dependencies
run: dotnet restore
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/PreRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v3
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v3
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
28 changes: 10 additions & 18 deletions src/CronBackgroundServices/CronBackgroundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,16 @@

namespace CronBackgroundServices;

internal class CronBackgroundService : BackgroundService
internal class CronBackgroundService(IRecurringAction Action, ILogger logger) : BackgroundService
{
protected readonly IRecurringAction Action;
private readonly ILogger _logger;
private readonly Timing _timing;
private readonly Timing _timing = new(Action.GetTimeZoneId());

public CronBackgroundService(IRecurringAction action, ILogger logger)
{
_timing = new Timing(action.GetTimeZoneId());
Action = action;
_logger = logger;
Cron = action.Cron;
_logger.LogTrace($"Using {Cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'");
}

private string Cron { get; }
private string Cron { get; } = Action.Cron;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogTrace(
$"Using {Cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'");
DateTimeOffset? next = null;

do
Expand All @@ -33,7 +24,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
next = _timing.GetNextOccurenceInRelativeTime(Cron);
var uText = _timing.Get10NextOccurrences(Cron);
var logText = $"Ten next occurrences :\n{uText.Aggregate((x, y) => x + "\n" + y)}";
_logger.LogTrace(logText);
logger.LogTrace(logText);
}

if (now > next)
Expand All @@ -44,11 +35,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
logger.LogError(e, e.Message);
}

next = _timing.GetNextOccurenceInRelativeTime(Cron);
_logger.LogTrace($"Next at {next.Value.DateTime.ToLongDateString()} {next.Value.DateTime.ToLongTimeString()}");
logger.LogTrace(next is not null
? $"Next at {next.Value.DateTime.ToLongDateString()} {next.Value.DateTime.ToLongTimeString()}"
: "No more occurences.");
}
else
{
Expand All @@ -57,7 +50,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
// cron occurence (lowest possible: every second)
await Task.Delay(100);
}

} while (!stoppingToken.IsCancellationRequested);
}
}
63 changes: 29 additions & 34 deletions src/CronBackgroundServices/CronBackgroundServices.csproj
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net7.0</TargetFrameworks>
<RootNamespace>CronBackgroundServices</RootNamespace>
<PackageId>CronBackgroundServices</PackageId>
<Authors>John Korsnes</Authors>
<Description>
A .NET Core Background Service using Cron expressions as triggers
</Description>
<PackageTags>dotnetcore</PackageTags>
<PackageProjectUrl>https://github.com/slackbot-net/slackbot.net</PackageProjectUrl>
<License>https://github.com/slackbot-net/slackbot.net/blob/master/LICENSE</License>
<RepositoryUrl>https://github.com/slackbot-net/slackbot.net</RepositoryUrl>
<PackageIconUrl>images/cron.png</PackageIconUrl>
<PackageIcon>cron.png</PackageIcon>
<RepositoryType>git</RepositoryType>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Cronos" Version="0.7.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<None Include="images/cron.png" Pack="true" PackagePath="" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<RootNamespace>CronBackgroundServices</RootNamespace>
<PackageId>CronBackgroundServices</PackageId>
<Authors>John Korsnes</Authors>
<Description>
A .NET Core Background Service using Cron expressions as triggers
</Description>
<PackageTags>dotnetcore</PackageTags>
<PackageProjectUrl>https://github.com/slackbot-net/slackbot.net</PackageProjectUrl>
<License>https://github.com/slackbot-net/slackbot.net/blob/master/LICENSE</License>
<RepositoryUrl>https://github.com/slackbot-net/slackbot.net</RepositoryUrl>
<PackageIconUrl>images/cron.png</PackageIconUrl>
<PackageIcon>cron.png</PackageIcon>
<RepositoryType>git</RepositoryType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Cronos" Version="0.7.1"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0"/>
</ItemGroup>
<ItemGroup>
<None Include="images/cron.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace CronBackgroundServices;

public static class SlackbotWorkerBuilderExtensions
public static class ServiceCollectionExtensions
{
/// <summary>
/// For distributed apps
/// For distributed apps
/// </summary>
public static IServiceCollection AddRecurrer<T>(this IServiceCollection services) where T : class, IRecurringAction
{
Expand All @@ -16,7 +16,7 @@ public static IServiceCollection AddRecurrer<T>(this IServiceCollection services
{
var allRecurrers = s.GetServices<IRecurringAction>();
var single = allRecurrers.First(r => r is T);
var loggerFactory = s.GetService<ILoggerFactory>();
var loggerFactory = s.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<T>();
return new CronBackgroundService(single, logger);
});
Expand Down
33 changes: 16 additions & 17 deletions src/CronBackgroundServices/IRecurringAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,28 @@ namespace CronBackgroundServices;
public interface IRecurringAction
{
/// <summary>
/// The job to be executed at intervals defined by the Cron expression
/// The cron expression (including seconds) as defined by the Cronos library:
/// See https://github.com/HangfireIO/Cronos#cron-format
/// Ex: Every second: */1 * * * * *
/// Ex: Every minute: 0 */1 * * * *
/// Ex: Every midnight: 0 0 */1 * * *
/// Ex: First of every month 0 0 0 1 * *
/// </summary>
/// <returns></returns>
Task Process(CancellationToken stoppingToken);
/// <returns>A valid Cron Expression</returns>
string Cron { get; }

/// <summary>
/// The cron expression (including seconds) as defined by the Cronos library:
/// See https://github.com/HangfireIO/Cronos#cron-format
/// Ex: Every second: */1 * * * * *
/// Ex: Every minute: 0 */1 * * * *
/// Ex: Every midnight: 0 0 */1 * * *
/// Ex: First of every month 0 0 0 1 * *
/// The job to be executed at intervals defined by the Cron expression
/// </summary>
/// <returns>A valid Cron Expression</returns>
string Cron { get; }
/// <returns></returns>
Task Process(CancellationToken stoppingToken);

/// <summary>
/// Optional: The TimeZone in which the Cron expression should be based on.
/// Defaults to UTC (Europe/London or GMT Standard Time)
///
/// NB! When overriding this and targeting versions below .NET 6, use platform specific identifiers
/// If your runtime is .NET 6 or above, it's not required. It will handles the conversion:
/// See https://github.com/dotnet/runtime/pull/49412
/// Optional: The TimeZone in which the Cron expression should be based on.
/// Defaults to UTC (Europe/London or GMT Standard Time)
/// NB! When overriding this and targeting versions below .NET 6, use platform specific identifiers
/// If your runtime is .NET 6 or above, it's not required. It will handles the conversion:
/// See https://github.com/dotnet/runtime/pull/49412
/// </summary>
/// <returns>timezoneId</returns>
string GetTimeZoneId()
Expand Down
18 changes: 8 additions & 10 deletions src/CronBackgroundServices/Timing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

namespace CronBackgroundServices;

internal class Timing
internal class Timing(string timeZoneId)
{
public readonly TimeZoneInfo TimeZoneInfo;

public Timing(string timeZoneId)
{
TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
}
public readonly TimeZoneInfo TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);

public DateTimeOffset RelativeNow(DateTimeOffset? nowutc = null)
{
Expand All @@ -27,11 +22,13 @@ public IEnumerable<string> Get10NextOccurrences(string cron)
var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds);
var fromUtc = DateTime.UtcNow;
var upcoming = new List<DateTime>();
upcoming.AddRange(Get10Occurrences(upcoming, expression, fromUtc, fromUtc.AddMonths(1)));
upcoming.AddRange(GetTenNextsOccurrences(upcoming, expression, fromUtc, fromUtc.AddMonths(1)));
return upcoming.Select(u => $"{u.ToLongDateString()} {u.ToLongTimeString()}");
}

private IEnumerable<DateTime> Get10Occurrences(List<DateTime> upcoming, CronExpression expression, DateTime fromUtc, DateTime toUtc)
private IEnumerable<DateTime> GetTenNextsOccurrences(List<DateTime> upcoming, CronExpression expression,
DateTime fromUtc,
DateTime toUtc)
{
while (true)
{
Expand All @@ -43,9 +40,10 @@ private IEnumerable<DateTime> Get10Occurrences(List<DateTime> upcoming, CronExpr
{
continue;
}

break;
}
return upcoming.Take(10);

return upcoming.Take(10);
}
}