using System; using System.Data; using Dapper; namespace GB5Shared.QueryExecutor { /// /// Dapper type handlers for modern .NET date types. /// /// SQL Server stores dates as datetime/datetime2 (DateTime). /// These handlers bridge the gap so DAL code can use DateOnly and /// DateTimeOffset directly in DTO properties without manual conversion. /// /// Registration: call GB5DapperTypeHandlers.Register() once at startup /// (QueryExecutor static initializer calls this automatically). /// public static class GB5DapperTypeHandlers { private static bool _registered; private static readonly object _lock = new(); public static void Register() { if (_registered) return; lock (_lock) { if (_registered) return; SqlMapper.AddTypeHandler(new DateOnlyTypeHandler()); SqlMapper.AddTypeHandler(new NullableDateOnlyTypeHandler()); SqlMapper.AddTypeHandler(new DateTimeOffsetFromDateTimeHandler()); SqlMapper.AddTypeHandler(new NullableDateTimeOffsetFromDateTimeHandler()); _registered = true; } } // SQL Server datetime minimum — 0001-01-01 is below it; send DBNull instead. private static readonly DateTime _sqlMinDate = new DateTime(1753, 1, 1, 0, 0, 0, DateTimeKind.Utc); // ── DateOnly ──────────────────────────────────────────────────────────── private sealed class DateOnlyTypeHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, DateOnly value) { if (value == DateOnly.MinValue) { parameter.Value = DBNull.Value; return; } parameter.DbType = DbType.Date; parameter.Value = value.ToDateTime(TimeOnly.MinValue); } public override DateOnly Parse(object value) => value is DateTime dt ? DateOnly.FromDateTime(dt) : DateOnly.Parse(value.ToString()!); } private sealed class NullableDateOnlyTypeHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, DateOnly? value) { if (value is null) { parameter.Value = DBNull.Value; return; } parameter.DbType = DbType.Date; parameter.Value = value.Value.ToDateTime(TimeOnly.MinValue); } public override DateOnly? Parse(object value) { if (value is DBNull || value is null) return null; return value is DateTime dt ? DateOnly.FromDateTime(dt) : DateOnly.Parse(value.ToString()!); } } // ── DateTimeOffset ─────────────────────────────────────────────────────── // SQL columns are datetime/datetime2 (no tz). We store UTC and reconstruct // a UTC DateTimeOffset on read. private sealed class DateTimeOffsetFromDateTimeHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, DateTimeOffset value) { var utc = value.UtcDateTime; if (value == DateTimeOffset.MinValue || utc < _sqlMinDate) { parameter.Value = DBNull.Value; return; } parameter.DbType = DbType.DateTime2; parameter.Value = utc; } public override DateTimeOffset Parse(object value) { if (value is DateTimeOffset dto) return dto; if (value is DateTime dt) return new DateTimeOffset( dt.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : dt); return DateTimeOffset.Parse(value.ToString()!); } } private sealed class NullableDateTimeOffsetFromDateTimeHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, DateTimeOffset? value) { if (value is null) { parameter.Value = DBNull.Value; return; } var utc = value.Value.UtcDateTime; if (value.Value == DateTimeOffset.MinValue || utc < _sqlMinDate) { parameter.Value = DBNull.Value; return; } parameter.DbType = DbType.DateTime2; parameter.Value = utc; } public override DateTimeOffset? Parse(object value) { if (value is DBNull || value is null) return null; if (value is DateTimeOffset dto) return dto; if (value is DateTime dt) return new DateTimeOffset( dt.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : dt); return DateTimeOffset.Parse(value.ToString()!); } } } }