Duration (TimeSpan) columns
A
TimeSpanproperty is a duration. nextorm stores it natively where the database has a duration or time-of-day type, and in an integer column otherwise. The storage unit of the integer form is declared withDurationAttributeor the fluentDuration(...)mapping; the default is ticks.
Prerequisites: Querying and projections · Data modification (INSERT) · Provider overview · Limitations
Where the value is stored
| Provider | Storage of a TimeSpan property |
|---|---|
| PostgreSQL | native interval |
| MySQL / MariaDB | native TIME |
| SQL Server | integer column (bigint), ticks by default — no native duration type |
| SQLite | integer column (bigint), ticks by default |
| ClickHouse | integer column (Int64), ticks by default |
| In-memory | the CLR TimeSpan value |
On the integer providers the value is written as DurationUnit units and read back with the same unit, so [Duration(DurationUnit.Seconds)] over a bigint column stores whole seconds. Reading, writing and comparisons all apply the same conversion, so a TimeSpan property round-trips without manual conversion.
A schema generator emits these column types for the declarations below:
-- PostgreSQL: native duration
create table tasks (id bigint, estimate interval, paused interval(3));
-- MySQL / MariaDB: native time-of-day
create table tasks (id bigint, estimate time, paused time(3));
-- SQL Server / SQLite: integer column (`bigint`), the type is independent of the declared unit
create table tasks (id bigint, estimate bigint, paused bigint);
-- ClickHouse: a nullable TimeSpan becomes Nullable(Int64)
create table tasks (id Int64, estimate Int64, paused Nullable(Int64));
Declaring the unit
Use the attribute on the property:
public class Task
{
public long Id { get; set; }
[Duration(DurationUnit.Seconds)]
public TimeSpan Estimate { get; set; }
// No attribute: ticks on the integer providers, the native type on PostgreSQL/MySQL/MariaDB.
public TimeSpan Elapsed { get; set; }
[Duration(DurationUnit.Milliseconds, Precision = 3)]
public TimeSpan? Paused { get; set; }
}
or the fluent mapping:
ctx.From<Task>(b => b
.Table("tasks")
.Property(x => x.Estimate).Duration(DurationUnit.Seconds).HasColumnName("estimate"));
DurationUnit is one of Ticks, Microseconds, Milliseconds, Seconds, Minutes, Hours, Days. Precision is the fractional-second precision of a native type (for example TIME(3) or interval(3)) and is ignored for the integer form. On PostgreSQL and MySQL/MariaDB the unit is ignored because the value is stored natively.
Queries and comparisons
A duration column is projected and filtered like any other column. A TimeSpan constant in a comparison is converted to the column's storage form, on either side of the operator:
// Reads the stored integer in the declared unit on SQL Server/SQLite/ClickHouse,
// and the native interval/TIME on PostgreSQL/MySQL/MariaDB.
var overdue = ctx.From<Task>()
.Where(x => x.Estimate > TimeSpan.FromMinutes(5))
.Select(x => new { x.Id, x.Estimate })
.ToList();
The generated SQL has the same shape on every provider; only the placeholder and the bound parameter differ:
-- PostgreSQL / MySQL / MariaDB: @p0 carries a native duration
select id, estimate
from tasks
where (estimate > @p0); -- @p0 = interval '00:05:00' / time '00:05:00'
-- SQL Server / SQLite / ClickHouse: @p0 carries the integer storage value
select id, estimate
from tasks
where (estimate > @p0); -- @p0 = 300 (seconds, from [Duration(DurationUnit.Seconds)])
On SQLite the placeholder is $p0 instead of @p0.
Writes go through the same conversion, including bulk insert.
Details and limitations
- PostgreSQL
intervalalso stores months, which aTimeSpancannot express losslessly. Reading a mapped interval whose returned value carries a non-zero month component — either sign — into aTimeSpancolumn throwsInvalidCastExceptioninstead of approximating; a zero-month value (days plus microseconds) converts exactly when it fits aTimeSpan, and an unrepresentable value fails withOverflowExceptioninstead of wrapping. Use a unit-based integer column if months matter. - SQL Server has a
timetype, but it is a time-of-day (less than 24 hours); nextorm therefore uses an integer column so durations longer than a day are representable. - A non-tick unit truncates the sub-unit remainder, exactly like a database column of that unit.
- Only a directly projected duration property carries its declared unit; a computed duration expression (
Select(x => x.Estimate + something)) is read with the default ticks unit. Project the property itself when a non-tick unit is in use. DateTimeOffsetis read directly by the provider driver; a date-part convention (value.Datevsvalue.UtcDateTime.Date) is not applied.- MySQL/MariaDB also has a limited native
TIMErange,−838:59:59…838:59:59(about ±34.9 days); a longer duration cannot be stored in aTimeSpanproperty there (nextorm always uses nativeTIME), so keep it in a separate integer-typed property (for examplelong).
The dialect surface used by schema-generation tooling is ISqlDialect.MakeDurationType for a non-nullable column and ISqlDialect.MakeNullableDurationType for a nullable one, with SupportsNativeDuration. Both return the same type on every provider except ClickHouse, whose non-nullable Int64 becomes Nullable(Int64) for the nullable case.