用于在 Blazor 和 JS 之间传递 DateTimeOffset 的自定义 Json 转换器。 JS 属性 Input/Output 是不同的类型

Custom Json Converter For Passing DateTimeOffset between Blazor and JS. JS property Input/Output are different types

我有一个场景,我必须将 DateTimeOffset 序列化为 JSON 作为简单的字符串值(例如 "2021-04-07T18:18:00.000Z",但将其反序列化为对象中嵌入的值(例如 {"_date":"2021-04-07T18:18:00.000Z"})。我怎样才能用 System.Text.Json 做到这一点?

详情如下。我正在为 blazor 包装一个 calendar UI lib。我用 C# classes 封装了 JS classes。从 Blazor 向 JS 发送日历“计划”(事件)时,DateTimes 只是作为序列化字符串传递。这行得通。

当从 JS 向 Blazor 发送“计划”时,它作为一个对象 (TZdate) 返回,里面是一个“_date”属性。

返回的json对象如下所示:

{
"end":{
      "_date":"2021-04-07T18:18:00.000Z"
       }
}

我试过为 DateTimeOffset 编写自定义转换器。这是读取方法:

        public override DateTimeOffset Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) =>
            DateTimeOffset.ParseExact(JsonDocument.Parse(reader.GetString()).RootElement.EnumerateObject().First().Value.EnumerateObject().First().Value.GetString(),
             TZDateFormat, CultureInfo.InvariantCulture);

以上方法无效。我不相信我没有正确使用 reader class 或者如何从这里的 json 中提取“_date”。

或者,我尝试用新的“TZDate”class 和 属性“_date”包装我的 DateTimeOffset 属性,但这在 JS 端中断,因为库是期待来自 C# 的简单日期时间字符串,而不是对象。

更改 JS 库可能不是选项。

我有哪些选择或如何解决这个问题?

您的 DateTimeOffset 值嵌入到对象中,如下所示:

{"_date":"2021-04-07T18:18:00.000Z"}

并且您想提取内部 _date 属性 的值到 return。您可以使用以下 JsonConverter<DateTime>.Read() 方法做到这一点:

public class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
    const string TZDateFormat = "O"; // Your custom format (not shown in your question).

    public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
        // Write as a simple string.
        writer.WriteStringValue(value.ToString(TZDateFormat, CultureInfo.InvariantCulture));
    
    const string _date = "_date";

    public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.String:
                // A simple DateTimeOffset string "value"
                return DateTimeOffset.ParseExact(reader.GetString(), TZDateFormat, CultureInfo.InvariantCulture);
            case JsonTokenType.StartObject:
            {
                // A DateTimeOffset string embedded in an object { "_date" : "value" }
                using var doc = JsonDocument.ParseValue(ref reader);
                if (doc.RootElement.TryGetProperty(_date, out var value))
                    return DateTimeOffset.ParseExact(value.GetString(), TZDateFormat, CultureInfo.InvariantCulture);
                return default(DateTimeOffset); // Or throw an exception?
            }
            default:
                throw new JsonException(); // Unknown token type
        }
    }
}

在您的 Read() 方法中,您尝试将 reader.GetString() 编辑的值 return 加载到 JsonDocument 中,但在方法的开头 reader 位于 StartObject 标记上而不是值字符串,并且 reader.GetString() only returns the string value of the current token, not the current token and its children as you would seem to want. To load the current token and its children into a JsonDocument, use JsonDocument.ParseValue(Utf8JsonReader).

(顺便说一句,JsonDocument是disposable,实际上必须disposed到return pooled memory回到内存池。)

如果您想避免构建 JsonDocument,您可以直接使用 Utf8JsonReader 流式传输 JSON,如下所示:

static byte [] _date = Encoding.UTF8.GetBytes("_date");

public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    switch (reader.TokenType)
    {
        case JsonTokenType.String:
            // A simple DateTimeOffset string "value"
            return DateTimeOffset.ParseExact(reader.GetString(), TZDateFormat, CultureInfo.InvariantCulture);
        case JsonTokenType.StartObject:
        {
            // A DateTimeOffset string embedded in an object { "_date" : "value" }
            DateTimeOffset? value = null;
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.EndObject:
                        return value.GetValueOrDefault();
                    case JsonTokenType.PropertyName:
                        var match = reader.ValueTextEquals(_date);
                        reader.Read();
                        if (match)
                            value = DateTimeOffset.ParseExact(reader.GetString(), TZDateFormat, CultureInfo.InvariantCulture);
                        else
                            reader.Skip();
                        break;
                    default:
                        throw new JsonException();
                }
            }
        }
        break;
    }
    throw new JsonException();
}

这有点复杂,但性能应该也更高。

在这两种情况下,我都会检查传入值是一个对象还是一个简单的字符串。如果是一个简单的字符串,我会继续将其解析为 DateTimeOffset.

演示 fiddle here.