Pomelo.EntityFrameworkCore.MySql
Pomelo.EntityFrameworkCore.MySql copied to clipboard
Feature request: provide a way to set JsonSerializer options
Currently when using UseMicrosoftJson
there does not appear to be a way to specify the serialization options (JsonSerializerOptions
) for JsonSerializer to use (eg the second parameter passed to JsonSerializer.Serialize
)
You can't set these globally in System.Text.Json, so you are stuck with the defaults - so no way to change casing or null handling, converters, etc etc.
It would be best if you could pass these into UseMicrosoftJson
@jonnermut Yeah, we should tackle this.
Any updates on this? i am having problem in the json TimeSpan deserialization, the converter i used globally doesn't apply here
"EndTime":{"Ticks":863990000000,"Days":0,"Hours":23,"Milliseconds":0,"Minutes":59,"Seconds":59,"TotalDays":0.999988425925926,"TotalHours":23.99972222222222,"TotalMilliseconds":86399000,"TotalMinutes":1439.9833333333333,"TotalSeconds":86399}
always return empty result for my case
@alandotic You should normally be able to set this up without having to specify custom JSON serializer options at all, just by specifying a JsonConverter
for the TimeSpan
property of your JSON (POCO) object explicitly:
Program.cs
using System;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
/// <summary>
/// EF Core entity.
/// </summary>
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public IceCreamDetails Details { get; set; }
}
/// <summary>
/// JSON object.
/// </summary>
public class IceCreamDetails
{
public double Kilojoule { get; set; }
[JsonConverter(typeof(TimeSpanConverter))] // <-- Setup converter for TimeSpan property
public TimeSpan MaxStorageDuration { get; set; }
}
/// <summary>
/// Custom JSON -- TimeSpan converter.
/// </summary>
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> TimeSpan.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=Issue1266";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(connectionString, serverVersion, o => o.UseMicrosoftJson())
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
entity.Property(e => e.Details)
.HasColumnType("json");
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Details = new IceCreamDetails
{
Kilojoule = 866.0,
MaxStorageDuration = new TimeSpan(180, 0, 0, 0)
}
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Details = new IceCreamDetails
{
Kilojoule = 904.0,
MaxStorageDuration = new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345))
}
},
new IceCream
{
IceCreamId = 3,
Name = "Matcha",
}
);
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreams = context.IceCreams
.OrderBy(i => i.IceCreamId)
.ToList();
Trace.Assert(iceCreams.Count == 3);
Trace.Assert(iceCreams[0].Details is not null);
Trace.Assert(iceCreams[0].Details.MaxStorageDuration == new TimeSpan(180, 0, 0, 0));
Trace.Assert(iceCreams[1].Details is not null);
Trace.Assert(iceCreams[1].Details.MaxStorageDuration == new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345)));
Trace.Assert(iceCreams[2].Details is null);
}
}
}
Another way is to use JsonSerializerOptions
after all and pass them to your own custom EF Core ValueConverter
:
Program.cs
using System;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
/// <summary>
/// EF Core entity.
/// </summary>
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public IceCreamDetails Details { get; set; }
}
/// <summary>
/// JSON object.
/// </summary>
public class IceCreamDetails
{
public double Kilojoule { get; set; }
public TimeSpan MaxStorageDuration { get; set; }
}
/// <summary>
/// Custom System.Text.Json -- TimeSpan converter.
/// </summary>
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> TimeSpan.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}
/// <summary>
/// Custom EF Core JSON ValueCoverter for System.Text.Json.
/// </summary>
public class MySqlJsonMicrosoftPocoValueConverterWithOptions<T> : ValueConverter<T, string>
{
public MySqlJsonMicrosoftPocoValueConverterWithOptions(JsonSerializerOptions options)
: base(
v => ConvertToProviderCore(v, options),
v => ConvertFromProviderCore(v, options))
{
}
private static string ConvertToProviderCore(T v, JsonSerializerOptions options)
=> JsonSerializer.Serialize(v, options);
private static T ConvertFromProviderCore(string v, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<T>(v, options);
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=Issue1266_01";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(connectionString, serverVersion, o => o.UseMicrosoftJson())
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Setup your JSON serializer options.
var jsonSerializerOptions = new JsonSerializerOptions();
jsonSerializerOptions.Converters.Add(new TimeSpanConverter());
modelBuilder.Entity<IceCream>(
entity =>
{
entity.Property(e => e.Details)
.HasColumnType("json")
.HasConversion(
new MySqlJsonMicrosoftPocoValueConverterWithOptions<IceCreamDetails>(
jsonSerializerOptions)); // <-- Use your custom value converter
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Details = new IceCreamDetails
{
Kilojoule = 866.0,
MaxStorageDuration = new TimeSpan(180, 0, 0, 0)
}
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Details = new IceCreamDetails
{
Kilojoule = 904.0,
MaxStorageDuration = new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345))
}
},
new IceCream
{
IceCreamId = 3,
Name = "Matcha",
}
);
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreams = context.IceCreams
.OrderBy(i => i.IceCreamId)
.ToList();
Trace.Assert(iceCreams.Count == 3);
Trace.Assert(iceCreams[0].Details is not null);
Trace.Assert(iceCreams[0].Details.MaxStorageDuration == new TimeSpan(180, 0, 0, 0));
Trace.Assert(iceCreams[1].Details is not null);
Trace.Assert(iceCreams[1].Details.MaxStorageDuration == new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345)));
Trace.Assert(iceCreams[2].Details is null);
}
}
}
Finally, if you have many JSON properties you want to apply your custom ValueConverter
to, you could just automate this with a loop at the end of your Fluent API definitions:
Program.cs
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
/// <summary>
/// EF Core entity.
/// </summary>
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
[Column(TypeName = "json")]
public IceCreamDetails Details { get; set; }
}
/// <summary>
/// JSON object.
/// </summary>
public class IceCreamDetails
{
public double Kilojoule { get; set; }
public TimeSpan MaxStorageDuration { get; set; }
}
/// <summary>
/// Custom JSON -- TimeSpan converter.
/// </summary>
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> TimeSpan.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}
//
// Custom EF Core JSON ValueCoverter for System.Text.Json:
//
public class MySqlJsonMicrosoftPocoValueConverterWithOptions<T> : ValueConverter<T, string>
{
public MySqlJsonMicrosoftPocoValueConverterWithOptions(JsonSerializerOptions options)
: base(
v => ConvertToProviderCore(v, options),
v => ConvertFromProviderCore(v, options))
{
}
private static string ConvertToProviderCore(T v, JsonSerializerOptions options)
=> JsonSerializer.Serialize(v, options);
private static T ConvertFromProviderCore(string v, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<T>(v, options);
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=Issue1266_02";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(connectionString, serverVersion, o => o.UseMicrosoftJson())
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Details = new IceCreamDetails
{
Kilojoule = 866.0,
MaxStorageDuration = new TimeSpan(180, 0, 0, 0)
}
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Details = new IceCreamDetails
{
Kilojoule = 904.0,
MaxStorageDuration = new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345))
}
},
new IceCream
{
IceCreamId = 3,
Name = "Matcha",
}
);
});
//
// Set your custom converter on all JSON properties:
//
var jsonSerializerOptions = new JsonSerializerOptions();
jsonSerializerOptions.Converters.Add(new TimeSpanConverter());
var jsonValueConverter = new MySqlJsonMicrosoftPocoValueConverterWithOptions<IceCreamDetails>(
jsonSerializerOptions);
foreach (var jsonProperty in modelBuilder.Model.GetEntityTypes()
.SelectMany(
e => e.GetDeclaredProperties()
.Where(
p => string.Equals(p.GetColumnBaseName(), "json", StringComparison.OrdinalIgnoreCase))))
{
jsonProperty.SetValueConverter(jsonValueConverter);
}
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreams = context.IceCreams
.OrderBy(i => i.IceCreamId)
.ToList();
Trace.Assert(iceCreams.Count == 3);
Trace.Assert(iceCreams[0].Details is not null);
Trace.Assert(iceCreams[0].Details.MaxStorageDuration == new TimeSpan(180, 0, 0, 0));
Trace.Assert(iceCreams[1].Details is not null);
Trace.Assert(iceCreams[1].Details.MaxStorageDuration == new TimeSpan(270, 12, 30, 21, 987).Add(TimeSpan.FromTicks(12345)));
Trace.Assert(iceCreams[2].Details is null);
}
}
}
Reminder to myself: I've got a local branch that is about half-way done.
@lauxjpn
Will it be possible to configure options at the property level? For example:
builder
.Property(f => f.Metadata)
.HasColumnType("json")
.JsonSerializerOptions((options) => ...);
@lauxjpn
Will it be possible to configure options at the property level? For example:
builder .Property(f => f.Metadata) .HasColumnType("json") .JsonSerializerOptions((options) => ...);
Yeah, something similar to that is what we will implement.