Tempus.EFCore
1.0.1
dotnet add package Tempus.EFCore --version 1.0.1
NuGet\Install-Package Tempus.EFCore -Version 1.0.1
<PackageReference Include="Tempus.EFCore" Version="1.0.1" />
<PackageVersion Include="Tempus.EFCore" Version="1.0.1" />
<PackageReference Include="Tempus.EFCore" />
paket add Tempus.EFCore --version 1.0.1
#r "nuget: Tempus.EFCore, 1.0.1"
#:package Tempus.EFCore@1.0.1
#addin nuget:?package=Tempus.EFCore&version=1.0.1
#tool nuget:?package=Tempus.EFCore&version=1.0.1
Tempus
A suite of .NET packages for correct, production-grade datetime handling — timezone conversions, DST safety, business calendars, fiscal calendars, SLA timers, recurring schedules, and holiday data.
Why Tempus?
- Timezone-aware from the start: Built around
DateTimeOffsetand IANA timezone identifiers - DST-safe: Handles ambiguous times during daylight saving transitions automatically
- Business logic ready: Business calendars, SLA timers, recurring schedules out of the box
- Holiday data: Embedded holiday providers for 10+ countries/regions
- Testable: Inject
ITempusClockinstead ofDateTime.UtcNowfor deterministic tests - Async-friendly: Designed for modern async .NET patterns
- EF Core integrated: Automatic temporal value converters
- Web-ready: ASP.NET Core middleware and JSON attribute converters
Packages at a Glance
Dependency Diagram
Tempus.Core (foundation)
├── Tempus.Business
│ ├── Tempus.Holidays (multi-region engine)
│ │ ├── Tempus.Holidays.US
│ │ ├── Tempus.Holidays.UK
│ │ ├── Tempus.Holidays.IN
│ │ ├── Tempus.Holidays.EU
│ │ └── Tempus.Holidays.Exchange
│ └── Tempus.AspNetCore
├── Tempus.EFCore
└── Tempus.Testing
Quick Start
Choose your configuration level based on needs:
⚡ Minimal: Just Clock Injection
For applications that only need testable datetime access:
dotnet add package Tempus.Core
// Program.cs
builder.Services.AddTempus();
// Service.cs
public class MyService(ITempusClock clock)
{
public DateOnly Today => clock.TodayUtc;
public DateTimeOffset NowInZone(string tz) => clock.NowIn(tz);
}
// Tests
var fakeClock = new FakeClock(new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero));
var service = new MyService(fakeClock);
fakeClock.Advance(TimeSpan.FromHours(2));
📅 Intermediate: Business Logic
For scheduling, SLAs, recurring events, and holiday awareness:
dotnet add package Tempus.Core
dotnet add package Tempus.Business
dotnet add package Tempus.Holidays
// Program.cs
builder.Services
.AddTempus()
.AddHolidays("CA", "CA-ON") // Canada + Ontario
.AddBusinessCalendar(opts =>
{
opts.WorkWeek = WorkWeek.MondayToFriday;
opts.BusinessHours = BusinessHours.NineToFive("America/Toronto");
})
.AddSlaTimer();
// Usage
public class TicketService(IBusinessCalendarFactory calFactory, ISlaTimerFactory slaFactory)
{
public void CheckSla(DateTimeOffset ticketCreated)
{
var calendar = calFactory.Default;
var sla = slaFactory.Create(ticketCreated, TimeSpan.FromHours(8));
if (sla.IsBreached)
Console.WriteLine("Ticket breached SLA!");
// Next business day
var dueDate = calendar.AddBusinessDays(DateOnly.FromDateTime(ticketCreated.Date), 2);
}
}
🚀 Full-Featured: Web + Database
For ASP.NET Core applications with user timezones and EF Core:
dotnet add package Tempus.Core
dotnet add package Tempus.Business
dotnet add package Tempus.Holidays
dotnet add package Tempus.AspNetCore
dotnet add package Tempus.EFCore
// Program.cs
builder.Services
.AddTempus()
.AddHolidays("CA", "CA-ON", "AU", "DE", "JP")
.AddBusinessCalendar(opts =>
{
opts.WorkWeek = WorkWeek.MondayToFriday;
opts.BusinessHours = BusinessHours.NineToFive("America/Toronto");
})
.AddFiscalCalendar(opts => opts.StartMonth = 4) // April fiscal year
.AddSlaTimer()
.AddControllers()
.AddTempusJson(); // JSON converters
app.UseTempusTimezone(); // Reads X-Timezone header or Accept-Language
// DbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseTemporalConventions(); // Auto-converters for all temporal types
}
// DTO.cs
public class EventDto
{
[UtcTime] public DateTimeOffset CreatedAt { get; set; } // Always UTC in JSON
[UserLocalTime] public DateTimeOffset StartsAt { get; set; } // Converted to user's TZ
[RelativeTime] public DateTimeOffset UpdatedAt { get; set; } // "2 hours ago"
}
🧪 Testing
For test projects:
dotnet add package Tempus.Testing
[Fact]
public void CalculatesCorrectSla()
{
var clock = new FakeClock(new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero));
var sla = new SlaTimer(clock.UtcNow, TimeSpan.FromHours(8), calendar: null);
clock.Advance(TimeSpan.FromHours(7));
Assert.False(sla.IsBreached);
clock.Advance(TimeSpan.FromHours(2));
Assert.True(sla.IsBreached);
}
Detailed Documentation
Tempus.Core
Core types for timezone-aware datetime handling.
ITempusClock — Testable Clock Injection
Inject instead of DateTime.UtcNow. Always returns UTC DateTimeOffset.
public class MyService(ITempusClock clock)
{
public DateOnly Today => clock.TodayUtc;
public DateTimeOffset NowInToronto() => clock.NowIn("America/Toronto");
public DateTimeOffset Now => clock.UtcNow;
}
Why? Enables deterministic unit tests by injecting a fake clock.
ITimezoneResolver — IANA ↔ Windows Conversion
public class TimezoneLookup(ITimezoneResolver resolver)
{
public void Resolve()
{
TimeZoneInfo tz = resolver.Resolve("America/New_York");
string windowsId = resolver.ToWindowsId("America/New_York"); // "Eastern Standard Time"
string ianaId = resolver.ToIanaId("Eastern Standard Time"); // "America/New_York"
}
}
Why? Windows and IANA use different timezone identifiers. Tempus handles the mapping.
IDstResolver — Ambiguous Time Handling
Automatically resolves ambiguous times during DST transitions (when clocks "fall back").
public class DstHandler(IDstResolver dst)
{
public void HandleAmbiguousTime()
{
// Nov 1, 2026: 1:30 AM happens twice during "fall back"
var result = dst.ToUtc(
new DateTime(2026, 11, 1, 1, 30, 0),
"America/New_York"
);
if (result.WasAmbiguous)
Console.WriteLine($"Resolved using {result.StrategyApplied}");
// Check if near a DST transition
var next = dst.NextTransition("America/New_York");
bool near = dst.IsNearTransition(DateTimeOffset.UtcNow, "America/New_York", TimeSpan.FromHours(48));
}
}
Why? DST transitions cause bugs. Tempus detects and resolves them automatically.
DateRange — Date Span Utilities
var range = new DateRange(new DateOnly(2026, 1, 1), new DateOnly(2026, 12, 31));
Console.WriteLine(range.TotalDays); // 365
Console.WriteLine(range.Contains(new DateOnly(2026, 6, 15))); // true
foreach (DateOnly date in range.ToDates()) { /* iterate */ }
DateRange? overlap = range.Intersect(other);
Extension Methods
// Convert local time to UTC safely
DateTimeOffset utc = someLocal.ToUtcSafe("America/New_York");
// Convert UTC to any timezone
DateTimeOffset local = utc.InZone("Europe/Berlin");
// Check DST status
bool isDst = utc.IsDst("America/Chicago");
// Day boundaries in any timezone
DateTimeOffset startOfDay = utc.StartOfDay("Asia/Tokyo");
DateTimeOffset endOfDay = utc.EndOfDay("Asia/Tokyo");
Tempus.Business
Business logic: calendars, SLAs, schedules, fiscal years.
IBusinessCalendar — Work Day Calculations
public class SchedulingService(IBusinessCalendarFactory calFactory)
{
public void Schedule()
{
var calendar = calFactory.Default;
bool isWorkday = calendar.IsBusinessDay(new DateOnly(2026, 12, 25)); // false (Christmas)
DateOnly next = calendar.NextBusinessDay(new DateOnly(2026, 12, 24));
DateOnly due = calendar.AddBusinessDays(new DateOnly(2026, 1, 1), 10);
int elapsed = calendar.BusinessDaysBetween(from, to);
}
}
Configuration:
builder.Services.AddBusinessCalendar(opts =>
{
opts.WorkWeek = WorkWeek.MondayToFriday;
opts.BusinessHours = BusinessHours.NineToFive("America/Toronto");
});
Why? Automatically accounts for weekends and holidays when calculating due dates.
ISlaTimer / ISlaTimerFactory — SLA Breach Detection
public class TicketService(ISlaTimerFactory slaFactory)
{
public void CheckSla(DateTimeOffset ticketCreated)
{
ISlaTimer sla = slaFactory.Create(ticketCreated, TimeSpan.FromHours(8));
Console.WriteLine(sla.ElapsedBusinessTime); // time spent on ticket
Console.WriteLine(sla.RemainingBusinessTime); // time left to resolve
Console.WriteLine(sla.IsBreached); // breached?
Console.WriteLine(sla.BreachesAt); // when will it breach?
}
}
Why? Tracks SLA compliance automatically, accounting for business hours and holidays.
RecurringSchedule — Event Recurrence
var schedule = new RecurringSchedule
{
Start = new DateOnly(2026, 1, 5), // Monday
Frequency = RecurrenceFrequency.Weekly,
Count = 10,
};
foreach (DateOnly occurrence in schedule.GetOccurrences()) { /* ... */ }
// Efficient bounded queries (no full enumeration)
var inQ2 = schedule.GetOccurrences(
new DateOnly(2026, 4, 1),
new DateOnly(2026, 6, 30)
);
bool happens = schedule.OccursOn(new DateOnly(2026, 3, 9)); // true
Frequencies: Daily, Weekly, BiWeekly, Monthly, Quarterly, Annually
Why? Avoid reinventing recurrence logic; this is a complex domain.
FiscalCalendar — Fiscal Year Tracking
// April fiscal year (UK/AU style)
var fiscal = new FiscalCalendar(startMonth: 4, startDay: 1);
int year = fiscal.GetFiscalYear(new DateOnly(2026, 5, 1)); // 2026
int quarter = fiscal.GetFiscalQuarter(new DateOnly(2026, 5, 1)); // Q1
DateOnly quarterStart = fiscal.GetQuarterStart(2026, 1); // 2026-04-01
Why? Fiscal calendars often differ from calendar years; map dates correctly for reporting.
Tempus.Holidays
Multi-region holiday data engine.
Supported Regions
| Code | Region |
|---|---|
CA |
Canada (national) |
CA-ON |
Ontario |
CA-BC |
British Columbia |
CA-QC |
Québec |
CA-AB |
Alberta |
AU |
Australia (national) |
AU-NSW |
New South Wales |
AU-VIC |
Victoria |
AU-QLD |
Queensland |
AU-WA |
Western Australia |
DE |
Germany (national) |
DE-BW |
Baden-Württemberg |
JP |
Japan (with substitute holiday rules) |
Usage
builder.Services.AddHolidays("CA", "CA-ON", "DE", "JP");
public class HolidayService(IEnumerable<IHolidayProvider> providers)
{
public IHolidayProvider For(string region) =>
providers.First(p => string.Equals(p.Region, region, StringComparison.Ordinal));
}
var ca = service.For("CA");
bool isHoliday = ca.IsHoliday(new DateOnly(2026, 7, 1)); // Canada Day
IEnumerable<Holiday> holidays = ca.GetHolidays(2026);
Custom Holiday Provider
dotnet new install Tempus.Templates
dotnet new tempus-holiday --name MyCompany --namespace Acme.Calendar
Why? Embed holiday data in your app; no external APIs or maintenance burden.
Tempus.AspNetCore
ASP.NET Core integration: timezone middleware, JSON converters.
Middleware — Automatic User Timezone Detection
app.UseTempusTimezone();
Reads timezone from:
X-Timezoneheader (e.g.,"America/Toronto")Accept-Languageheader (if timezone can be inferred)- Falls back to UTC
Available via ITempusUserContext:
public class EventController(ITempusUserContext ctx)
{
[HttpGet]
public IActionResult GetEvent(int id)
{
var userTz = ctx.UserTimezone; // "America/Toronto"
// ...
}
}
JSON Attributes — Automatic Serialization
public class EventDto
{
[UtcTime]
public DateTimeOffset CreatedAt { get; set; } // Always UTC in JSON
[UserLocalTime]
public DateTimeOffset StartsAt { get; set; } // Converted to user's timezone
[RelativeTime]
public DateTimeOffset UpdatedAt { get; set; } // "2 hours ago", "1 day from now"
}
Why? Automatically converts times in/out of the user's timezone without manual code.
Tempus.EFCore
Entity Framework Core temporal type converters.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseTemporalConventions();
// Auto-registers converters for:
// - DateTime, DateTime?
// - DateTimeOffset, DateTimeOffset?
// - DateOnly, DateOnly?
// - TimeOnly, TimeOnly?
}
Why? Ensures your database stores temporal data safely and consistently.
Tempus.Testing
Deterministic testing with FakeClock.
[Fact]
public void SlaIsBreached_WhenTargetExceeded()
{
var clock = new FakeClock(new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero));
var sla = new SlaTimer(clock.UtcNow, TimeSpan.FromHours(8), calendar: null);
clock.Advance(TimeSpan.FromHours(7));
Assert.False(sla.IsBreached);
clock.Advance(TimeSpan.FromHours(2));
Assert.True(sla.IsBreached);
}
Methods:
clock.Advance(TimeSpan)— Skip forwardclock.AdvanceDays(int)— Skip daysclock.SetTo(DateTimeOffset)— Jump to exact time
Contributing
Getting Started
Clone and build:
git clone https://github.com/[username]/Tempus.git cd Tempus dotnet buildRun all tests:
dotnet testMake changes:
- Create a feature branch:
git checkout -b feature/my-feature - Keep commits focused and atomic
- Include tests for new functionality
- Ensure no compiler warnings
- Create a feature branch:
Validate before pushing:
dotnet build --configuration Release dotnet testSubmit a PR — CI runs on Ubuntu, Windows, and macOS.
Code Style
- Follow C# naming conventions (PascalCase for public members, camelCase for locals)
- Use nullable reference types (
#nullable enable) - Write XML documentation for public APIs
- Prefer expression-bodied members where readable
Testing
- Write unit tests for all public APIs
- Use
Tempus.Testing.FakeClockfor datetime-dependent tests - Aim for >80% code coverage on new code
License
MIT — see LICENSE.
Changelog
See CHANGELOG.md for release history and version information.
Built with ❤️ for developers who care about time.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- Tempus.Core (>= 1.0.1)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- Tempus.Core (>= 1.0.1)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- Tempus.Core (>= 1.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.