DatabaseJobFeature silently sets the global SQL Server DateTimeConverter.DateStyle to Utc, which shifts all DateTime.Now values by -8 hours. Why, and can it be opted out?
Suggested forum category: Customer Forums / OrmLite (or Background Jobs)
TL;DR
After registering DatabaseJobFeature (ServiceStack Background Jobs), every DateTime.Now (Kind = Local) that my application writes to any SQL Server table through OrmLite is stored as UTC. On a server in UTC+8 the stored value is 8 hours earlier than the wall-clock time. This happens because DatabaseJobFeature.Init() contains:
DateTimeConverter dateTimeConverter = Dialect.GetDateTimeConverter();
if (dateTimeConverter.DateStyle == DateTimeKind.Unspecified)
dateTimeConverter.DateStyle = DateTimeKind.Utc;
DateStyle is a property of the dialect-wide shared converter, not of the job tables, so the side effect leaks into every table that uses the same dialect instance (SqlServerDialect.Provider, SqlServer2017Dialect.Provider, …).
Questions:
-
Why does
DatabaseJobFeatureneed to change the globalDateStyle? -
Is there a supported way to disable or scope this change (for example, a feature option, or only applying it to a dedicated
NamedConnection/ dialect instance)? -
If not, would you consider making it opt-in or scoped to the job tables?
Environment
| Item | Value |
|---|---|
| Runtime | ASP.NET Core, net10.0 |
| ServiceStack | 10.4.0 (also reproduced on 10.3.0; the same code exists in 10.0.6) |
| OrmLite | ServiceStack.OrmLite 10.4.0 with SQL Server |
| Server time zone | China Standard Time (UTC+8) |
| Plugin | DatabaseJobFeature + CommandsFeature (RDBMS Background Jobs) |
What I observed
At 10:28 local time a customer order was saved with an entity property set by DateTime.Now:
var order = new CoffeeOrder { InputDate = DateTime.Now, /* ... */ };
db.Insert(order);
The OrmLite debug log showed the parameter as @InputDate=2026/10/9 2:28:00, i.e. the Local value was converted to UTC before it was sent to SQL Server. The row in the database contained 02:28.
This behaviour is new in my project. Everything had been stored as local time for years, with no custom DateStyle anywhere in my code. The only thing that changed was that I added Background Jobs a few days earlier (services.AddPlugin(new DatabaseJobFeature { ... })).
Investigation
-
Release notes. I read the v10.3 and v10.4 release notes. I found no mention of a change to DateTime, UTC, or
DateStylehandling in OrmLite. -
DateTimeConverteritself did not change. I decompiledServiceStack.OrmLite.Converters.DateTimeConverterfrom the NuGet packages 8.10.0, 10.0.6, 10.3.0 and 10.4.0. The code is identical in all four.DateStyleis a plain auto-property that defaults toDateTimeKind.Unspecified. -
Without Background Jobs nothing converts the value. With a throw-away console app (OrmLite 10.4.0, SQL Server dialects only) the result was:
SqlServer2017: DateStyle=Unspecified Now=12:56 ToDbValue=12:56 kind=Local SqlServer2008: DateStyle=Unspecified Now=12:56 ToDbValue=12:56 kind=Local Default: DateStyle=Unspecified Now=12:56 ToDbValue=12:56 kind=LocalSo OrmLite on its own stores
DateTime.Nowunchanged. -
Found the source. I searched the decompiled
ServiceStack,ServiceStack.ServerandServiceStack.Mvcassemblies forDateStyle. The only place that assigns it isDatabaseJobFeature.Init()(in 10.0.6, 10.3.0 and 10.4.0):protected void Init(IServiceProvider services) { if (DbFactory == null) { /* resolve IDbConnectionFactory from DI */ } if (DbProvider == null) { DbProvider = DbJobsProvider.Create(DbFactory, NamedConnection); } DateTimeConverter dateTimeConverter = Dialect.GetDateTimeConverter(); if (dateTimeConverter.DateStyle == DateTimeKind.Unspecified) { dateTimeConverter.DateStyle = DateTimeKind.Utc; } }Dialectis the dialect of theIDbConnectionFactorythe feature uses. In a normal app that is the application’s default factory, so the change is global for the whole application. -
Why the effect is “-8 hours”. With
DateStyle = Utc,DateTimeConverter.ToDbValuedoesdateTime.ToUniversalTime()for values whoseKindisLocal.DateTime.NowisLocal, so it is converted. (DateTime.UtcNowand values withKind = Unspecifiedare not changed.)
Side effects I ran into
-
Every business table that stores
DateTime.Nowis now wrong by -8 hours (for rows written while the feature was registered). -
Round-trip inconsistency. When reading,
Utcstyle marks every value asKind = Utc. Code that used to readKind = Unspecifiedvalues now receives UTC-kind values. -
UserAuth.OrmLiteAuthRepository/OrmLiteAuthRepositoryBasealways writesCreatedDate/ModifiedDateusingDateTime.UtcNow. That is unchanged and by design, but in an application whose database convention is “local wall-clock time” it makes the auth tables 8 hours different from every other table. I would also like to know if there is a supported way to control that (see question 4).
What I tried (and why it is not satisfactory)
| Attempt | Result |
|---|---|
Set DateStyle = Local globally |
DateTime.Now is stored correctly, but Kind = Unspecified values (e.g. new DateTime(2026, 10, 9) or a date coming back from a JSON round-trip in session state) are treated as UTC and shifted by +8 hours, so a date-only value 2026-10-09 00:00 became 08:00. |
Reset DateStyle = Unspecified in DatabaseJobFeature.ConfigureDialect |
Business tables are correct again. But the job tables are written with DateTime.UtcNow and read back with Kind = Unspecified, so they serialize without a Z/offset. The admin UI interprets them as local time and shows them 8 hours early (e.g. 05:24 AM instead of 13:24). |
Give DatabaseJobFeature its own OrmLiteConnectionFactory with a new dialect instance (new SqlServer2017OrmLiteDialectProvider()) |
This is the best workaround so far. DateStyle = Utc only affects the job tables, and the business tables keep the shared dialect with Unspecified. |
| Admin UI still shows UTC without a time zone | My app sets JsConfig.DateHandler = ISO8601DateTime globally, which writes yyyy-MM-dd HH:mm:ss without a zone. I had to add jsconfig=eccn,dh:timestampoffset to the /api requests that come from /admin-ui. |
| AM/PM in the admin UI | The admin UI hard-codes timeFmt12. I had to copy servicestack-client.mjs to wwwroot/js/ and patch it to get 24-hour time. |
The workarounds treat the symptom in the display layer. The data already written to the database while DateStyle = Utc was in effect is still 8 hours off, and I would have to repair it manually, table by table.
Questions
-
Why is the global
DateStyleforced toUtcinDatabaseJobFeature.Init()? I understand that the job tables storeDateTime.UtcNowand want those values to be read back asKind = Utc. But is it intended to change a shared converter that other tables in the same database use? -
Is there a supported way to disable or restrict it? For example:
-
a property on
DatabaseJobFeature(such asUseUtcDateStyle = false), or -
only set
DateStylewhenNamedConnectionpoints to a dedicated connection/dialect, or -
apply the UTC handling to the job table models only (e.g. through
[Alias]/custom field converters, or by always reading withDateTime.SpecifyKind(x, DateTimeKind.Utc)inDbJobs) instead of changing the converter.
-
-
Could the feature document this behaviour? It is not mentioned in the Background Jobs docs or release notes, and it is very hard to track down because
DateStyleis not set anywhere in the application code. -
For
UserAuth/OrmLiteAuthRepository: is there a recommended way to makeCreatedDate/ModifiedDateuse local time (or to let the application choose the clock), other than anOrmLiteConfig.InsertFilter/UpdateFilterthat converts the values?
Thanks! Happy to provide a minimal repro project if useful.
Minimal reproduction
// Program.cs (net10.0, ServiceStack 10.4.0)
var builder = WebApplication.CreateBuilder(args);
var dbFactory = new OrmLiteConnectionFactory(connectionString, SqlServer2017Dialect.Provider);
builder.Services.AddSingleton<IDbConnectionFactory>(dbFactory);
// (1) Before adding Background Jobs:
Console.WriteLine(SqlServer2017Dialect.Provider.GetDateTimeConverter().DateStyle); // Unspecified
builder.Services.AddPlugin(new CommandsFeature());
builder.Services.AddPlugin(new DatabaseJobFeature());
var app = builder.Build();
app.UseServiceStack(new AppHost()); // plugins are loaded here
// (2) After the plugin has been loaded:
Console.WriteLine(SqlServer2017Dialect.Provider.GetDateTimeConverter().DateStyle); // Utc
using var db = dbFactory.Open();
db.CreateTableIfNotExists<Foo>();
db.Insert(new Foo { CreatedAt = DateTime.Now }); // stored as DateTime.Now - 8h on a UTC+8 machine
public class Foo
{
[AutoIncrement] public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
Expected: Foo.CreatedAt is stored exactly as passed (DateTime.Now), as it was before adding DatabaseJobFeature.
Actual: it is converted with ToUniversalTime().