From 46d8b417ac55803691d1998989b35df4e5d01926 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 24 Aug 2022 16:55:29 +0800 Subject: [PATCH] Add `InputDateTimeFormats` and `OutputDateTimeFormat` to `AbpJsonOptions`. Resolve #12338 --- .../Volo/Abp/Json/AbpJsonOptions.cs | 18 ++- ...pCamelCasePropertyNamesContractResolver.cs | 8 +- .../Json/Newtonsoft/AbpDateTimeConverter.cs | 116 ++++++++++++++++++ .../Newtonsoft/AbpDefaultContractResolver.cs | 8 +- .../Newtonsoft/AbpJsonIsoDateTimeConverter.cs | 60 --------- .../JsonConverters/AbpDateTimeConverter.cs | 24 ++-- .../AbpNullableDateTimeConverter.cs | 24 ++-- .../Mvc/Json/JsonResultController_Tests.cs | 2 +- .../Mvc/Json/JsonSerializer_Tests.cs | 2 +- ...pSystemTextJsonSerializerProvider_Tests.cs | 19 ++- 10 files changed, 183 insertions(+), 98 deletions(-) create mode 100644 framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs delete mode 100644 framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonIsoDateTimeConverter.cs diff --git a/framework/src/Volo.Abp.Json.Abstractions/Volo/Abp/Json/AbpJsonOptions.cs b/framework/src/Volo.Abp.Json.Abstractions/Volo/Abp/Json/AbpJsonOptions.cs index f7f5370255..043c1081d7 100644 --- a/framework/src/Volo.Abp.Json.Abstractions/Volo/Abp/Json/AbpJsonOptions.cs +++ b/framework/src/Volo.Abp.Json.Abstractions/Volo/Abp/Json/AbpJsonOptions.cs @@ -1,9 +1,21 @@ -namespace Volo.Abp.Json; +using System.Collections.Generic; + +namespace Volo.Abp.Json; public class AbpJsonOptions { /// - /// Used to set default value for the DateTimeFormat. + /// Formats of input JSON date, Empty string means default format. /// - public string DefaultDateTimeFormat { get; set; } + public List InputDateTimeFormats { get; set; } + + /// + /// Format of output json date, Null or empty string means default format. + /// + public string OutputDateTimeFormat { get; set; } + + public AbpJsonOptions() + { + InputDateTimeFormats = new List(); + } } diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpCamelCasePropertyNamesContractResolver.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpCamelCasePropertyNamesContractResolver.cs index a73213af6c..3e4e5d0121 100644 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpCamelCasePropertyNamesContractResolver.cs +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpCamelCasePropertyNamesContractResolver.cs @@ -9,12 +9,12 @@ namespace Volo.Abp.Json.Newtonsoft; public class AbpCamelCasePropertyNamesContractResolver : CamelCasePropertyNamesContractResolver, ITransientDependency { - private readonly Lazy _dateTimeConverter; + private readonly Lazy _dateTimeConverter; public AbpCamelCasePropertyNamesContractResolver(IServiceProvider serviceProvider) { - _dateTimeConverter = new Lazy( - serviceProvider.GetRequiredService, + _dateTimeConverter = new Lazy( + serviceProvider.GetRequiredService, true ); @@ -28,7 +28,7 @@ public class AbpCamelCasePropertyNamesContractResolver : CamelCasePropertyNamesC { var property = base.CreateProperty(member, memberSerialization); - if (AbpJsonIsoDateTimeConverter.ShouldNormalize(member, property)) + if (AbpDateTimeConverter.ShouldNormalize(member, property)) { property.Converter = _dateTimeConverter.Value; } diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs new file mode 100644 index 0000000000..3bd97f7aee --- /dev/null +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Volo.Abp.Reflection; +using Volo.Abp.Timing; + +namespace Volo.Abp.Json.Newtonsoft; + +public class AbpDateTimeConverter : DateTimeConverterBase +{ + private readonly string _dateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; + private readonly DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind; + private readonly CultureInfo _culture = CultureInfo.InvariantCulture; + private readonly IClock _clock; + private readonly AbpJsonOptions _options; + + public AbpDateTimeConverter(IClock clock, IOptions options) + { + _clock = clock; + _options = options.Value; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DateTime) || objectType == typeof(DateTime?); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var nullable = Nullable.GetUnderlyingType(objectType) != null; + if (reader.TokenType == JsonToken.Null) + { + if (!nullable) + { + throw new JsonSerializationException($"Cannot convert null value to {objectType.FullName}."); + } + + return null; + } + + if (reader.TokenType == JsonToken.Date) + { + return _clock.Normalize(reader.Value.To()); + } + + if (reader.TokenType != JsonToken.String) + { + throw new JsonSerializationException($"Unexpected token parsing date. Expected String, got {reader.TokenType}."); + } + + var dateText = reader.Value?.ToString(); + + if (dateText.IsNullOrEmpty() && nullable) + { + return null; + } + + if (_options.InputDateTimeFormats.Any()) + { + foreach (var format in _options.InputDateTimeFormats) + { + if (DateTime.TryParseExact(dateText, format, _culture, _dateTimeStyles, out var d1)) + { + return _clock.Normalize(d1); + } + } + } + + var date = !_dateTimeFormat.IsNullOrEmpty() ? + DateTime.ParseExact(dateText, _dateTimeFormat, _culture, _dateTimeStyles) : + DateTime.Parse(dateText, _culture, _dateTimeStyles); + + return _clock.Normalize(date); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + value = _clock.Normalize(value.To()); + } + + if (value is DateTime dateTime) + { + if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal || + (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal) + { + dateTime = dateTime.ToUniversalTime(); + } + + writer.WriteValue(_options.OutputDateTimeFormat.IsNullOrWhiteSpace() + ? dateTime.ToString(_dateTimeFormat, _culture) + : dateTime.ToString(_options.OutputDateTimeFormat, _culture)); + } + else + { + throw new JsonSerializationException($"Unexpected value when converting date. Expected DateTime or DateTimeOffset, got {value.GetType()}."); + } + } + + internal static bool ShouldNormalize(MemberInfo member, JsonProperty property) + { + if (property.PropertyType != typeof(DateTime) && + property.PropertyType != typeof(DateTime?)) + { + return false; + } + + return ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(member) == null; + } +} diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDefaultContractResolver.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDefaultContractResolver.cs index f39f6af578..131e95d4c2 100644 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDefaultContractResolver.cs +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDefaultContractResolver.cs @@ -9,12 +9,12 @@ namespace Volo.Abp.Json.Newtonsoft; public class AbpDefaultContractResolver : DefaultContractResolver, ITransientDependency { - private readonly Lazy _dateTimeConverter; + private readonly Lazy _dateTimeConverter; public AbpDefaultContractResolver(IServiceProvider serviceProvider) { - _dateTimeConverter = new Lazy( - serviceProvider.GetRequiredService, + _dateTimeConverter = new Lazy( + serviceProvider.GetRequiredService, true ); } @@ -23,7 +23,7 @@ public class AbpDefaultContractResolver : DefaultContractResolver, ITransientDep { var property = base.CreateProperty(member, memberSerialization); - if (AbpJsonIsoDateTimeConverter.ShouldNormalize(member, property)) + if (AbpDateTimeConverter.ShouldNormalize(member, property)) { property.Converter = _dateTimeConverter.Value; } diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonIsoDateTimeConverter.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonIsoDateTimeConverter.cs deleted file mode 100644 index b859688a0d..0000000000 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonIsoDateTimeConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Reflection; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Reflection; -using Volo.Abp.Timing; - -namespace Volo.Abp.Json.Newtonsoft; - -public class AbpJsonIsoDateTimeConverter : IsoDateTimeConverter, ITransientDependency -{ - private readonly IClock _clock; - - public AbpJsonIsoDateTimeConverter(IClock clock, IOptions abpJsonOptions) - { - _clock = clock; - - if (abpJsonOptions.Value.DefaultDateTimeFormat != null) - { - DateTimeFormat = abpJsonOptions.Value.DefaultDateTimeFormat; - } - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(DateTime) || objectType == typeof(DateTime?); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var date = base.ReadJson(reader, objectType, existingValue, serializer) as DateTime?; - - if (date.HasValue) - { - return _clock.Normalize(date.Value); - } - - return null; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var date = value as DateTime?; - base.WriteJson(writer, date.HasValue ? _clock.Normalize(date.Value) : value, serializer); - } - - internal static bool ShouldNormalize(MemberInfo member, JsonProperty property) - { - if (property.PropertyType != typeof(DateTime) && - property.PropertyType != typeof(DateTime?)) - { - return false; - } - - return ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(member) == null; - } -} diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs index 417037e776..366a933d13 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Options; @@ -21,20 +22,23 @@ public class AbpDateTimeConverter : JsonConverter, ITransientDependenc public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (!_options.DefaultDateTimeFormat.IsNullOrWhiteSpace()) + if (_options.InputDateTimeFormats.Any()) { if (reader.TokenType == JsonTokenType.String) { - var s = reader.GetString(); - if (DateTime.TryParseExact(s, _options.DefaultDateTimeFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1)) + foreach (var format in _options.InputDateTimeFormats) { - return _clock.Normalize(d1); + var s = reader.GetString(); + if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1)) + { + return _clock.Normalize(d1); + } } - - throw new JsonException($"'{s}' can't parse to DateTime({_options.DefaultDateTimeFormat})!"); } - - throw new JsonException("Reader's TokenType is not String!"); + else + { + throw new JsonException("Reader's TokenType is not String!"); + } } if (reader.TryGetDateTime(out var d3)) @@ -47,13 +51,13 @@ public class AbpDateTimeConverter : JsonConverter, ITransientDependenc public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - if (_options.DefaultDateTimeFormat.IsNullOrWhiteSpace()) + if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace()) { writer.WriteStringValue(_clock.Normalize(value)); } else { - writer.WriteStringValue(_clock.Normalize(value).ToString(_options.DefaultDateTimeFormat, CultureInfo.CurrentUICulture)); + writer.WriteStringValue(_clock.Normalize(value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture)); } } } diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs index 8b3459fcce..da5983250d 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpNullableDateTimeConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Options; @@ -21,20 +22,23 @@ public class AbpNullableDateTimeConverter : JsonConverter, ITransient public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (!_options.DefaultDateTimeFormat.IsNullOrWhiteSpace()) + if (_options.InputDateTimeFormats.Any()) { if (reader.TokenType == JsonTokenType.String) { - var s = reader.GetString(); - if (DateTime.TryParseExact(s, _options.DefaultDateTimeFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1)) + foreach (var format in _options.InputDateTimeFormats) { - return _clock.Normalize(d1); + var s = reader.GetString(); + if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var d1)) + { + return _clock.Normalize(d1); + } } - - throw new JsonException($"'{s}' can't parse to DateTime({_options.DefaultDateTimeFormat})!"); } - - throw new JsonException("Reader's TokenType is not String!"); + else + { + throw new JsonException("Reader's TokenType is not String!"); + } } if (reader.TryGetDateTime(out var d2)) @@ -53,13 +57,13 @@ public class AbpNullableDateTimeConverter : JsonConverter, ITransient } else { - if (_options.DefaultDateTimeFormat.IsNullOrWhiteSpace()) + if (_options.OutputDateTimeFormat.IsNullOrWhiteSpace()) { writer.WriteStringValue(_clock.Normalize(value.Value)); } else { - writer.WriteStringValue(_clock.Normalize(value.Value).ToString(_options.DefaultDateTimeFormat, CultureInfo.CurrentUICulture)); + writer.WriteStringValue(_clock.Normalize(value.Value).ToString(_options.OutputDateTimeFormat, CultureInfo.CurrentUICulture)); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonResultController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonResultController_Tests.cs index 851ef88db8..e7015313cb 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonResultController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonResultController_Tests.cs @@ -14,7 +14,7 @@ public class JsonResultController_Tests : AspNetCoreMvcTestBase { services.Configure(options => { - options.DefaultDateTimeFormat = "yyyy*MM*dd"; + options.OutputDateTimeFormat = "yyyy*MM*dd"; }); base.ConfigureServices(context, services); diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonSerializer_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonSerializer_Tests.cs index 2f986c89c3..1613e4bb49 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonSerializer_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Json/JsonSerializer_Tests.cs @@ -21,7 +21,7 @@ public class JsonSerializer_Tests : AspNetCoreMvcTestBase { services.Configure(options => { - options.DefaultDateTimeFormat = "yyyy*MM*dd"; + options.OutputDateTimeFormat = "yyyy*MM*dd"; }); base.ConfigureServices(context, services); diff --git a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSerializerProvider_Tests.cs b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSerializerProvider_Tests.cs index f9823e7d65..f605914d6e 100644 --- a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSerializerProvider_Tests.cs +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSerializerProvider_Tests.cs @@ -220,7 +220,8 @@ public class AbpSystemTextJsonSerializerProvider_DateTimeFormat_Tests : AbpSyste { services.Configure(options => { - options.DefaultDateTimeFormat = "yyyy*MM*dd"; + options.InputDateTimeFormats.Add("yyyy*MM*dd"); + options.OutputDateTimeFormat = "yyyy*MM*dd HH*mm*ss"; }); } @@ -233,8 +234,12 @@ public class AbpSystemTextJsonSerializerProvider_DateTimeFormat_Tests : AbpSyste file.CreationTime.Month.ShouldBe(11); file.CreationTime.Day.ShouldBe(20); - var newJson = JsonSerializer.Serialize(file); - newJson.ShouldBe(json); + json = JsonSerializer.Serialize(new FileWithDatetime() + { + Name = "abp", + CreationTime = new DateTime(2020, 11, 20, 12, 34, 56) + }); + json.ShouldContain("\"2020*11*20 12*34*56\""); } [Fact] @@ -256,8 +261,12 @@ public class AbpSystemTextJsonSerializerProvider_DateTimeFormat_Tests : AbpSyste file.CreationTime.Value.Month.ShouldBe(11); file.CreationTime.Value.Day.ShouldBe(20); - var newJson = JsonSerializer.Serialize(file); - newJson.ShouldBe(json); + json = JsonSerializer.Serialize(new FileWithDatetime() + { + Name = "abp", + CreationTime = new DateTime(2020, 11, 20, 12, 34, 56) + }); + json.ShouldContain("\"2020*11*20 12*34*56\""); } }