Skip to content

Commit

Permalink
Proper JSON serialization support for NodaTime and BCL date/time types
Browse files Browse the repository at this point in the history
Leftovers from #2922
  • Loading branch information
roji committed Nov 15, 2023
1 parent 05b9d51 commit 778cb45
Show file tree
Hide file tree
Showing 34 changed files with 1,165 additions and 235 deletions.
35 changes: 32 additions & 3 deletions src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util;
Expand All @@ -23,7 +25,7 @@ public class DateMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public DateMapping()
: base("date", typeof(LocalDate), NpgsqlDbType.Date)
: base("date", typeof(LocalDate), NpgsqlDbType.Date, JsonLocalDateReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -72,9 +74,10 @@ protected override string GenerateNonNullSqlLiteral(object value)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateEmbeddedNonNullSqlLiteral(object value)
{
var date = (LocalDate)value;
=> FormatLocalDate((LocalDate)value);

private static string FormatLocalDate(LocalDate date)
{
if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
if (date == LocalDate.MinIsoValue)
Expand Down Expand Up @@ -102,4 +105,30 @@ public override Expression GenerateCodeLiteral(object value)
var date = (LocalDate)value;
return ConstantNew(Constructor, date.Year, date.Month, date.Day);
}

private sealed class JsonLocalDateReaderWriter : JsonValueReaderWriter<LocalDate>
{
public static JsonLocalDateReaderWriter Instance { get; } = new();

public override LocalDate FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
{
var s = manager.CurrentReader.GetString()!;

if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
switch (s)
{
case "-infinity":
return LocalDate.MinIsoValue;
case "infinity":
return LocalDate.MaxIsoValue;
}
}

return LocalDatePattern.Iso.Parse(s).GetValueOrThrow();
}

public override void ToJsonTyped(Utf8JsonWriter writer, LocalDate value)
=> writer.WriteStringValue(FormatLocalDate(value));
}
}
26 changes: 24 additions & 2 deletions src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -32,7 +34,7 @@ public class DurationIntervalMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public DurationIntervalMapping()
: base("interval", typeof(Duration), NpgsqlDbType.Interval)
: base("interval", typeof(Duration), NpgsqlDbType.Interval, JsonDurationReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -72,7 +74,16 @@ public override RelationalTypeMapping WithStoreTypeAndSize(string storeType, int
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"INTERVAL '{NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan())}'";
=> $"INTERVAL '{GenerateEmbeddedNonNullSqlLiteral(value)}'";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateEmbeddedNonNullSqlLiteral(object value)
=> NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan());

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -115,4 +126,15 @@ public override Expression GenerateCodeLiteral(object value)
void Compose(Expression toAdd)
=> e = e is null ? toAdd : Expression.Add(e, toAdd);
}

private sealed class JsonDurationReaderWriter : JsonValueReaderWriter<Duration>
{
public static JsonDurationReaderWriter Instance { get; } = new();

public override Duration FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> Duration.FromTimeSpan(NpgsqlIntervalTypeMapping.ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!));

public override void ToJsonTyped(Utf8JsonWriter writer, Duration value)
=> writer.WriteStringValue(NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(value.ToTimeSpan()));
}
}
15 changes: 14 additions & 1 deletion src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

Expand Down Expand Up @@ -35,7 +37,7 @@ public class PeriodIntervalMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PeriodIntervalMapping()
: base("interval", typeof(Period), NpgsqlDbType.Interval)
: base("interval", typeof(Period), NpgsqlDbType.Interval, JsonPeriodReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -150,4 +152,15 @@ public override Expression GenerateCodeLiteral(object value)
void Compose(Expression toAdd)
=> e = e is null ? toAdd : Expression.Add(e, toAdd);
}

private sealed class JsonPeriodReaderWriter : JsonValueReaderWriter<Period>
{
public static JsonPeriodReaderWriter Instance { get; } = new();

public override Period FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> PeriodPattern.NormalizingIso.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow();

public override void ToJsonTyped(Utf8JsonWriter writer, Period value)
=> writer.WriteStringValue(PeriodPattern.NormalizingIso.Format(value));
}
}
15 changes: 14 additions & 1 deletion src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util;
Expand Down Expand Up @@ -31,7 +33,7 @@ public class TimeMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public TimeMapping()
: base("time", typeof(LocalTime), NpgsqlDbType.Time)
: base("time", typeof(LocalTime), NpgsqlDbType.Time, JsonLocalTimeReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -106,4 +108,15 @@ public override Expression GenerateCodeLiteral(object value)
? ConstantNew(ConstructorWithSeconds, time.Hour, time.Minute, time.Second)
: ConstantNew(ConstructorWithMinutes, time.Hour, time.Minute);
}

private sealed class JsonLocalTimeReaderWriter : JsonValueReaderWriter<LocalTime>
{
public static JsonLocalTimeReaderWriter Instance { get; } = new();

public override LocalTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> LocalTimePattern.ExtendedIso.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow();

public override void ToJsonTyped(Utf8JsonWriter writer, LocalTime value)
=> writer.WriteStringValue(LocalTimePattern.ExtendedIso.Format(value));
}
}
22 changes: 16 additions & 6 deletions src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util;
Expand Down Expand Up @@ -43,7 +45,7 @@ public class TimeTzMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public TimeTzMapping()
: base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz)
: base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz, JsonOffsetTimeReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -92,7 +94,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMETZ '{GenerateLiteralCore(value)}'";
=> $"TIMETZ '{Pattern.Format((OffsetTime)value)}'";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -101,10 +103,7 @@ protected override string GenerateNonNullSqlLiteral(object value)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateEmbeddedNonNullSqlLiteral(object value)
=> $@"""{GenerateLiteralCore(value)}""";

private string GenerateLiteralCore(object value)
=> Pattern.Format((OffsetTime)value);
=> $@"""{Pattern.Format((OffsetTime)value)}""";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -140,4 +139,15 @@ public override Expression GenerateCodeLiteral(object value)
? ConstantCall(OffsetFromHoursMethod, offsetSeconds / 3600)
: ConstantCall(OffsetFromSeconds, offsetSeconds));
}

private sealed class JsonOffsetTimeReaderWriter : JsonValueReaderWriter<OffsetTime>
{
public static JsonOffsetTimeReaderWriter Instance { get; } = new();

public override OffsetTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> Pattern.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow();

public override void ToJsonTyped(Utf8JsonWriter writer, OffsetTime value)
=> writer.WriteStringValue(Pattern.Format(value));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util;
Expand Down Expand Up @@ -29,7 +31,7 @@ public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public TimestampLocalDateTimeMapping()
: base("timestamp without time zone", typeof(LocalDateTime), NpgsqlDbType.Timestamp)
: base("timestamp without time zone", typeof(LocalDateTime), NpgsqlDbType.Timestamp, JsonLocalDateTimeReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -78,7 +80,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMP '{GenerateLiteralCore(value)}'";
=> $"TIMESTAMP '{Format((LocalDateTime)value)}'";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -87,21 +89,18 @@ protected override string GenerateNonNullSqlLiteral(object value)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateEmbeddedNonNullSqlLiteral(object value)
=> $@"""{GenerateLiteralCore(value)}""";
=> $@"""{Format((LocalDateTime)value)}""";

private string GenerateLiteralCore(object value)
private static string Format(LocalDateTime localDateTime)
{
var localDateTime = (LocalDateTime)value;

// TODO: Switch to use LocalDateTime.MinMaxValue when available (#4061)
if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
if (localDateTime == LocalDate.MinIsoValue + LocalTime.MinValue)
if (localDateTime == LocalDateTime.MinIsoValue)
{
return "-infinity";
}

if (localDateTime == LocalDate.MaxIsoValue + LocalTime.MaxValue)
if (localDateTime == LocalDateTime.MaxIsoValue)
{
return "infinity";
}
Expand Down Expand Up @@ -133,4 +132,30 @@ internal static Expression GenerateCodeLiteral(LocalDateTime dateTime)
? newExpr
: Expression.Call(newExpr, PlusNanosecondsMethod, Expression.Constant((long)dateTime.NanosecondOfSecond));
}

private sealed class JsonLocalDateTimeReaderWriter : JsonValueReaderWriter<LocalDateTime>
{
public static JsonLocalDateTimeReaderWriter Instance { get; } = new();

public override LocalDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
{
var s = manager.CurrentReader.GetString()!;

if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
switch (s)
{
case "-infinity":
return LocalDateTime.MinIsoValue;
case "infinity":
return LocalDateTime.MaxIsoValue;
}
}

return LocalDateTimePattern.ExtendedIso.Parse(s).GetValueOrThrow();
}

public override void ToJsonTyped(Utf8JsonWriter writer, LocalDateTime value)
=> writer.WriteStringValue(Format(value));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

Expand All @@ -19,7 +22,7 @@ public class TimestampTzInstantMapping : NpgsqlTypeMapping
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public TimestampTzInstantMapping()
: base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz)
: base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz, JsonInstantReaderWriter.Instance)
{
}

Expand Down Expand Up @@ -68,7 +71,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMPTZ '{GenerateLiteralCore(value)}'";
=> $"TIMESTAMPTZ '{Format((Instant)value)}'";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -77,12 +80,10 @@ protected override string GenerateNonNullSqlLiteral(object value)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateEmbeddedNonNullSqlLiteral(object value)
=> $@"""{GenerateLiteralCore(value)}""";
=> $@"""{Format((Instant)value)}""";

private string GenerateLiteralCore(object value)
private static string Format(Instant instant)
{
var instant = (Instant)value;

if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
if (instant == Instant.MinValue)
Expand Down Expand Up @@ -113,4 +114,30 @@ public override Expression GenerateCodeLiteral(object value)

private static readonly MethodInfo _fromUnixTimeTicks
= typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), new[] { typeof(long) })!;

private sealed class JsonInstantReaderWriter : JsonValueReaderWriter<Instant>
{
public static JsonInstantReaderWriter Instance { get; } = new();

public override Instant FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
{
var s = manager.CurrentReader.GetString()!;

if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions)
{
switch (s)
{
case "-infinity":
return Instant.MinValue;
case "infinity":
return Instant.MaxValue;
}
}

return InstantPattern.ExtendedIso.Parse(s).GetValueOrThrow();
}

public override void ToJsonTyped(Utf8JsonWriter writer, Instant value)
=> writer.WriteStringValue(Format(value));
}
}
Loading

0 comments on commit 778cb45

Please sign in to comment.