Solving the Entity Framework part
We need to tell EF how to handle DateOnly and DateOnly? — so we first need a pair of converters:
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace MyProject.Data
{
/// <summary>
/// Converts <seecref="DateOnly" /> to <seecref="DateTime"/> and vice
versa.
/// </summary>
public class DateOnlyConverter : ValueConverter<DateOnly, DateTime>
{
/// <summary>
/// Creates a new instance of this converter.
/// </summary>
public DateOnlyConverter() : base(
d=>d.ToDateTime(TimeOnly.MinValue),
d=>DateOnly.FromDateTime(d))
{ }
}
/// <summary>
/// Converts <seecref="DateOnly?" /> to <seecref="DateTime?"/> and
vice versa.
/// </summary>
public class NullableDateOnlyConverter : ValueConverter<DateOnly?,
DateTime?>
{
/// <summary>
/// Creates a new instance of this converter.
/// </summary>
public NullableDateOnlyConverter() : base(
d=>d==null
?null
:new DateTime?(d.Value.ToDateTime(TimeOnly.MinValue)),
d=>d==null
?null
:new DateOnly?(DateOnly.FromDateTime(d.Value)))
{ }
}
}
Then we need to configure the conventions in our ApplicationDbContext to use them:
protected override void ConfigureConventions
(ModelConfigurationBuilder builder)
{
builder.Properties<DateOnly>()
.HaveConversion<DateOnlyConverter>()
.HaveColumnType("date");
builder.Properties<DateOnly?>()
.HaveConversion<NullableDateOnlyConverter>()
.HaveColumnType("date");
}
Tadah! EF Core can now handle DateOnly properly and uses the DATE column type to store it.
Solving the WebAPI Part
Like with EF, we need another pair of converters to tell the JSON serializer how to (de)serialize DateOnly and DateOnly?.
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
public class DateOnlyConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(
refUtf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TryGetDateTime(out var dt))
{
return DateOnly.FromDateTime(dt);
};
var value = reader.GetString();
if (value == null)
{
return default;
}
var match = new Regex("^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)
(T|\\s|\\z)").Match(value);
return match.Success
? new DateOnly(int.Parse(match.Groups[1].Value),
int.Parse(match.Groups[2].Value),
int.Parse(match.Groups[3].Value))
:default;
}
public override void Write(Utf8JsonWriter writer,
DateOnly value,
JsonSerializerOptions options)
=>writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
public class DateOnlyNullableConverter : JsonConverter<DateOnly?>
{
public override DateOnly? Read(refUtf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TryGetDateTime(out var dt))
{
return DateOnly.FromDateTime(dt);
};
var value = reader.GetString();
if (value == null)
{
return default;
}
var match = new Regex("^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)
(T|\\s|\\z)").Match(value);
return match.Success
? new DateOnly(int.Parse(match.Groups[1].Value),
int.Parse(match.Groups[2].Value),
int.Parse(match.Groups[3].Value)):default;
}
public override void Write(Utf8JsonWriter writer,
DateOnly? value,
JsonSerializerOptions options)
=>writer.WriteStringValue(value?.ToString("yyyy-MM-dd"));
}
public static class DateOnlyConverterExtensions
{
public static void AddDateOnlyConverters(thisJsonSerializerOptions
options)
{
options.Converters.Add(newDateOnlyConverter());
options.Converters.Add(newDateOnlyNullableConverter());
}
}
Then we can tell Web API to use it in our Program/Startup.cs by changing the JSON options.
builder.Services
.AddMvc()
.AddJsonOptions(opt =>
opt.JsonSerializerOptions.AddDateOnlyConverters());
The problem solved from both sides. It seems likely that .Net 6 and EF will get updates to make both of these redundant in the future, but these work well and are sufficiently elegant that I’m using them in production — and should be forward compatible once official support is added.
Source: Medium - Michael Biggins
The Tech Platform
Comments