From b8fedaf3fabdbf152e45f606c1c0ff247f80236c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:09:10 +0000 Subject: [PATCH 01/11] Add fluent schedule and cron expression builder Implement a Schedule builder with two modes: - Interval expressions: Schedule.Every(5).Minutes - Cron expressions: Schedule.Cron.Daily(hour: 3) and Schedule.Cron.Create() for custom field-by-field building https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/ScheduleTests.cs | 202 ++++++++++++++++++++++++++++++++ Cronitor/Constants/Schedule.cs | 129 ++++++++++++++++++++ README.md | 25 +++- 3 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 Cronitor.Tests/ScheduleTests.cs create mode 100644 Cronitor/Constants/Schedule.cs diff --git a/Cronitor.Tests/ScheduleTests.cs b/Cronitor.Tests/ScheduleTests.cs new file mode 100644 index 0000000..e000d08 --- /dev/null +++ b/Cronitor.Tests/ScheduleTests.cs @@ -0,0 +1,202 @@ +using Cronitor.Constants; +using Xunit; + +namespace Cronitor.Tests +{ + public class ScheduleTests + { + [Fact] + public void EveryMinute() + { + var result = Schedule.Cron.EveryMinute(); + + Assert.Equal("* * * * *", result); + } + + [Fact] + public void EveryHourAtZero() + { + var result = Schedule.Cron.EveryHour(); + + Assert.Equal("0 * * * *", result); + } + + [Fact] + public void EveryHourAtMinute() + { + var result = Schedule.Cron.EveryHour(minute: 30); + + Assert.Equal("30 * * * *", result); + } + + [Fact] + public void DailyAtMidnight() + { + var result = Schedule.Cron.Daily(); + + Assert.Equal("0 0 * * *", result); + } + + [Fact] + public void DailyAtHour() + { + var result = Schedule.Cron.Daily(hour: 3); + + Assert.Equal("0 3 * * *", result); + } + + [Fact] + public void DailyAtHourAndMinute() + { + var result = Schedule.Cron.Daily(hour: 0, minute: 35); + + Assert.Equal("35 0 * * *", result); + } + + [Fact] + public void WeeklyOnDay() + { + var result = Schedule.Cron.Weekly(day: 1); + + Assert.Equal("0 0 * * 1", result); + } + + [Fact] + public void WeeklyOnDayAtTime() + { + var result = Schedule.Cron.Weekly(day: 1, hour: 8, minute: 30); + + Assert.Equal("30 8 * * 1", result); + } + + [Fact] + public void MonthlyOnDay() + { + var result = Schedule.Cron.Monthly(day: 15); + + Assert.Equal("0 0 15 * *", result); + } + + [Fact] + public void MonthlyOnDayAtTime() + { + var result = Schedule.Cron.Monthly(day: 1, hour: 6, minute: 15); + + Assert.Equal("15 6 1 * *", result); + } + + [Fact] + public void YearlyOnDate() + { + var result = Schedule.Cron.Yearly(month: 1, day: 1); + + Assert.Equal("0 0 1 1 *", result); + } + + [Fact] + public void YearlyOnDateAtTime() + { + var result = Schedule.Cron.Yearly(month: 6, day: 15, hour: 12, minute: 30); + + Assert.Equal("30 12 15 6 *", result); + } + + [Fact] + public void RawExpression() + { + var result = Schedule.Cron.Expression("0 0 * * *"); + + Assert.Equal("0 0 * * *", result); + } + + [Fact] + public void EverySeconds() + { + var result = Schedule.Every(60).Seconds; + + Assert.Equal("every 60 seconds", result); + } + + [Fact] + public void EveryMinutes() + { + var result = Schedule.Every(5).Minutes; + + Assert.Equal("every 5 minutes", result); + } + + [Fact] + public void EveryHours() + { + var result = Schedule.Every(2).Hours; + + Assert.Equal("every 2 hours", result); + } + + [Fact] + public void EveryDays() + { + var result = Schedule.Every(1).Days; + + Assert.Equal("every 1 days", result); + } + + [Fact] + public void CustomCronBuilder() + { + var result = Schedule.Cron.Create() + .Minute(30) + .Hour(3) + .DayOfMonth(15) + .Month(6) + .DayOfWeek(1) + .Build(); + + Assert.Equal("30 3 15 6 1", result); + } + + [Fact] + public void CustomCronBuilderDefaults() + { + var result = Schedule.Cron.Create() + .Minute(0) + .Hour(3) + .Build(); + + Assert.Equal("0 3 * * *", result); + } + + [Fact] + public void CustomCronBuilderWithStep() + { + var result = Schedule.Cron.Create() + .MinuteEvery(15) + .Build(); + + Assert.Equal("*/15 * * * *", result); + } + + [Fact] + public void CustomCronBuilderWithRange() + { + var result = Schedule.Cron.Create() + .Minute(0) + .HourRange(9, 17) + .DayOfWeekRange(1, 5) + .Build(); + + Assert.Equal("0 9-17 * * 1-5", result); + } + + [Fact] + public void CustomCronBuilderToString() + { + var result = Schedule.Cron.Create() + .Minute(0) + .Hour(12) + .ToString(); + + Assert.Equal("0 12 * * *", result); + } + } +} diff --git a/Cronitor/Constants/Schedule.cs b/Cronitor/Constants/Schedule.cs new file mode 100644 index 0000000..b834cfb --- /dev/null +++ b/Cronitor/Constants/Schedule.cs @@ -0,0 +1,129 @@ +namespace Cronitor.Constants +{ + public static class Schedule + { + public static CronBuilder Cron => new CronBuilder(); + public static IntervalBuilder Every(int value) => new IntervalBuilder(value); + } + + public class IntervalBuilder + { + private readonly int _value; + + public IntervalBuilder(int value) + { + _value = value; + } + + public string Seconds => $"every {_value} seconds"; + public string Minutes => $"every {_value} minutes"; + public string Hours => $"every {_value} hours"; + public string Days => $"every {_value} days"; + } + + public class CronBuilder + { + /// + /// Every minute: * * * * * + /// + public string EveryMinute() => "* * * * *"; + + /// + /// Every hour at the given minute: {minute} * * * * + /// + public string EveryHour(int minute = 0) => $"{minute} * * * *"; + + /// + /// Daily at the given time: {minute} {hour} * * * + /// + public string Daily(int hour = 0, int minute = 0) => $"{minute} {hour} * * *"; + + /// + /// Weekly on the given day at the given time: {minute} {hour} * * {day} + /// + public string Weekly(int day, int hour = 0, int minute = 0) => $"{minute} {hour} * * {day}"; + + /// + /// Monthly on the given day at the given time: {minute} {hour} {day} * * + /// + public string Monthly(int day, int hour = 0, int minute = 0) => $"{minute} {hour} {day} * *"; + + /// + /// Yearly on the given month and day at the given time: {minute} {hour} {day} {month} * + /// + public string Yearly(int month, int day, int hour = 0, int minute = 0) => $"{minute} {hour} {day} {month} *"; + + /// + /// Pass a raw cron expression through as-is. + /// + public string Expression(string expression) => expression; + + /// + /// Start building a custom cron expression field by field. + /// + public CronFieldBuilder Create() => new CronFieldBuilder(); + } + + public class CronFieldBuilder + { + private string _minute = "*"; + private string _hour = "*"; + private string _dayOfMonth = "*"; + private string _month = "*"; + private string _dayOfWeek = "*"; + + public CronFieldBuilder Minute(int value) { _minute = value.ToString(); return this; } + public CronFieldBuilder Minute(string value) { _minute = value; return this; } + + public CronFieldBuilder Hour(int value) { _hour = value.ToString(); return this; } + public CronFieldBuilder Hour(string value) { _hour = value; return this; } + + public CronFieldBuilder DayOfMonth(int value) { _dayOfMonth = value.ToString(); return this; } + public CronFieldBuilder DayOfMonth(string value) { _dayOfMonth = value; return this; } + + public CronFieldBuilder Month(int value) { _month = value.ToString(); return this; } + public CronFieldBuilder Month(string value) { _month = value; return this; } + + public CronFieldBuilder DayOfWeek(int value) { _dayOfWeek = value.ToString(); return this; } + public CronFieldBuilder DayOfWeek(string value) { _dayOfWeek = value; return this; } + + /// + /// Set a step value for the minute field: */{step} or {value}/{step} + /// + public CronFieldBuilder MinuteEvery(int step) { _minute = $"*/{step}"; return this; } + + /// + /// Set a step value for the hour field: */{step} or {value}/{step} + /// + public CronFieldBuilder HourEvery(int step) { _hour = $"*/{step}"; return this; } + + /// + /// Set a range for the minute field: {from}-{to} + /// + public CronFieldBuilder MinuteRange(int from, int to) { _minute = $"{from}-{to}"; return this; } + + /// + /// Set a range for the hour field: {from}-{to} + /// + public CronFieldBuilder HourRange(int from, int to) { _hour = $"{from}-{to}"; return this; } + + /// + /// Set a range for the day of month field: {from}-{to} + /// + public CronFieldBuilder DayOfMonthRange(int from, int to) { _dayOfMonth = $"{from}-{to}"; return this; } + + /// + /// Set a range for the month field: {from}-{to} + /// + public CronFieldBuilder MonthRange(int from, int to) { _month = $"{from}-{to}"; return this; } + + /// + /// Set a range for the day of week field: {from}-{to} + /// + public CronFieldBuilder DayOfWeekRange(int from, int to) { _dayOfWeek = $"{from}-{to}"; return this; } + + public string Build() => $"{_minute} {_hour} {_dayOfMonth} {_month} {_dayOfWeek}"; + + public override string ToString() => Build(); + } +} diff --git a/README.md b/README.md index 6a2555b..bd6b7e3 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,32 @@ public class SomeClass ### Suggestions * Add support for Quartz.NET Jobs * Implement Timezone constant (if not too big of a hassle to maintain) -* Implement cron expression-language (if found as needed?) +* ~~Implement cron expression-language (if found as needed?)~~ * Implement Cronitor `assertions`-language (if found as needed?) +### Schedule / Cron Expressions +A fluent builder for schedule expressions is available via the `Schedule` class in `Cronitor.Constants`. It supports both Cronitor interval expressions and standard cron expressions: + +```c# +using Cronitor.Constants; + +// Interval expressions +Schedule.Every(60).Seconds // "every 60 seconds" +Schedule.Every(5).Minutes // "every 5 minutes" + +// Cron shortcuts +Schedule.Cron.Daily(hour: 3) // "0 3 * * *" +Schedule.Cron.Weekly(day: 1, hour: 8) // "0 8 * * 1" +Schedule.Cron.Monthly(day: 15) // "0 0 15 * *" + +// Custom cron expressions +Schedule.Cron.Create() + .MinuteEvery(15) + .HourRange(9, 17) + .DayOfWeekRange(1, 5) + .Build() // "*/15 9-17 * * 1-5" +``` + ## Contributing Pull requests and features are happily considered! By participating in this project you agree to abide by the [Code of Conduct](http://contributor-covenant.org/version/2/0). From d937cc9c52e59a29030e209b52c719aefdb900f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:35:48 +0000 Subject: [PATCH 02/11] Split Schedule into Scheduling folder and add ScheduleExpression type - Move each public class into its own file under Constants/Scheduling/ to fix CS-R1035 (multiple public classes in single file) - Add ScheduleExpression wrapper type with JSON converter and implicit string conversions, following the same pattern as AssertionRule - Change Monitor.Schedule from string to ScheduleExpression - Update Heartbeat constructor to accept ScheduleExpression - Update all test builders and request tests accordingly https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/Builders/CheckBuilder.cs | 7 +- Cronitor.Tests/Builders/HeartbeatBuilder.cs | 7 +- Cronitor.Tests/Builders/JobBuilder.cs | 9 +- Cronitor.Tests/MonitorTypeTests.cs | 8 +- .../Requests/CreateMonitorRequestTests.cs | 3 +- .../Requests/UpdateMonitorRequestTests.cs | 3 +- Cronitor.Tests/ScheduleTests.cs | 97 ++++++++++--- Cronitor/Constants/Schedule.cs | 129 ------------------ Cronitor/Constants/Scheduling/CronBuilder.cs | 45 ++++++ .../Constants/Scheduling/CronFieldBuilder.cs | 65 +++++++++ .../Constants/Scheduling/IntervalBuilder.cs | 17 +++ Cronitor/Constants/Scheduling/Schedule.cs | 8 ++ .../Scheduling/ScheduleExpression.cs | 35 +++++ Cronitor/Models/Monitor.cs | 3 +- Cronitor/Models/Monitors/Heartbeat.cs | 5 +- README.md | 2 +- 16 files changed, 272 insertions(+), 171 deletions(-) delete mode 100644 Cronitor/Constants/Schedule.cs create mode 100644 Cronitor/Constants/Scheduling/CronBuilder.cs create mode 100644 Cronitor/Constants/Scheduling/CronFieldBuilder.cs create mode 100644 Cronitor/Constants/Scheduling/IntervalBuilder.cs create mode 100644 Cronitor/Constants/Scheduling/Schedule.cs create mode 100644 Cronitor/Constants/Scheduling/ScheduleExpression.cs diff --git a/Cronitor.Tests/Builders/CheckBuilder.cs b/Cronitor.Tests/Builders/CheckBuilder.cs index 573bcb8..2b29584 100644 --- a/Cronitor.Tests/Builders/CheckBuilder.cs +++ b/Cronitor.Tests/Builders/CheckBuilder.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Cronitor.Constants; +using Cronitor.Constants.Scheduling; using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders @@ -7,7 +8,7 @@ namespace Cronitor.Tests.Builders public class CheckBuilder { private readonly string _key = "Key"; - private readonly string _schedule = "every 60 seconds"; + private readonly ScheduleExpression _schedule = new ScheduleExpression("every 60 seconds"); private readonly string _timezone = "Europe/Stockholm"; private readonly string _url = "https://www.google.se"; @@ -35,4 +36,4 @@ public Check Build() }; } } -} \ No newline at end of file +} diff --git a/Cronitor.Tests/Builders/HeartbeatBuilder.cs b/Cronitor.Tests/Builders/HeartbeatBuilder.cs index 9b4afca..8e7064c 100644 --- a/Cronitor.Tests/Builders/HeartbeatBuilder.cs +++ b/Cronitor.Tests/Builders/HeartbeatBuilder.cs @@ -1,11 +1,12 @@ -using Cronitor.Models.Monitors; +using Cronitor.Constants.Scheduling; +using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders { public class HeartbeatBuilder { private readonly string _key = "Key"; - private readonly string _schedule = "every 60 seconds"; + private readonly ScheduleExpression _schedule = new ScheduleExpression("every 60 seconds"); private readonly string _timezone = "Europe/Stockholm"; public Heartbeat Build() @@ -16,4 +17,4 @@ public Heartbeat Build() }; } } -} \ No newline at end of file +} diff --git a/Cronitor.Tests/Builders/JobBuilder.cs b/Cronitor.Tests/Builders/JobBuilder.cs index 6c02350..1b66f81 100644 --- a/Cronitor.Tests/Builders/JobBuilder.cs +++ b/Cronitor.Tests/Builders/JobBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Cronitor.Constants.Scheduling; using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders @@ -12,7 +13,7 @@ public class JobBuilder private readonly string _group = "Group"; private readonly string _note = "Note"; private readonly string _platform = "Platform"; - private string _schedule = "35 0 * * *"; + private ScheduleExpression _schedule = new ScheduleExpression("35 0 * * *"); private readonly int? _scheduleTolerance = 1; private readonly string _timeZone = "Europe/Stockholm"; @@ -57,10 +58,10 @@ public JobBuilder Notify(List notify) return this; } - public JobBuilder Schedule(string schedule) + public JobBuilder Schedule(ScheduleExpression schedule) { _schedule = schedule; return this; } } -} \ No newline at end of file +} diff --git a/Cronitor.Tests/MonitorTypeTests.cs b/Cronitor.Tests/MonitorTypeTests.cs index d52574f..2541af6 100644 --- a/Cronitor.Tests/MonitorTypeTests.cs +++ b/Cronitor.Tests/MonitorTypeTests.cs @@ -1,5 +1,5 @@ using Cronitor.Constants; -using Cronitor.Models; +using Cronitor.Constants.Scheduling; using Cronitor.Models.Monitors; using Cronitor.Tests.Helpers; using System.Collections.Generic; @@ -27,7 +27,7 @@ public void ShouldCreateCheckMonitor() Region.Sydney, Region.Virginia }; - const string schedule = "every 60 seconds"; + ScheduleExpression schedule = "every 60 seconds"; const string timezone = "Europe/Stockholm"; const string url = "https://www.google.se"; @@ -54,7 +54,7 @@ public void ShouldCreateCheckMonitor() [Fact] public void ShouldCreateHeartbeatMonitor() { - const string schedule = "every 60 seconds"; + ScheduleExpression schedule = "every 60 seconds"; const string timezone = "Europe/Stockholm"; var monitor = new Heartbeat(MonitorKey, schedule) @@ -83,7 +83,7 @@ public void ShouldCreateJobMonitor() "developers", "administrators" }; - const string schedule = "every 60 seconds"; + ScheduleExpression schedule = "every 60 seconds"; const string timezone = "Europe/Stockholm"; var monitor = new Job(MonitorKey) diff --git a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs index 21efabc..6d0f778 100644 --- a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Cronitor.Constants; +using Cronitor.Constants.Scheduling; using Cronitor.Models; using Cronitor.Requests; using Cronitor.Tests.Helpers; @@ -16,7 +17,7 @@ public async Task ShouldCreateMonitorRequestAsync(string expected) { var monitor = new Monitor("nightly-backup-job") .With(x => x.Type, MonitorType.Job.ToString()) - .With(x => x.Schedule, "0 0 * * *") + .With(x => x.Schedule, new ScheduleExpression("0 0 * * *")) .With(x => x.Notify, new List { "default" }) .With(x => x.Tags, new List { "nightly" }) .With(x => x.Assertions, new List { "metric.duration < 15min" }) diff --git a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs index fc0a886..a3308e0 100644 --- a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Cronitor.Constants; +using Cronitor.Constants.Scheduling; using Cronitor.Models; using Cronitor.Requests; using Cronitor.Tests.Helpers; @@ -17,7 +18,7 @@ public async Task ShouldCreateUpdateMonitorRequestAsync(string expected) { var monitor = new Monitor("nightly-backup-job") .With(x => x.Type, MonitorType.Job.ToString()) - .With(x => x.Schedule, "0 0 * * *") + .With(x => x.Schedule, new ScheduleExpression("0 0 * * *")) .With(x => x.Notify, new List { "default" }) .With(x => x.Tags, new List { "nightly" }) .With(x => x.Assertions, new List { "metric.duration < 15min" }) diff --git a/Cronitor.Tests/ScheduleTests.cs b/Cronitor.Tests/ScheduleTests.cs index e000d08..6965eaa 100644 --- a/Cronitor.Tests/ScheduleTests.cs +++ b/Cronitor.Tests/ScheduleTests.cs @@ -1,4 +1,7 @@ -using Cronitor.Constants; +using Cronitor.Constants.Scheduling; +using Cronitor.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; using Xunit; namespace Cronitor.Tests @@ -10,7 +13,7 @@ public void EveryMinute() { var result = Schedule.Cron.EveryMinute(); - Assert.Equal("* * * * *", result); + Assert.Equal("* * * * *", result.Value); } [Fact] @@ -18,7 +21,7 @@ public void EveryHourAtZero() { var result = Schedule.Cron.EveryHour(); - Assert.Equal("0 * * * *", result); + Assert.Equal("0 * * * *", result.Value); } [Fact] @@ -26,7 +29,7 @@ public void EveryHourAtMinute() { var result = Schedule.Cron.EveryHour(minute: 30); - Assert.Equal("30 * * * *", result); + Assert.Equal("30 * * * *", result.Value); } [Fact] @@ -34,7 +37,7 @@ public void DailyAtMidnight() { var result = Schedule.Cron.Daily(); - Assert.Equal("0 0 * * *", result); + Assert.Equal("0 0 * * *", result.Value); } [Fact] @@ -42,7 +45,7 @@ public void DailyAtHour() { var result = Schedule.Cron.Daily(hour: 3); - Assert.Equal("0 3 * * *", result); + Assert.Equal("0 3 * * *", result.Value); } [Fact] @@ -50,7 +53,7 @@ public void DailyAtHourAndMinute() { var result = Schedule.Cron.Daily(hour: 0, minute: 35); - Assert.Equal("35 0 * * *", result); + Assert.Equal("35 0 * * *", result.Value); } [Fact] @@ -58,7 +61,7 @@ public void WeeklyOnDay() { var result = Schedule.Cron.Weekly(day: 1); - Assert.Equal("0 0 * * 1", result); + Assert.Equal("0 0 * * 1", result.Value); } [Fact] @@ -66,7 +69,7 @@ public void WeeklyOnDayAtTime() { var result = Schedule.Cron.Weekly(day: 1, hour: 8, minute: 30); - Assert.Equal("30 8 * * 1", result); + Assert.Equal("30 8 * * 1", result.Value); } [Fact] @@ -74,7 +77,7 @@ public void MonthlyOnDay() { var result = Schedule.Cron.Monthly(day: 15); - Assert.Equal("0 0 15 * *", result); + Assert.Equal("0 0 15 * *", result.Value); } [Fact] @@ -82,7 +85,7 @@ public void MonthlyOnDayAtTime() { var result = Schedule.Cron.Monthly(day: 1, hour: 6, minute: 15); - Assert.Equal("15 6 1 * *", result); + Assert.Equal("15 6 1 * *", result.Value); } [Fact] @@ -90,7 +93,7 @@ public void YearlyOnDate() { var result = Schedule.Cron.Yearly(month: 1, day: 1); - Assert.Equal("0 0 1 1 *", result); + Assert.Equal("0 0 1 1 *", result.Value); } [Fact] @@ -98,7 +101,7 @@ public void YearlyOnDateAtTime() { var result = Schedule.Cron.Yearly(month: 6, day: 15, hour: 12, minute: 30); - Assert.Equal("30 12 15 6 *", result); + Assert.Equal("30 12 15 6 *", result.Value); } [Fact] @@ -106,7 +109,7 @@ public void RawExpression() { var result = Schedule.Cron.Expression("0 0 * * *"); - Assert.Equal("0 0 * * *", result); + Assert.Equal("0 0 * * *", result.Value); } [Fact] @@ -114,7 +117,7 @@ public void EverySeconds() { var result = Schedule.Every(60).Seconds; - Assert.Equal("every 60 seconds", result); + Assert.Equal("every 60 seconds", result.Value); } [Fact] @@ -122,7 +125,7 @@ public void EveryMinutes() { var result = Schedule.Every(5).Minutes; - Assert.Equal("every 5 minutes", result); + Assert.Equal("every 5 minutes", result.Value); } [Fact] @@ -130,7 +133,7 @@ public void EveryHours() { var result = Schedule.Every(2).Hours; - Assert.Equal("every 2 hours", result); + Assert.Equal("every 2 hours", result.Value); } [Fact] @@ -138,7 +141,7 @@ public void EveryDays() { var result = Schedule.Every(1).Days; - Assert.Equal("every 1 days", result); + Assert.Equal("every 1 days", result.Value); } [Fact] @@ -152,7 +155,7 @@ public void CustomCronBuilder() .DayOfWeek(1) .Build(); - Assert.Equal("30 3 15 6 1", result); + Assert.Equal("30 3 15 6 1", result.Value); } [Fact] @@ -163,7 +166,7 @@ public void CustomCronBuilderDefaults() .Hour(3) .Build(); - Assert.Equal("0 3 * * *", result); + Assert.Equal("0 3 * * *", result.Value); } [Fact] @@ -173,7 +176,7 @@ public void CustomCronBuilderWithStep() .MinuteEvery(15) .Build(); - Assert.Equal("*/15 * * * *", result); + Assert.Equal("*/15 * * * *", result.Value); } [Fact] @@ -185,7 +188,7 @@ public void CustomCronBuilderWithRange() .DayOfWeekRange(1, 5) .Build(); - Assert.Equal("0 9-17 * * 1-5", result); + Assert.Equal("0 9-17 * * 1-5", result.Value); } [Fact] @@ -198,5 +201,55 @@ public void CustomCronBuilderToString() Assert.Equal("0 12 * * *", result); } + + [Fact] + public void ToStringReturnsValue() + { + var expression = Schedule.Cron.Daily(hour: 3); + + Assert.Equal("0 3 * * *", expression.ToString()); + } + + [Fact] + public void ImplicitStringConversion() + { + string result = Schedule.Every(5).Minutes; + + Assert.Equal("every 5 minutes", result); + } + + [Fact] + public void ImplicitScheduleExpressionFromString() + { + ScheduleExpression expression = "0 0 * * *"; + + Assert.Equal("0 0 * * *", expression.Value); + } + + [Fact] + public void SerializesAsJsonString() + { + var expression = Schedule.Cron.Daily(); + + var json = Serializer.Serialize(new { schedule = expression }); + + Assert.Equal("{\"schedule\":\"0 0 * * *\"}", json); + } + + [Fact] + public void DeserializesFromJsonString() + { + var json = "{\"schedule\":\"0 3 * * *\"}"; + + var result = Serializer.Deserialize(json); + + Assert.Equal("0 3 * * *", result.Schedule.Value); + } + + private class ScheduleContainer + { + [JsonPropertyName("schedule")] + public ScheduleExpression Schedule { get; set; } + } } } diff --git a/Cronitor/Constants/Schedule.cs b/Cronitor/Constants/Schedule.cs deleted file mode 100644 index b834cfb..0000000 --- a/Cronitor/Constants/Schedule.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace Cronitor.Constants -{ - public static class Schedule - { - public static CronBuilder Cron => new CronBuilder(); - public static IntervalBuilder Every(int value) => new IntervalBuilder(value); - } - - public class IntervalBuilder - { - private readonly int _value; - - public IntervalBuilder(int value) - { - _value = value; - } - - public string Seconds => $"every {_value} seconds"; - public string Minutes => $"every {_value} minutes"; - public string Hours => $"every {_value} hours"; - public string Days => $"every {_value} days"; - } - - public class CronBuilder - { - /// - /// Every minute: * * * * * - /// - public string EveryMinute() => "* * * * *"; - - /// - /// Every hour at the given minute: {minute} * * * * - /// - public string EveryHour(int minute = 0) => $"{minute} * * * *"; - - /// - /// Daily at the given time: {minute} {hour} * * * - /// - public string Daily(int hour = 0, int minute = 0) => $"{minute} {hour} * * *"; - - /// - /// Weekly on the given day at the given time: {minute} {hour} * * {day} - /// - public string Weekly(int day, int hour = 0, int minute = 0) => $"{minute} {hour} * * {day}"; - - /// - /// Monthly on the given day at the given time: {minute} {hour} {day} * * - /// - public string Monthly(int day, int hour = 0, int minute = 0) => $"{minute} {hour} {day} * *"; - - /// - /// Yearly on the given month and day at the given time: {minute} {hour} {day} {month} * - /// - public string Yearly(int month, int day, int hour = 0, int minute = 0) => $"{minute} {hour} {day} {month} *"; - - /// - /// Pass a raw cron expression through as-is. - /// - public string Expression(string expression) => expression; - - /// - /// Start building a custom cron expression field by field. - /// - public CronFieldBuilder Create() => new CronFieldBuilder(); - } - - public class CronFieldBuilder - { - private string _minute = "*"; - private string _hour = "*"; - private string _dayOfMonth = "*"; - private string _month = "*"; - private string _dayOfWeek = "*"; - - public CronFieldBuilder Minute(int value) { _minute = value.ToString(); return this; } - public CronFieldBuilder Minute(string value) { _minute = value; return this; } - - public CronFieldBuilder Hour(int value) { _hour = value.ToString(); return this; } - public CronFieldBuilder Hour(string value) { _hour = value; return this; } - - public CronFieldBuilder DayOfMonth(int value) { _dayOfMonth = value.ToString(); return this; } - public CronFieldBuilder DayOfMonth(string value) { _dayOfMonth = value; return this; } - - public CronFieldBuilder Month(int value) { _month = value.ToString(); return this; } - public CronFieldBuilder Month(string value) { _month = value; return this; } - - public CronFieldBuilder DayOfWeek(int value) { _dayOfWeek = value.ToString(); return this; } - public CronFieldBuilder DayOfWeek(string value) { _dayOfWeek = value; return this; } - - /// - /// Set a step value for the minute field: */{step} or {value}/{step} - /// - public CronFieldBuilder MinuteEvery(int step) { _minute = $"*/{step}"; return this; } - - /// - /// Set a step value for the hour field: */{step} or {value}/{step} - /// - public CronFieldBuilder HourEvery(int step) { _hour = $"*/{step}"; return this; } - - /// - /// Set a range for the minute field: {from}-{to} - /// - public CronFieldBuilder MinuteRange(int from, int to) { _minute = $"{from}-{to}"; return this; } - - /// - /// Set a range for the hour field: {from}-{to} - /// - public CronFieldBuilder HourRange(int from, int to) { _hour = $"{from}-{to}"; return this; } - - /// - /// Set a range for the day of month field: {from}-{to} - /// - public CronFieldBuilder DayOfMonthRange(int from, int to) { _dayOfMonth = $"{from}-{to}"; return this; } - - /// - /// Set a range for the month field: {from}-{to} - /// - public CronFieldBuilder MonthRange(int from, int to) { _month = $"{from}-{to}"; return this; } - - /// - /// Set a range for the day of week field: {from}-{to} - /// - public CronFieldBuilder DayOfWeekRange(int from, int to) { _dayOfWeek = $"{from}-{to}"; return this; } - - public string Build() => $"{_minute} {_hour} {_dayOfMonth} {_month} {_dayOfWeek}"; - - public override string ToString() => Build(); - } -} diff --git a/Cronitor/Constants/Scheduling/CronBuilder.cs b/Cronitor/Constants/Scheduling/CronBuilder.cs new file mode 100644 index 0000000..bad49f9 --- /dev/null +++ b/Cronitor/Constants/Scheduling/CronBuilder.cs @@ -0,0 +1,45 @@ +namespace Cronitor.Constants.Scheduling +{ + public class CronBuilder + { + /// + /// Every minute: * * * * * + /// + public ScheduleExpression EveryMinute() => new ScheduleExpression("* * * * *"); + + /// + /// Every hour at the given minute: {minute} * * * * + /// + public ScheduleExpression EveryHour(int minute = 0) => new ScheduleExpression($"{minute} * * * *"); + + /// + /// Daily at the given time: {minute} {hour} * * * + /// + public ScheduleExpression Daily(int hour = 0, int minute = 0) => new ScheduleExpression($"{minute} {hour} * * *"); + + /// + /// Weekly on the given day at the given time: {minute} {hour} * * {day} + /// + public ScheduleExpression Weekly(int day, int hour = 0, int minute = 0) => new ScheduleExpression($"{minute} {hour} * * {day}"); + + /// + /// Monthly on the given day at the given time: {minute} {hour} {day} * * + /// + public ScheduleExpression Monthly(int day, int hour = 0, int minute = 0) => new ScheduleExpression($"{minute} {hour} {day} * *"); + + /// + /// Yearly on the given month and day at the given time: {minute} {hour} {day} {month} * + /// + public ScheduleExpression Yearly(int month, int day, int hour = 0, int minute = 0) => new ScheduleExpression($"{minute} {hour} {day} {month} *"); + + /// + /// Pass a raw cron expression through as-is. + /// + public ScheduleExpression Expression(string expression) => new ScheduleExpression(expression); + + /// + /// Start building a custom cron expression field by field. + /// + public CronFieldBuilder Create() => new CronFieldBuilder(); + } +} diff --git a/Cronitor/Constants/Scheduling/CronFieldBuilder.cs b/Cronitor/Constants/Scheduling/CronFieldBuilder.cs new file mode 100644 index 0000000..338d6ad --- /dev/null +++ b/Cronitor/Constants/Scheduling/CronFieldBuilder.cs @@ -0,0 +1,65 @@ +namespace Cronitor.Constants.Scheduling +{ + public class CronFieldBuilder + { + private string _minute = "*"; + private string _hour = "*"; + private string _dayOfMonth = "*"; + private string _month = "*"; + private string _dayOfWeek = "*"; + + public CronFieldBuilder Minute(int value) { _minute = value.ToString(); return this; } + public CronFieldBuilder Minute(string value) { _minute = value; return this; } + + public CronFieldBuilder Hour(int value) { _hour = value.ToString(); return this; } + public CronFieldBuilder Hour(string value) { _hour = value; return this; } + + public CronFieldBuilder DayOfMonth(int value) { _dayOfMonth = value.ToString(); return this; } + public CronFieldBuilder DayOfMonth(string value) { _dayOfMonth = value; return this; } + + public CronFieldBuilder Month(int value) { _month = value.ToString(); return this; } + public CronFieldBuilder Month(string value) { _month = value; return this; } + + public CronFieldBuilder DayOfWeek(int value) { _dayOfWeek = value.ToString(); return this; } + public CronFieldBuilder DayOfWeek(string value) { _dayOfWeek = value; return this; } + + /// + /// Set a step value for the minute field: */{step} + /// + public CronFieldBuilder MinuteEvery(int step) { _minute = $"*/{step}"; return this; } + + /// + /// Set a step value for the hour field: */{step} + /// + public CronFieldBuilder HourEvery(int step) { _hour = $"*/{step}"; return this; } + + /// + /// Set a range for the minute field: {from}-{to} + /// + public CronFieldBuilder MinuteRange(int from, int to) { _minute = $"{from}-{to}"; return this; } + + /// + /// Set a range for the hour field: {from}-{to} + /// + public CronFieldBuilder HourRange(int from, int to) { _hour = $"{from}-{to}"; return this; } + + /// + /// Set a range for the day of month field: {from}-{to} + /// + public CronFieldBuilder DayOfMonthRange(int from, int to) { _dayOfMonth = $"{from}-{to}"; return this; } + + /// + /// Set a range for the month field: {from}-{to} + /// + public CronFieldBuilder MonthRange(int from, int to) { _month = $"{from}-{to}"; return this; } + + /// + /// Set a range for the day of week field: {from}-{to} + /// + public CronFieldBuilder DayOfWeekRange(int from, int to) { _dayOfWeek = $"{from}-{to}"; return this; } + + public ScheduleExpression Build() => new ScheduleExpression($"{_minute} {_hour} {_dayOfMonth} {_month} {_dayOfWeek}"); + + public override string ToString() => Build().Value; + } +} diff --git a/Cronitor/Constants/Scheduling/IntervalBuilder.cs b/Cronitor/Constants/Scheduling/IntervalBuilder.cs new file mode 100644 index 0000000..d8d2be7 --- /dev/null +++ b/Cronitor/Constants/Scheduling/IntervalBuilder.cs @@ -0,0 +1,17 @@ +namespace Cronitor.Constants.Scheduling +{ + public class IntervalBuilder + { + private readonly int _value; + + public IntervalBuilder(int value) + { + _value = value; + } + + public ScheduleExpression Seconds => new ScheduleExpression($"every {_value} seconds"); + public ScheduleExpression Minutes => new ScheduleExpression($"every {_value} minutes"); + public ScheduleExpression Hours => new ScheduleExpression($"every {_value} hours"); + public ScheduleExpression Days => new ScheduleExpression($"every {_value} days"); + } +} diff --git a/Cronitor/Constants/Scheduling/Schedule.cs b/Cronitor/Constants/Scheduling/Schedule.cs new file mode 100644 index 0000000..d3f96d1 --- /dev/null +++ b/Cronitor/Constants/Scheduling/Schedule.cs @@ -0,0 +1,8 @@ +namespace Cronitor.Constants.Scheduling +{ + public static class Schedule + { + public static CronBuilder Cron => new CronBuilder(); + public static IntervalBuilder Every(int value) => new IntervalBuilder(value); + } +} diff --git a/Cronitor/Constants/Scheduling/ScheduleExpression.cs b/Cronitor/Constants/Scheduling/ScheduleExpression.cs new file mode 100644 index 0000000..9eea8c8 --- /dev/null +++ b/Cronitor/Constants/Scheduling/ScheduleExpression.cs @@ -0,0 +1,35 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Cronitor.Constants.Scheduling +{ + [JsonConverter(typeof(ScheduleExpressionConverter))] + public class ScheduleExpression + { + public string Value { get; } + + public ScheduleExpression(string value) + { + Value = value; + } + + public override string ToString() => Value; + + public static implicit operator string(ScheduleExpression expression) => expression?.Value; + public static implicit operator ScheduleExpression(string value) => new ScheduleExpression(value); + } + + public class ScheduleExpressionConverter : JsonConverter + { + public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new ScheduleExpression(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, ScheduleExpression value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + } +} diff --git a/Cronitor/Models/Monitor.cs b/Cronitor/Models/Monitor.cs index ba2705b..d362404 100644 --- a/Cronitor/Models/Monitor.cs +++ b/Cronitor/Models/Monitor.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Text.Json.Serialization; +using Cronitor.Constants.Scheduling; namespace Cronitor.Models { @@ -110,7 +111,7 @@ public class Monitor /// An interval expression must be used. The range of accepted values is 30 seconds to 1 hour.e.g. ‘every 2 minutes’ /// [JsonPropertyName("schedule")] - public string Schedule { get; set; } + public ScheduleExpression Schedule { get; set; } /// /// Number of missed scheduled executions to allow before sending an alert. /// diff --git a/Cronitor/Models/Monitors/Heartbeat.cs b/Cronitor/Models/Monitors/Heartbeat.cs index 7eab02f..5eb66bc 100644 --- a/Cronitor/Models/Monitors/Heartbeat.cs +++ b/Cronitor/Models/Monitors/Heartbeat.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using Cronitor.Constants.Scheduling; namespace Cronitor.Models.Monitors { @@ -7,7 +8,7 @@ public class Heartbeat : Monitor [JsonPropertyName("type")] public override string Type { get; set; } = "event"; - public Heartbeat(string key, string schedule) + public Heartbeat(string key, ScheduleExpression schedule) : base(key) { Schedule = schedule; diff --git a/README.md b/README.md index bd6b7e3..8c33b52 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ public class SomeClass A fluent builder for schedule expressions is available via the `Schedule` class in `Cronitor.Constants`. It supports both Cronitor interval expressions and standard cron expressions: ```c# -using Cronitor.Constants; +using Cronitor.Constants.Scheduling; // Interval expressions Schedule.Every(60).Seconds // "every 60 seconds" From 8dfe47fdd7044f72253c2bccf3d58e08ba1a11aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:37:27 +0000 Subject: [PATCH 03/11] Use implicit string conversion for ScheduleExpression in builders https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/Builders/CheckBuilder.cs | 2 +- Cronitor.Tests/Builders/HeartbeatBuilder.cs | 2 +- Cronitor.Tests/Builders/JobBuilder.cs | 2 +- Cronitor.Tests/MonitorTypeTests.cs | 6 +++--- Cronitor.Tests/Requests/CreateMonitorRequestTests.cs | 2 +- Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cronitor.Tests/Builders/CheckBuilder.cs b/Cronitor.Tests/Builders/CheckBuilder.cs index 2b29584..89c81ea 100644 --- a/Cronitor.Tests/Builders/CheckBuilder.cs +++ b/Cronitor.Tests/Builders/CheckBuilder.cs @@ -8,7 +8,7 @@ namespace Cronitor.Tests.Builders public class CheckBuilder { private readonly string _key = "Key"; - private readonly ScheduleExpression _schedule = new ScheduleExpression("every 60 seconds"); + private readonly ScheduleExpression _schedule = Schedule.Every(60).Seconds; private readonly string _timezone = "Europe/Stockholm"; private readonly string _url = "https://www.google.se"; diff --git a/Cronitor.Tests/Builders/HeartbeatBuilder.cs b/Cronitor.Tests/Builders/HeartbeatBuilder.cs index 8e7064c..13f161c 100644 --- a/Cronitor.Tests/Builders/HeartbeatBuilder.cs +++ b/Cronitor.Tests/Builders/HeartbeatBuilder.cs @@ -6,7 +6,7 @@ namespace Cronitor.Tests.Builders public class HeartbeatBuilder { private readonly string _key = "Key"; - private readonly ScheduleExpression _schedule = new ScheduleExpression("every 60 seconds"); + private readonly ScheduleExpression _schedule = Schedule.Every(60).Seconds; private readonly string _timezone = "Europe/Stockholm"; public Heartbeat Build() diff --git a/Cronitor.Tests/Builders/JobBuilder.cs b/Cronitor.Tests/Builders/JobBuilder.cs index 1b66f81..77c7b10 100644 --- a/Cronitor.Tests/Builders/JobBuilder.cs +++ b/Cronitor.Tests/Builders/JobBuilder.cs @@ -13,7 +13,7 @@ public class JobBuilder private readonly string _group = "Group"; private readonly string _note = "Note"; private readonly string _platform = "Platform"; - private ScheduleExpression _schedule = new ScheduleExpression("35 0 * * *"); + private ScheduleExpression _schedule = Schedule.Cron.Daily(hour: 0, minute: 35); private readonly int? _scheduleTolerance = 1; private readonly string _timeZone = "Europe/Stockholm"; diff --git a/Cronitor.Tests/MonitorTypeTests.cs b/Cronitor.Tests/MonitorTypeTests.cs index 2541af6..7643d48 100644 --- a/Cronitor.Tests/MonitorTypeTests.cs +++ b/Cronitor.Tests/MonitorTypeTests.cs @@ -27,7 +27,7 @@ public void ShouldCreateCheckMonitor() Region.Sydney, Region.Virginia }; - ScheduleExpression schedule = "every 60 seconds"; + var schedule = Schedule.Every(60).Seconds; const string timezone = "Europe/Stockholm"; const string url = "https://www.google.se"; @@ -54,7 +54,7 @@ public void ShouldCreateCheckMonitor() [Fact] public void ShouldCreateHeartbeatMonitor() { - ScheduleExpression schedule = "every 60 seconds"; + var schedule = Schedule.Every(60).Seconds; const string timezone = "Europe/Stockholm"; var monitor = new Heartbeat(MonitorKey, schedule) @@ -83,7 +83,7 @@ public void ShouldCreateJobMonitor() "developers", "administrators" }; - ScheduleExpression schedule = "every 60 seconds"; + var schedule = Schedule.Every(60).Seconds; const string timezone = "Europe/Stockholm"; var monitor = new Job(MonitorKey) diff --git a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs index 6d0f778..08904dd 100644 --- a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs @@ -17,7 +17,7 @@ public async Task ShouldCreateMonitorRequestAsync(string expected) { var monitor = new Monitor("nightly-backup-job") .With(x => x.Type, MonitorType.Job.ToString()) - .With(x => x.Schedule, new ScheduleExpression("0 0 * * *")) + .With(x => x.Schedule, Schedule.Cron.Daily()) .With(x => x.Notify, new List { "default" }) .With(x => x.Tags, new List { "nightly" }) .With(x => x.Assertions, new List { "metric.duration < 15min" }) diff --git a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs index a3308e0..6cdd2a2 100644 --- a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs @@ -18,7 +18,7 @@ public async Task ShouldCreateUpdateMonitorRequestAsync(string expected) { var monitor = new Monitor("nightly-backup-job") .With(x => x.Type, MonitorType.Job.ToString()) - .With(x => x.Schedule, new ScheduleExpression("0 0 * * *")) + .With(x => x.Schedule, Schedule.Cron.Daily()) .With(x => x.Notify, new List { "default" }) .With(x => x.Tags, new List { "nightly" }) .With(x => x.Assertions, new List { "metric.duration < 15min" }) From 8b7066ee9062af3d1fa0b1b36959f44794894ac3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:39:37 +0000 Subject: [PATCH 04/11] Move Scheduling folder to project root with Cronitor.Scheduling namespace https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/Builders/CheckBuilder.cs | 2 +- Cronitor.Tests/Builders/HeartbeatBuilder.cs | 2 +- Cronitor.Tests/Builders/JobBuilder.cs | 2 +- Cronitor.Tests/MonitorTypeTests.cs | 2 +- Cronitor.Tests/Requests/CreateMonitorRequestTests.cs | 2 +- Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs | 2 +- Cronitor.Tests/ScheduleTests.cs | 2 +- Cronitor/Models/Monitor.cs | 2 +- Cronitor/Models/Monitors/Heartbeat.cs | 2 +- Cronitor/{Constants => }/Scheduling/CronBuilder.cs | 2 +- Cronitor/{Constants => }/Scheduling/CronFieldBuilder.cs | 2 +- Cronitor/{Constants => }/Scheduling/IntervalBuilder.cs | 2 +- Cronitor/{Constants => }/Scheduling/Schedule.cs | 2 +- Cronitor/{Constants => }/Scheduling/ScheduleExpression.cs | 2 +- README.md | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) rename Cronitor/{Constants => }/Scheduling/CronBuilder.cs (97%) rename Cronitor/{Constants => }/Scheduling/CronFieldBuilder.cs (98%) rename Cronitor/{Constants => }/Scheduling/IntervalBuilder.cs (93%) rename Cronitor/{Constants => }/Scheduling/Schedule.cs (83%) rename Cronitor/{Constants => }/Scheduling/ScheduleExpression.cs (96%) diff --git a/Cronitor.Tests/Builders/CheckBuilder.cs b/Cronitor.Tests/Builders/CheckBuilder.cs index 89c81ea..b4e2ad0 100644 --- a/Cronitor.Tests/Builders/CheckBuilder.cs +++ b/Cronitor.Tests/Builders/CheckBuilder.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Cronitor.Constants; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders diff --git a/Cronitor.Tests/Builders/HeartbeatBuilder.cs b/Cronitor.Tests/Builders/HeartbeatBuilder.cs index 13f161c..ac2d025 100644 --- a/Cronitor.Tests/Builders/HeartbeatBuilder.cs +++ b/Cronitor.Tests/Builders/HeartbeatBuilder.cs @@ -1,4 +1,4 @@ -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders diff --git a/Cronitor.Tests/Builders/JobBuilder.cs b/Cronitor.Tests/Builders/JobBuilder.cs index 77c7b10..d1998d0 100644 --- a/Cronitor.Tests/Builders/JobBuilder.cs +++ b/Cronitor.Tests/Builders/JobBuilder.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models.Monitors; namespace Cronitor.Tests.Builders diff --git a/Cronitor.Tests/MonitorTypeTests.cs b/Cronitor.Tests/MonitorTypeTests.cs index 7643d48..8b9442e 100644 --- a/Cronitor.Tests/MonitorTypeTests.cs +++ b/Cronitor.Tests/MonitorTypeTests.cs @@ -1,5 +1,5 @@ using Cronitor.Constants; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models.Monitors; using Cronitor.Tests.Helpers; using System.Collections.Generic; diff --git a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs index 08904dd..08a285a 100644 --- a/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/CreateMonitorRequestTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Cronitor.Constants; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models; using Cronitor.Requests; using Cronitor.Tests.Helpers; diff --git a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs index 6cdd2a2..f10b04f 100644 --- a/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs +++ b/Cronitor.Tests/Requests/UpdateMonitorRequestTests.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Cronitor.Constants; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Models; using Cronitor.Requests; using Cronitor.Tests.Helpers; diff --git a/Cronitor.Tests/ScheduleTests.cs b/Cronitor.Tests/ScheduleTests.cs index 6965eaa..e7776c5 100644 --- a/Cronitor.Tests/ScheduleTests.cs +++ b/Cronitor.Tests/ScheduleTests.cs @@ -1,4 +1,4 @@ -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; using Cronitor.Serialization; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/Cronitor/Models/Monitor.cs b/Cronitor/Models/Monitor.cs index d362404..4e36130 100644 --- a/Cronitor/Models/Monitor.cs +++ b/Cronitor/Models/Monitor.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Text.Json.Serialization; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; namespace Cronitor.Models { diff --git a/Cronitor/Models/Monitors/Heartbeat.cs b/Cronitor/Models/Monitors/Heartbeat.cs index 5eb66bc..611e21a 100644 --- a/Cronitor/Models/Monitors/Heartbeat.cs +++ b/Cronitor/Models/Monitors/Heartbeat.cs @@ -1,5 +1,5 @@ using System.Text.Json.Serialization; -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; namespace Cronitor.Models.Monitors { diff --git a/Cronitor/Constants/Scheduling/CronBuilder.cs b/Cronitor/Scheduling/CronBuilder.cs similarity index 97% rename from Cronitor/Constants/Scheduling/CronBuilder.cs rename to Cronitor/Scheduling/CronBuilder.cs index bad49f9..4357f78 100644 --- a/Cronitor/Constants/Scheduling/CronBuilder.cs +++ b/Cronitor/Scheduling/CronBuilder.cs @@ -1,4 +1,4 @@ -namespace Cronitor.Constants.Scheduling +namespace Cronitor.Scheduling { public class CronBuilder { diff --git a/Cronitor/Constants/Scheduling/CronFieldBuilder.cs b/Cronitor/Scheduling/CronFieldBuilder.cs similarity index 98% rename from Cronitor/Constants/Scheduling/CronFieldBuilder.cs rename to Cronitor/Scheduling/CronFieldBuilder.cs index 338d6ad..628618e 100644 --- a/Cronitor/Constants/Scheduling/CronFieldBuilder.cs +++ b/Cronitor/Scheduling/CronFieldBuilder.cs @@ -1,4 +1,4 @@ -namespace Cronitor.Constants.Scheduling +namespace Cronitor.Scheduling { public class CronFieldBuilder { diff --git a/Cronitor/Constants/Scheduling/IntervalBuilder.cs b/Cronitor/Scheduling/IntervalBuilder.cs similarity index 93% rename from Cronitor/Constants/Scheduling/IntervalBuilder.cs rename to Cronitor/Scheduling/IntervalBuilder.cs index d8d2be7..899de41 100644 --- a/Cronitor/Constants/Scheduling/IntervalBuilder.cs +++ b/Cronitor/Scheduling/IntervalBuilder.cs @@ -1,4 +1,4 @@ -namespace Cronitor.Constants.Scheduling +namespace Cronitor.Scheduling { public class IntervalBuilder { diff --git a/Cronitor/Constants/Scheduling/Schedule.cs b/Cronitor/Scheduling/Schedule.cs similarity index 83% rename from Cronitor/Constants/Scheduling/Schedule.cs rename to Cronitor/Scheduling/Schedule.cs index d3f96d1..1d9a326 100644 --- a/Cronitor/Constants/Scheduling/Schedule.cs +++ b/Cronitor/Scheduling/Schedule.cs @@ -1,4 +1,4 @@ -namespace Cronitor.Constants.Scheduling +namespace Cronitor.Scheduling { public static class Schedule { diff --git a/Cronitor/Constants/Scheduling/ScheduleExpression.cs b/Cronitor/Scheduling/ScheduleExpression.cs similarity index 96% rename from Cronitor/Constants/Scheduling/ScheduleExpression.cs rename to Cronitor/Scheduling/ScheduleExpression.cs index 9eea8c8..9003f79 100644 --- a/Cronitor/Constants/Scheduling/ScheduleExpression.cs +++ b/Cronitor/Scheduling/ScheduleExpression.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Cronitor.Constants.Scheduling +namespace Cronitor.Scheduling { [JsonConverter(typeof(ScheduleExpressionConverter))] public class ScheduleExpression diff --git a/README.md b/README.md index 8c33b52..390facd 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ public class SomeClass A fluent builder for schedule expressions is available via the `Schedule` class in `Cronitor.Constants`. It supports both Cronitor interval expressions and standard cron expressions: ```c# -using Cronitor.Constants.Scheduling; +using Cronitor.Scheduling; // Interval expressions Schedule.Every(60).Seconds // "every 60 seconds" From 08a6d3280634f77e8cde58db57d7eb793215cf66 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:43:14 +0000 Subject: [PATCH 05/11] Split ScheduleExpressionConverter into its own file https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor/Scheduling/ScheduleExpression.cs | 15 --------------- .../Scheduling/ScheduleExpressionConverter.cs | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 Cronitor/Scheduling/ScheduleExpressionConverter.cs diff --git a/Cronitor/Scheduling/ScheduleExpression.cs b/Cronitor/Scheduling/ScheduleExpression.cs index 9003f79..cd6cd20 100644 --- a/Cronitor/Scheduling/ScheduleExpression.cs +++ b/Cronitor/Scheduling/ScheduleExpression.cs @@ -1,5 +1,3 @@ -using System; -using System.Text.Json; using System.Text.Json.Serialization; namespace Cronitor.Scheduling @@ -19,17 +17,4 @@ public ScheduleExpression(string value) public static implicit operator string(ScheduleExpression expression) => expression?.Value; public static implicit operator ScheduleExpression(string value) => new ScheduleExpression(value); } - - public class ScheduleExpressionConverter : JsonConverter - { - public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new ScheduleExpression(reader.GetString()); - } - - public override void Write(Utf8JsonWriter writer, ScheduleExpression value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } - } } diff --git a/Cronitor/Scheduling/ScheduleExpressionConverter.cs b/Cronitor/Scheduling/ScheduleExpressionConverter.cs new file mode 100644 index 0000000..660f0f8 --- /dev/null +++ b/Cronitor/Scheduling/ScheduleExpressionConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Cronitor.Scheduling +{ + public class ScheduleExpressionConverter : JsonConverter + { + public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new ScheduleExpression(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, ScheduleExpression value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + } +} From dee0cb29a454700ec53485917c69dad07a21cf9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:44:17 +0000 Subject: [PATCH 06/11] Use pattern matching in ScheduleExpressionConverter.Read (CS-R1138) https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor/Scheduling/ScheduleExpressionConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cronitor/Scheduling/ScheduleExpressionConverter.cs b/Cronitor/Scheduling/ScheduleExpressionConverter.cs index 660f0f8..b5be8ca 100644 --- a/Cronitor/Scheduling/ScheduleExpressionConverter.cs +++ b/Cronitor/Scheduling/ScheduleExpressionConverter.cs @@ -8,7 +8,7 @@ public class ScheduleExpressionConverter : JsonConverter { public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return new ScheduleExpression(reader.GetString()); + return reader.GetString() is { } value ? new ScheduleExpression(value) : null; } public override void Write(Utf8JsonWriter writer, ScheduleExpression value, JsonSerializerOptions options) From f8767d47438a0b2e6a1ce393488cf11d97f6e3b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:46:31 +0000 Subject: [PATCH 07/11] Suppress CS-R1138 on ScheduleExpressionConverter.Read Parameter order is dictated by the JsonConverter base class override. https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor/Scheduling/ScheduleExpressionConverter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cronitor/Scheduling/ScheduleExpressionConverter.cs b/Cronitor/Scheduling/ScheduleExpressionConverter.cs index b5be8ca..43f393b 100644 --- a/Cronitor/Scheduling/ScheduleExpressionConverter.cs +++ b/Cronitor/Scheduling/ScheduleExpressionConverter.cs @@ -6,7 +6,9 @@ namespace Cronitor.Scheduling { public class ScheduleExpressionConverter : JsonConverter { +#pragma warning disable CS-R1138 // Parameter order is dictated by JsonConverter base class public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) +#pragma warning restore CS-R1138 { return reader.GetString() is { } value ? new ScheduleExpression(value) : null; } From 5c9ecb3035268cd5bb71ace15e287aea49f2383f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:47:45 +0000 Subject: [PATCH 08/11] Fix build errors across all target frameworks - Replace C# 8.0 pattern matching with null check for netstandard2.0 - Fully qualify Schedule reference in JobBuilder to avoid clash with method of the same name - Add missing using Cronitor.Models for Request in MonitorTypeTests https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/Builders/JobBuilder.cs | 2 +- Cronitor.Tests/MonitorTypeTests.cs | 1 + Cronitor/Scheduling/ScheduleExpressionConverter.cs | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cronitor.Tests/Builders/JobBuilder.cs b/Cronitor.Tests/Builders/JobBuilder.cs index 8fcc4e3..f51c32f 100644 --- a/Cronitor.Tests/Builders/JobBuilder.cs +++ b/Cronitor.Tests/Builders/JobBuilder.cs @@ -14,7 +14,7 @@ public class JobBuilder private readonly string _group = "Group"; private readonly string _note = "Note"; private readonly string _platform = "Platform"; - private ScheduleExpression _schedule = Schedule.Cron.Daily(hour: 0, minute: 35); + private ScheduleExpression _schedule = Cronitor.Scheduling.Schedule.Cron.Daily(hour: 0, minute: 35); private readonly int? _scheduleTolerance = 1; private readonly string _timeZone = "Europe/Stockholm"; diff --git a/Cronitor.Tests/MonitorTypeTests.cs b/Cronitor.Tests/MonitorTypeTests.cs index c9afaaf..5ce66be 100644 --- a/Cronitor.Tests/MonitorTypeTests.cs +++ b/Cronitor.Tests/MonitorTypeTests.cs @@ -1,4 +1,5 @@ using Cronitor.Constants; +using Cronitor.Models; using Cronitor.Scheduling; using Cronitor.Models.Monitors; using Cronitor.Tests.Helpers; diff --git a/Cronitor/Scheduling/ScheduleExpressionConverter.cs b/Cronitor/Scheduling/ScheduleExpressionConverter.cs index 43f393b..5142e64 100644 --- a/Cronitor/Scheduling/ScheduleExpressionConverter.cs +++ b/Cronitor/Scheduling/ScheduleExpressionConverter.cs @@ -10,7 +10,8 @@ public class ScheduleExpressionConverter : JsonConverter public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) #pragma warning restore CS-R1138 { - return reader.GetString() is { } value ? new ScheduleExpression(value) : null; + var value = reader.GetString(); + return value != null ? new ScheduleExpression(value) : null; } public override void Write(Utf8JsonWriter writer, ScheduleExpression value, JsonSerializerOptions options) From edab1eebc42b8198b1a71ecd0be724edae620a60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:50:44 +0000 Subject: [PATCH 09/11] Remove invalid pragma for CS-R1138 analyzer rule CS-R1138 is a third-party analyzer rule, not a compiler warning. Pragma directives only work with compiler warnings. The parameter order is dictated by the JsonConverter base class and cannot be changed. https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor/Scheduling/ScheduleExpressionConverter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cronitor/Scheduling/ScheduleExpressionConverter.cs b/Cronitor/Scheduling/ScheduleExpressionConverter.cs index 5142e64..0dfc816 100644 --- a/Cronitor/Scheduling/ScheduleExpressionConverter.cs +++ b/Cronitor/Scheduling/ScheduleExpressionConverter.cs @@ -6,9 +6,7 @@ namespace Cronitor.Scheduling { public class ScheduleExpressionConverter : JsonConverter { -#pragma warning disable CS-R1138 // Parameter order is dictated by JsonConverter base class public override ScheduleExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) -#pragma warning restore CS-R1138 { var value = reader.GetString(); return value != null ? new ScheduleExpression(value) : null; From 5eba4f9525eb445d355ee6d8b3ba3dcb2e329975 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:55:42 +0000 Subject: [PATCH 10/11] Rename JobBuilder.Schedule() to WithSchedule() to fix name clash The Schedule() method shadowed the Schedule static class import, preventing the field initializer from resolving. Fully qualifying also failed because the Cronitor static class shadows the namespace. https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/Builders/JobBuilder.cs | 4 ++-- Cronitor.Tests/MonitorsClientTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cronitor.Tests/Builders/JobBuilder.cs b/Cronitor.Tests/Builders/JobBuilder.cs index f51c32f..9d6ce90 100644 --- a/Cronitor.Tests/Builders/JobBuilder.cs +++ b/Cronitor.Tests/Builders/JobBuilder.cs @@ -14,7 +14,7 @@ public class JobBuilder private readonly string _group = "Group"; private readonly string _note = "Note"; private readonly string _platform = "Platform"; - private ScheduleExpression _schedule = Cronitor.Scheduling.Schedule.Cron.Daily(hour: 0, minute: 35); + private ScheduleExpression _schedule = Schedule.Cron.Daily(hour: 0, minute: 35); private readonly int? _scheduleTolerance = 1; private readonly string _timeZone = "Europe/Stockholm"; @@ -40,7 +40,7 @@ public JobBuilder Notify(List notify) return this; } - public JobBuilder Schedule(ScheduleExpression schedule) + public JobBuilder WithSchedule(ScheduleExpression schedule) { _schedule = schedule; return this; diff --git a/Cronitor.Tests/MonitorsClientTests.cs b/Cronitor.Tests/MonitorsClientTests.cs index f2681d4..e7885ee 100644 --- a/Cronitor.Tests/MonitorsClientTests.cs +++ b/Cronitor.Tests/MonitorsClientTests.cs @@ -133,7 +133,7 @@ public void ShouldExecuteCreateMethod() }; var monitor = Make.Job .Notify(notify) - .Schedule("every 60 seconds") + .WithSchedule("every 60 seconds") .Build(); _httpClient.Setup(x => x.SendAsync(It.IsAny())).Returns(Task.FromResult(new CreateMonitorResponse { Monitors = new List { monitor } })); From de1cb2eb37d6018a84ffe5314ad821b5bad07c37 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 29 Mar 2026 06:59:44 +0000 Subject: [PATCH 11/11] Fix SerializesAsJsonString test using concrete type Anonymous types have read-only properties, which the Serializer skips due to IgnoreReadOnlyProperties = true. Use ScheduleContainer instead. https://claude.ai/code/session_01XFQ49AHWaNZH3pyffR78LN --- Cronitor.Tests/ScheduleTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cronitor.Tests/ScheduleTests.cs b/Cronitor.Tests/ScheduleTests.cs index e7776c5..e433420 100644 --- a/Cronitor.Tests/ScheduleTests.cs +++ b/Cronitor.Tests/ScheduleTests.cs @@ -229,9 +229,9 @@ public void ImplicitScheduleExpressionFromString() [Fact] public void SerializesAsJsonString() { - var expression = Schedule.Cron.Daily(); + var container = new ScheduleContainer { Schedule = Schedule.Cron.Daily() }; - var json = Serializer.Serialize(new { schedule = expression }); + var json = Serializer.Serialize(container); Assert.Equal("{\"schedule\":\"0 0 * * *\"}", json); }