Skip to content

Commit 9a4952e

Browse files
committed
Added the json config along with fix on source generator rules to strict on cron expression 6th segment
1 parent 22a95ed commit 9a4952e

File tree

10 files changed

+110
-52
lines changed

10 files changed

+110
-52
lines changed

README.md

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public class CleanupJobs(ICleanUpService cleanUpService)
120120
{
121121
private readonly ICleanUpService _cleanUpService = cleanUpService;
122122

123-
[TickerFunction(functionName: "CleanupLogs", cronExpression: "0 0 * * *" )]
123+
[TickerFunction(functionName: "CleanupLogs", cronExpression: "0 0 0 * * *" )]
124124
public asynt Task CleanupLogs(TickerFunctionContext<string> tickerContext, CancellationToken cancellationToken)
125125
{
126126
var logFileName = tickerContext.Request; // output cleanup_example_file.txt
@@ -131,27 +131,20 @@ public class CleanupJobs(ICleanUpService cleanUpService)
131131

132132
> This uses a cron expression to run daily at midnight.
133133

134-
#### 📅 Cron Expression Formats
134+
#### 📅 Cron Expression Format
135135

136-
TickerQ supports both **5-part** and **6-part** cron expressions:
136+
TickerQ uses **6-part** cron expressions with seconds precision:
137137

138-
**5-part format (standard):**
139-
```
140-
minute hour day month day-of-week
141-
```
142-
Examples:
143-
- `"0 0 * * *"` - Daily at midnight
144-
- `"0 */6 * * *"` - Every 6 hours
145-
- `"30 14 * * 1"` - Every Monday at 2:30 PM
146-
147-
**6-part format (with seconds):**
148138
```
149139
second minute hour day month day-of-week
150140
```
141+
151142
Examples:
152143
- `"0 0 0 * * *"` - Daily at midnight (00:00:00)
153144
- `"30 0 0 * * *"` - Daily at 00:00:30 (30 seconds past midnight)
154145
- `"0 0 */2 * * *"` - Every 2 hours on the hour
146+
- `"0 0 */6 * * *"` - Every 6 hours
147+
- `"0 30 14 * * 1"` - Every Monday at 2:30 PM
155148
- `"*/10 * * * * *"` - Every 10 seconds
156149

157150
---
@@ -180,10 +173,10 @@ Schedule Cron Ticker:
180173
await _cronTickerManager.AddAsync(new CronTicker
181174
{
182175
Function = "CleanupLogs",
183-
Expression = "0 */6 * * *", // Every 6 hours (5-part format)
184-
// Or use 6-part format with seconds:
185-
// Expression = "0 0 */6 * * *", // Every 6 hours at :00:00
176+
Expression = "0 0 */6 * * *", // Every 6 hours at :00:00
177+
// Other examples:
186178
// Expression = "*/30 * * * * *", // Every 30 seconds
179+
// Expression = "0 0 0 * * *", // Daily at midnight
187180
Request = TickerHelper.CreateTickerRequest<string>("cleanup_example_file.txt"),
188181
Retries = 2,
189182
RetryIntervals = new[] { 60, 300 }

src/TickerQ.Dashboard/DashboardOptionsBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Text;
3+
using System.Text.Json;
34
using Microsoft.AspNetCore.Builder;
45
using Microsoft.AspNetCore.Cors.Infrastructure;
56
using TickerQ.Dashboard.Authentication;
@@ -20,6 +21,12 @@ public class DashboardOptionsBuilder
2021
public Action<IApplicationBuilder> PreDashboardMiddleware { get; set; }
2122
public Action<IApplicationBuilder> PostDashboardMiddleware { get; set; }
2223

24+
/// <summary>
25+
/// JsonSerializerOptions specifically for Dashboard API endpoints.
26+
/// Separate from request serialization options to prevent user configuration from breaking dashboard APIs.
27+
/// </summary>
28+
internal JsonSerializerOptions DashboardJsonOptions { get; set; }
29+
2330
public void SetCorsPolicy(Action<CorsPolicyBuilder> corsPolicyBuilder)
2431
=> CorsPolicyBuilder = corsPolicyBuilder;
2532

@@ -74,6 +81,19 @@ public DashboardOptionsBuilder WithSessionTimeout(int minutes)
7481
return this;
7582
}
7683

84+
/// <summary>
85+
/// Configure JSON serialization options for Dashboard API endpoints.
86+
/// These are separate from request serialization to maintain dashboard integrity.
87+
/// </summary>
88+
/// <param name="configure">Action to configure JsonSerializerOptions for dashboard APIs</param>
89+
/// <returns>The DashboardOptionsBuilder for method chaining</returns>
90+
public DashboardOptionsBuilder ConfigureDashboardJsonOptions(Action<JsonSerializerOptions> configure)
91+
{
92+
DashboardJsonOptions ??= new JsonSerializerOptions();
93+
configure?.Invoke(DashboardJsonOptions);
94+
return this;
95+
}
96+
7797
/// <summary>Validate the authentication configuration</summary>
7898
internal void Validate()
7999
{

src/TickerQ.Dashboard/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Extensions.FileProviders;
1010
using TickerQ.Dashboard.Authentication;
1111
using TickerQ.Dashboard.Endpoints;
12+
using TickerQ.Dashboard.Infrastructure;
1213
using System.Text.Json;
1314
using System.Text.RegularExpressions;
1415
using TickerQ.Utilities.Entities;
@@ -21,6 +22,24 @@ internal static void AddDashboardService<TTimeTicker, TCronTicker>(this IService
2122
where TTimeTicker : TimeTickerEntity<TTimeTicker>, new()
2223
where TCronTicker : CronTickerEntity, new()
2324
{
25+
// Configure default Dashboard JSON options if not already configured
26+
if (config.DashboardJsonOptions == null)
27+
{
28+
config.DashboardJsonOptions = new JsonSerializerOptions
29+
{
30+
PropertyNameCaseInsensitive = true,
31+
Converters = { new StringToByteArrayConverter() }
32+
};
33+
}
34+
else
35+
{
36+
// Ensure StringToByteArrayConverter is always present
37+
if (!config.DashboardJsonOptions.Converters.Any(c => c is StringToByteArrayConverter))
38+
{
39+
config.DashboardJsonOptions.Converters.Add(new StringToByteArrayConverter());
40+
}
41+
}
42+
2443
// Register the dashboard configuration for DI
2544
services.AddSingleton(config);
2645
services.AddSingleton(config.Auth);

src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ private static async Task<IResult> GetTimeTickersGraphData<TTimeTicker, TCronTic
294294
private static async Task<IResult> CreateChainJobs<TTimeTicker, TCronTicker>(
295295
HttpContext context,
296296
ITimeTickerManager<TTimeTicker> timeTickerManager,
297+
DashboardOptionsBuilder dashboardOptions,
297298
CancellationToken cancellationToken)
298299
where TTimeTicker : TimeTickerEntity<TTimeTicker>, new()
299300
where TCronTicker : CronTickerEntity, new()
@@ -302,15 +303,8 @@ private static async Task<IResult> CreateChainJobs<TTimeTicker, TCronTicker>(
302303
using var reader = new StreamReader(context.Request.Body);
303304
var jsonString = await reader.ReadToEndAsync(cancellationToken);
304305

305-
// Create JsonSerializerOptions with our custom converter for this endpoint only
306-
var jsonOptions = new JsonSerializerOptions
307-
{
308-
PropertyNameCaseInsensitive = true,
309-
Converters = { new StringToByteArrayConverter() }
310-
};
311-
312-
// Deserialize with custom converter
313-
var chainRoot = JsonSerializer.Deserialize<TTimeTicker>(jsonString, jsonOptions);
306+
// Use Dashboard-specific JSON options
307+
var chainRoot = JsonSerializer.Deserialize<TTimeTicker>(jsonString, dashboardOptions.DashboardJsonOptions);
314308

315309
var result = await timeTickerManager.AddAsync(chainRoot, cancellationToken);
316310

src/TickerQ.Dashboard/Infrastructure/Dashboard/TickerDashboardRepository.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,23 @@ internal class TickerDashboardRepository<TTimeTicker, TCronTicker> : ITickerDash
2525
private readonly TickerExecutionContext _executionContext;
2626
private readonly ITimeTickerManager<TTimeTicker> _timeTickerManager;
2727
private readonly ITickerClock _clock;
28+
private readonly DashboardOptionsBuilder _dashboardOptions;
2829
public TickerDashboardRepository(
2930
TickerExecutionContext executionContext,
3031
ITickerPersistenceProvider<TTimeTicker, TCronTicker> persistenceProvider,
3132
ITickerQHostScheduler tickerQHostScheduler,
32-
ITickerQNotificationHubSender notificationHubSender, ITimeTickerManager<TTimeTicker> timeTickerManager, ITickerClock clock)
33+
ITickerQNotificationHubSender notificationHubSender,
34+
ITimeTickerManager<TTimeTicker> timeTickerManager,
35+
ITickerClock clock,
36+
DashboardOptionsBuilder dashboardOptions)
3337
{
3438
_persistenceProvider = persistenceProvider ?? throw new ArgumentNullException(nameof(persistenceProvider));
3539
_tickerQHostScheduler = tickerQHostScheduler ?? throw new ArgumentNullException(nameof(tickerQHostScheduler));
3640
_notificationHubSender = notificationHubSender ?? throw new ArgumentNullException(nameof(notificationHubSender));
3741
_timeTickerManager = timeTickerManager;
3842
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
3943
_executionContext = executionContext ?? throw new ArgumentNullException(nameof(executionContext));
44+
_dashboardOptions = dashboardOptions ?? throw new ArgumentNullException(nameof(dashboardOptions));
4045
}
4146

4247
public async Task<TTimeTicker[]> GetTimeTickersAsync(CancellationToken cancellationToken)
@@ -669,7 +674,7 @@ public async Task DeleteCronTickerOccurrenceByIdAsync(Guid id, CancellationToken
669674
out var functionTypeContext)) return (jsonRequest, 2);
670675
try
671676
{
672-
JsonSerializer.Deserialize(jsonRequest, functionTypeContext.Item2);
677+
JsonSerializer.Deserialize(jsonRequest, functionTypeContext.Item2, _dashboardOptions.DashboardJsonOptions);
673678
return (jsonRequest, 1);
674679
}
675680
catch
@@ -760,7 +765,7 @@ public async Task UpdateCronTickerAsync(Guid id, UpdateCronTickerRequest request
760765
// Process the request using the function
761766
if (!string.IsNullOrWhiteSpace(request.Request))
762767
{
763-
var serializedRequest = JsonSerializer.Deserialize<object>(request.Request);
768+
var serializedRequest = JsonSerializer.Deserialize<object>(request.Request, _dashboardOptions.DashboardJsonOptions);
764769
cronTicker.Request = TickerHelper.CreateTickerRequest(serializedRequest);
765770
}
766771

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
34
<AssemblyName>TickerQ.SourceGenerator</AssemblyName>
45
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
5-
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
9+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
10+
<PrivateAssets>all</PrivateAssets>
11+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
12+
</PackageReference>
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10.0,)"/>
1014
</ItemGroup>
1115
</Project>

src/TickerQ.SourceGenerator/Validation/CronValidator.cs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,31 @@ namespace TickerQ.SourceGenerator.Validation
55
{
66
public static class CronValidator
77
{
8-
// Format with seconds (6 parts): seconds, minutes, hours, day, month, day-of-week
9-
private static readonly int[] MinValuesArray6Part = { 0, 0, 0, 1, 1, 0 };
10-
private static readonly int[] MaxValuesArray6Part = { 59, 59, 23, 31, 12, 6 };
11-
12-
// Format without seconds (5 parts): minutes, hours, day, month, day-of-week
13-
private static readonly int[] MinValuesArray5Part = { 0, 0, 1, 1, 0 };
14-
private static readonly int[] MaxValuesArray5Part = { 59, 23, 31, 12, 6 };
8+
// Format with seconds (6 parts only): seconds, minutes, hours, day, month, day-of-week
9+
private static readonly int[] MinValues = { 0, 0, 0, 1, 1, 0 };
10+
private static readonly int[] MaxValues = { 59, 59, 23, 31, 12, 6 };
1511

1612
// Performance constants
17-
private const int MaxPartsCount = 6;
13+
private const int RequiredPartsCount = 6;
1814

1915
public static bool IsValidCronExpression(string expression)
2016
{
2117
if (string.IsNullOrEmpty(expression)) return false;
2218

2319
// Use Span for efficient string splitting without allocations
2420
var expressionSpan = expression.AsSpan();
25-
var parts = new string[MaxPartsCount];
21+
var parts = new string[RequiredPartsCount];
2622
var partCount = SplitIntoSpan(expressionSpan, parts);
2723

28-
// Support both 5-part (without seconds) and 6-part (with seconds) cron expressions
29-
if (partCount != 5 && partCount != 6)
24+
// Only support 6-part cron expressions with seconds
25+
if (partCount != RequiredPartsCount)
3026
return false;
3127

32-
// Select appropriate min/max values based on part count
33-
ReadOnlySpan<int> minSpan = partCount == 6
34-
? MinValuesArray6Part
35-
: MinValuesArray5Part;
36-
37-
ReadOnlySpan<int> maxSpan = partCount == 6
38-
? MaxValuesArray6Part
39-
: MaxValuesArray5Part;
28+
// Validate each part with corresponding min/max values
29+
ReadOnlySpan<int> minSpan = MinValues;
30+
ReadOnlySpan<int> maxSpan = MaxValues;
4031

41-
for (int i = 0; i < partCount; i++)
32+
for (int i = 0; i < RequiredPartsCount; i++)
4233
{
4334
if (!ValidatePart(parts[i], minSpan[i], maxSpan[i]))
4435
return false;

src/TickerQ.Utilities/TickerHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ namespace TickerQ.Utilities
99
public static class TickerHelper
1010
{
1111
private static readonly byte[] GZipSignature = [0x1f, 0x8b, 0x08, 0x00];
12+
13+
/// <summary>
14+
/// JsonSerializerOptions specifically for ticker request serialization/deserialization.
15+
/// Can be configured during application startup via TickerOptionsBuilder.
16+
/// </summary>
17+
public static JsonSerializerOptions RequestJsonSerializerOptions { get; set; } = new JsonSerializerOptions();
1218

1319
public static byte[] CreateTickerRequest<T>(T data)
1420
{
@@ -22,7 +28,7 @@ public static byte[] CreateTickerRequest<T>(T data)
2228
Span<byte> compressedBytes;
2329
var serialized = data is byte[] bytes
2430
? bytes
25-
: JsonSerializer.SerializeToUtf8Bytes(data);
31+
: JsonSerializer.SerializeToUtf8Bytes(data, RequestJsonSerializerOptions);
2632

2733
using (var memoryStream = new MemoryStream())
2834
{
@@ -46,7 +52,7 @@ public static T ReadTickerRequest<T>(byte[] gzipBytes)
4652
{
4753
var serializedObject = ReadTickerRequestAsString(gzipBytes);
4854

49-
return JsonSerializer.Deserialize<T>(serializedObject);
55+
return JsonSerializer.Deserialize<T>(serializedObject, RequestJsonSerializerOptions);
5056
}
5157

5258
public static string ReadTickerRequestAsString(byte[] gzipBytes)

src/TickerQ.Utilities/TickerOptionsBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Text.Json;
23
using Microsoft.AspNetCore.Builder;
34
using Microsoft.Extensions.DependencyInjection;
45
using TickerQ.Utilities.Entities;
@@ -28,6 +29,25 @@ public TickerOptionsBuilder<TTimeTicker, TCronTicker> ConfigureScheduler(Action<
2829
schedulerOptionsBuilder?.Invoke(_schedulerOptions);
2930
return this;
3031
}
32+
33+
34+
/// <summary>
35+
/// JsonSerializerOptions specifically for serializing/deserializing ticker requests.
36+
/// If not set, default JsonSerializerOptions will be used.
37+
/// </summary>
38+
internal JsonSerializerOptions RequestJsonSerializerOptions { get; set; }
39+
40+
/// <summary>
41+
/// Configures the JSON serialization options specifically for ticker request serialization/deserialization.
42+
/// </summary>
43+
/// <param name="configure">Action to configure JsonSerializerOptions for ticker requests</param>
44+
/// <returns>The TickerOptionsBuilder for method chaining</returns>
45+
public TickerOptionsBuilder<TTimeTicker, TCronTicker> ConfigureRequestJsonOptions(Action<JsonSerializerOptions> configure)
46+
{
47+
RequestJsonSerializerOptions ??= new JsonSerializerOptions();
48+
configure?.Invoke(RequestJsonSerializerOptions);
49+
return this;
50+
}
3151

3252
public TickerOptionsBuilder<TTimeTicker, TCronTicker> SetExceptionHandler<THandler>() where THandler : ITickerExceptionHandler
3353
{

src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public static IServiceCollection AddTickerQ<TTimeTicker, TCronTicker>(this IServ
3232
var optionInstance = new TickerOptionsBuilder<TTimeTicker,TCronTicker>(tickerExecutionContext, schedulerOptionsBuilder);
3333
optionsBuilder?.Invoke(optionInstance);
3434
CronScheduleCache.TimeZoneInfo = schedulerOptionsBuilder.SchedulerTimeZone;
35+
36+
// Apply JSON serializer options for ticker requests if configured during service registration
37+
if (optionInstance.RequestJsonSerializerOptions != null)
38+
{
39+
TickerHelper.RequestJsonSerializerOptions = optionInstance.RequestJsonSerializerOptions;
40+
}
3541
services.AddSingleton<ITimeTickerManager<TTimeTicker>, TickerManager<TTimeTicker, TCronTicker>>();
3642
services.AddSingleton<ICronTickerManager<TCronTicker>, TickerManager<TTimeTicker, TCronTicker>>();
3743
services.AddSingleton<IInternalTickerManager, InternalTickerManager<TTimeTicker, TCronTicker>>();

0 commit comments

Comments
 (0)