- Each database connection resolves its own timezone. Configure it under
appSettings.db.<name>.timezoneor omit it and letDBautodetect (cached per connection string, falls back toUTC). DBconverts SQLdatetimevalues from the database timezone to UTC on read and from UTC to the database timezone on write.- Field names ending in
_utcare treated as already-UTC instants.DBdoes not apply database timezone conversion to those fields on read or helper-built writes. - SQL Server
datetimeoffsetvalues are treated as instants. Untyped rows and scalar reads expose them as UTC-compatibleDateTime/SQL strings; typed DTO reads may useDateTimeOffsetto keep the stored offset. - SQLite stores date/time values through
Microsoft.Data.Sqliteas SQLite-compatible values. Use declared column types (DATE,DATETIME,DATETIMEOFFSET) soDBcan apply the same date-only, datetime, and offset-aware rules. Configure SQLite databases withtimezone = "UTC"unless you have a deliberate single-node DB timezone policy. - SQL
datevalues stay date-only. They are kept as calendar values and are not shifted through timezone conversion. - Database values are read in SQL formats:
- Date:
YYYY-MM-DD - Datetime:
YYYY-MM-DD HH:mm:ss
- Date:
- On output, real datetimes can be converted to the user's timezone and formatted with the user's date/time formats. Date-only values keep the same calendar day for every user.
appSettings keys define defaults applied for anonymous users or when there's no per-user override:
appSettings.date_format- default date format id (see constants below). Example:0(MDY)appSettings.time_format- default time format id. Example:0(12h)appSettings.timezone- default timezone id. Example:"UTC"
These values are available at runtime via fw.config("date_format"), fw.config("time_format"), fw.config("timezone") and are copied into fw.G.
On each request FW pulls user-specific settings from session, if present:
Session("date_format")Session("time_format")Session("timezone")
Accessors:
FW.userDateFormatFW.userTimeFormatFW.userTimezone
Set these session keys at login or on profile save to personalize formatting and conversions for a user.
DateUtils exposes constants and helpers:
-
Formats
DateUtils.DATE_FORMAT_MDY= 0 ->M/d/yyyyDateUtils.DATE_FORMAT_DMY= 10 ->d/M/yyyyDateUtils.TIME_FORMAT_12= 0 ->h:mm ttDateUtils.TIME_FORMAT_24= 10 ->H:mm
-
Timezone
DateUtils.TZ_UTC="UTC"
-
Mapping helpers
DateUtils.mapDateFormat(int)DateUtils.mapTimeFormat(int)DateUtils.mapTimeWithSecondsFormat(int)
-
FW.formatUserDateTime(dbValue)- takes a UTCDateTime(or SQL string) and returns a user-formatted string. Real datetimes are converted from UTC to the user's timezone; date-only values stay on the original calendar day. -
ParsePage templates are preconfigured from FW:
DateFormat=mapDateFormat(userDateFormat)DateFormatShort=DateFormat + " " + mapTimeFormat(userTimeFormat)DateFormatLong=DateFormat + " " + mapTimeWithSecondsFormat(userTimeFormat)- InputTimezone defaults to
UTC - OutputTimezone =
fw.userTimezone
The framework treats these inputs as calendar dates, not instants in time:
- SQL
datevalues such as2026-04-13 - User-formatted date-only strings that match the current user date format
DateTimevalues that intentionally represent a date-only value (DateTimeKind.Unspecifiedat midnight)
Those values render without timezone shifting in both FW.formatUserDateTime(...) and ParsePage <~tag date>.
Real datetimes still follow the normal timezone pipeline:
- DB read: database timezone -> UTC
- Display: UTC ->
fw.userTimezone - Save from UI:
fw.userTimezone-> UTC
UTC and offset-bearing instants use the explicit instant pipeline:
_utcdatetime/datetime2: stored and read as UTC with no database timezone shift.datetimeoffset: stored as an offset-aware instant; untyped framework output normalizes to UTC, while typed DTOs can preserveDateTimeOffset. SQLite uses declaredDATETIMEOFFSETcolumns for the same framework contract.
Use FwController.modelAddOrUpdate plus FwModel.convertUserInput(item) before add/update. It converts human-entered strings while keeping internals in UTC:
datefields ->YYYY-MM-DDviaDateUtils.Str2SQL(str, fw.userDateFormat)datetimefields -> parsed with the user's formats, converted fromfw.userTimezoneto UTC, and passed to DB as UTCDateTimevalues.datetimeoffsetfields -> parsed likedatetime, then passed as UTCDateTimeOffsetvalues.datetime_localDynamic/Vue controls submit browser-nativeYYYY-MM-DDTHH:mm; the backend treats that as a user-local datetime and uses the same UTC save pipeline.- Dynamic form fields with type
date,date_popup, ordate_comboare normalized to SQLYYYY-MM-DDon save even if the backing DB column isdatetime, so semantically date-only fields do not pick up per-user timezone shifts later.
Notes:
- If a value is already a
DateTime(UTC) orDB.NOW, form conversion leaves it alone; DB helpers resolveDB.NOWwith field metadata so_utcfields use current UTC time. - If a value is already a
DateTimeOffset, offset-aware fields accept it directly;_utcfields normalize it to a UTC offset. - If the string is already in SQL datetime format, it is parsed as UTC and passed through.
- If the string is browser
datetime-localformat, it is parsed as a user-local wall time. - Raw SQL cannot infer target field names. For raw
query/execcalls, name UTC datetime parameters with an_utcsuffix or passDateTimeOffsetwhen no DB timezone conversion should be applied.
-
Resolved DB timezone is cached per connection (configured or autodetected).
-
DateUtils.convertTimezone(DateTime dt, string from_tz, string to_tz)- low-level helper reused by models. -
Show a timestamp from DB:
- C#:
ps["created_on"] = fw.formatUserDateTime(row["created_on"]);
- C#:
-
Show a date-only value from DB or UI:
- C#:
ps["due_date"] = fw.formatUserDateTime(row["due_date"]); - Result stays on the same day for every user when
row["due_date"]is a SQLdateor other date-only value.
- C#:
-
Accept a date from a form and save:
- Controller builds
itemfrom request, Model callsconvertUserInput(item)to normalize and convert to UTC, thenadd/update.
- Controller builds
Timezones must match Windows time zone IDs (used by TimeZoneInfo.FindSystemTimeZoneById). Examples: "UTC", "Pacific Standard Time", "Europe/Berlin".
In /My/Settings, auto stores an empty users.timezone preference and uses the browser-detected timezone for the active session. New users default to this empty auto preference. UTC is a separate explicit option for users who want the UI to stay in UTC regardless of browser auto-detect results.
If an invalid timezone is supplied, DateUtils.convertTimezone logs the issue and returns the original DateTime.
- Ensure
appsettings.jsonhas sensible defaults fordate_format,time_format,timezone, and optional per-DBtimezoneoverrides. - Set session values to emulate a user preference and verify formatting on pages and API responses.
- FW accessors and initialization:
osafw-app/App_Code/fw/FW.cs - Input conversion:
osafw-app/App_Code/fw/FwModel.cs-convertUserInput - Dynamic save normalization:
osafw-app/App_Code/fw/FwDynamicController.cs - Date helpers and constants:
osafw-app/App_Code/fw/DateUtils.cs