Base solution for your next web application
Open Closed

Issue with DateTime UTC #12002


User avatar
0
gekko created

Solution: Angular + Asp.Net Core Version: 13.1.1

Issue: After upgrading from version 12.2.1 to Version 13.1.1 submitting a datetime value as UTC does not work correctly.

After you upgraded to .Net 8 , you also removed .AddNewtonsoftJson() from Startup.cs

What I do is submitting data that contains a date value explicit as UTC, here an example of Json data we want to submit:

{
    "id": 3133,
    "name": "Test mission",
	...
    "date": "2024-05-10T00:00:00.000Z",
	...
}

As you can see, the "date" value is formatted as UTC.

The c# Type of "date" is DateTime:

public DateTime Date { get; set; }

When submitting the data to the api, the date kind is now not Utc but local. I tested this:

without .AddNewtonsoftJson() in Startup.cs

var test = input.Date.Kind; // => local

but with .AddNewtonsoftJson() in Startup.cs

var test = input.Date.Kind; // => Utc

I did a simple additional test in the same CreateOrUpdate method (without .AddNewtonsoftJson() in Startup.cs)

var testDate = System.Text.Json.JsonSerializer.Deserialize<DateTime>("\"2024-05-14T00:00:00.000Z\"");
var testKind = testDate.Kind; // => Utc

From this simple test I can see that System.Text.Json can understand that this is intended as Utc date, but why is it not with the date I am sending to the api?

The problem with the new setting is, that the data saved to the database does change the time part of the date. In my case it has a value of +2 hours : 2024-05-14 02:00:00.0000000

All suggestions about this I found was to implement a custom JsonConverter for Datetime. I tried to implement one, but unfortunately it does never hit:

Converter:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace custom.JsonConverters
{
    public class JsonDateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString()).ToUniversalTime();
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
        }
    }
}

In Startup.cs :

mvcBuilder.AddJsonOptions(options =>
{
	options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
});

Do you have an idea what is wrong?


8 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @gekko

    Thanks, we will check this and get back to you.

  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @gekko

    Can you try again after adding Clock.Provider = ClockProviders.Utc to the PreInitialize method in the *CoreModule in the *Core project?

  • User Avatar
    0
    gekko created

    Hi, I tried your suggested change. Actually, if I send a change to the api, the date(s) are then of kind Utc as expected.

    However, it does also change the dates format when data is fetched with a get request. With Clock.Provider = ClockProviders.Utc all dates are then formatted as Utc too. That, unfortunately breaks all the parts where we deal with dates and times in our client applications, because before the change the dates in get requests were not formatted as UTC.

    If we would start from scratch this would not be a problem. But we have to deal with this all over the place, and not only in the angular app, but also in mobile apps too.

    But that is not the only problem. The even bigger problem is, because we do not only have the angular app as a client, but also mobile app(s) on Appstores. From the moment we would publish the api change and with that the current behaviour, our client apps would be broken. We do not have control that all the client apps are then immediately updated. So we risk that customers app is broken, or wrong data is displayed or edited...

    Is there a way to control the behavior more precisely so that it behaves as before? Sure, we can let .AddNewtonsoftJson() in Startup.cs for the moment. But we'd prefer to make it compatible with our solution without it. What I still don't get is, why the JsonDateTimeConverter does not work.

    using System;
    using System.Text.Json;
    using System.Text.Json.Serialization;
    
    namespace custom.JsonConverters
    {
        public class JsonDateTimeConverter : JsonConverter<DateTime>
        {
            public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                return DateTime.Parse(reader.GetString()).ToUniversalTime();
            }
    
            public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
            {
                writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
            }
        }
    }
    

    Because I made a similar one, a JsonDoubleConverter (because with another issue with double values), and that one was actually applied.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Can you try this ?

    First, add line below to your Startup.cs right after var mvcBuilder... line;

    services.AddOptions<JsonOptions>()
                    .Configure<IServiceProvider>((options, rootServiceProvider) =>
                    {
                        var aspNetCoreConfiguration = rootServiceProvider.GetRequiredService<IAbpAspNetCoreConfiguration>();
                        options.JsonSerializerOptions.TypeInfoResolver = new MyAbpDateTimeJsonTypeInfoResolver(aspNetCoreConfiguration.InputDateTimeFormats, aspNetCoreConfiguration.OutputDateTimeFormat);                    
                    });
    

    and define used classes as shown below;

    public class MyDateTimeConverter : JsonConverter<DateTime>
    {
        protected List<string> InputDateTimeFormats { get; set; }
        protected string OutputDateTimeFormat { get; set; }
    
        public MyDateTimeConverter(List<string> inputDateTimeFormats = null, string outputDateTimeFormat = null)
        {
            InputDateTimeFormats = inputDateTimeFormats ?? new List<string>();
            OutputDateTimeFormat = outputDateTimeFormat;
        }
    
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (!InputDateTimeFormats.IsNullOrEmpty())
            {
                if (reader.TokenType == JsonTokenType.String)
                {
                    var s = reader.GetString();
                    foreach (var format in InputDateTimeFormats)
                    {
                        if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None,
                                out var outDateTime))
                        {
                            return Clock.Normalize(outDateTime);
                        }
                    }
                }
                else
                {
                    throw new JsonException("Reader's TokenType is not String!");
                }
            }
    
            var dateText = reader.GetString();
            if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var date))
            {
                return Clock.Normalize(date);
            }
    
            throw new JsonException("Can't get datetime from the reader!");
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            if (OutputDateTimeFormat.IsNullOrWhiteSpace())
            {
                writer.WriteStringValue(Clock.Normalize(value));
            }
            else
            {
                writer.WriteStringValue(Clock.Normalize(value)
                    .ToString(OutputDateTimeFormat, CultureInfo.CurrentUICulture));
            }
        }
    }
    
    public class MyAbpDateTimeJsonTypeInfoResolver : DefaultJsonTypeInfoResolver
    {
        public MyAbpDateTimeJsonTypeInfoResolver(List<string> inputDateTimeFormats = null,
            string outputDateTimeFormat = null)
        {
            Modifiers.Add(
                new MyAbpDateTimeConverterModifier(inputDateTimeFormats, outputDateTimeFormat).CreateModifyAction());
        }
    }
    
    public class MyAbpDateTimeConverterModifier
    {
        private readonly List<string> _inputDateTimeFormats;
        private readonly string _outputDateTimeFormat;
    
        public MyAbpDateTimeConverterModifier(List<string> inputDateTimeFormats, string outputDateTimeFormat)
        {
            _inputDateTimeFormats = inputDateTimeFormats;
            _outputDateTimeFormat = outputDateTimeFormat;
        }
    
        public Action<JsonTypeInfo> CreateModifyAction()
        {
            return Modify;
        }
    
        private void Modify(JsonTypeInfo jsonTypeInfo)
        {
            foreach (var property in jsonTypeInfo.Properties.Where(x =>
                         x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?)))
            {
                if (property.AttributeProvider == null ||
                    !property.AttributeProvider.GetCustomAttributes(typeof(DisableDateTimeNormalizationAttribute), false)
                        .Any())
                {
                    property.CustomConverter = property.PropertyType == typeof(DateTime)
                        ? (JsonConverter) new MyDateTimeConverter(_inputDateTimeFormats, _outputDateTimeFormat)
                        : new MyNullableDateTimeConverter(_inputDateTimeFormats, _outputDateTimeFormat);
                }
            }
        }
    }
    
    public class MyNullableDateTimeConverter : JsonConverter<DateTime?>, ITransientDependency
    {
        protected List<string> InputDateTimeFormats { get; set; }
        protected string OutputDateTimeFormat { get; set; }
    
        public MyNullableDateTimeConverter(List<string> inputDateTimeFormats = null, string outputDateTimeFormat = null)
        {
            InputDateTimeFormats = inputDateTimeFormats ?? new List<string>();
            OutputDateTimeFormat = outputDateTimeFormat;
        }
    
        public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (!InputDateTimeFormats.IsNullOrEmpty())
            {
                if (reader.TokenType == JsonTokenType.String)
                {
                    var s = reader.GetString();
                    if (s.IsNullOrEmpty())
                    {
                        return null;
                    }
    
                    foreach (var format in InputDateTimeFormats)
                    {
                        if (DateTime.TryParseExact(s, format, CultureInfo.CurrentUICulture, DateTimeStyles.None,
                                out var outDateTime))
                        {
                            return Clock.Normalize(outDateTime);
                        }
                    }
                }
                else
                {
                    throw new JsonException("Reader's TokenType is not String!");
                }
            }
    
            var dateText = reader.GetString();
            if (dateText.IsNullOrEmpty())
            {
                return null;
            }
    
            if (DateTime.TryParse(dateText, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var date))
            {
                return Clock.Normalize(date);
            }
    
            return null;
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
        {
            if (value == null)
            {
                writer.WriteNullValue();
            }
            else
            {
                if (OutputDateTimeFormat.IsNullOrWhiteSpace())
                {
                    writer.WriteStringValue(Clock.Normalize(value.Value));
                }
                else
                {
                    writer.WriteStringValue(Clock.Normalize(value.Value)
                        .ToString(OutputDateTimeFormat, CultureInfo.CurrentUICulture));
                }
            }
        }
    }
    
  • User Avatar
    0
    gekko created

    Hi, I did all the suggested modifications, but actually it does not change anything.

    I tried to set a couple of breakpoints in the classes I added, but they are never hit.? The only breakpoint that hits is the initialization of the MyAbpDateTimeJsonTypeInfoResolver in Startup. In Startup.cs, the values for aspNetCoreConfiguration.InputDateTimeFormats and aspNetCoreConfiguration.OutputDateTimeFormat are both null at the moment it gets called. Is that expected?

    Basically MyAbpDateTimeJsonTypeInfoResolver is the same as AbpDateTimeJsonTypeInfoResolver from aspnetboilerplate (and the other classes are almost same). So what should change with this implementation?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @gekko

    Yes, it is same but if you you can debug it, you can change the implementation. Let us try again and get back to you.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @gekko

    You were right. Please first change Startup.cs as shown below;

    services.AddOptions<Microsoft.AspNetCore.Mvc.JsonOptions>()
    		.PostConfigure<IServiceProvider>((options, rootServiceProvider) =>
    		{
    			var aspNetCoreConfiguration = rootServiceProvider.GetRequiredService<IAbpAspNetCoreConfiguration>();
    			options.JsonSerializerOptions.TypeInfoResolver = new MyAbpDateTimeJsonTypeInfoResolver(aspNetCoreConfiguration.InputDateTimeFormats, aspNetCoreConfiguration.OutputDateTimeFormat);                    
    		});
    

    Then, I noticed that Newtonsoft uses DateTimeStyles.RoundtripKind but we are using DateTimeStyles.None (I'm not sure why, I will investigate this).

    Just change all DateTimeStyles.None to DateTimeStyles.RoundtripKind in MyDateTimeConverter. Then, it should work.

  • User Avatar
    0
    gekko created

    Hi @ismcagdas

    sorry for the late reply.

    I did now test it, and with the changes to DateTimeStyles.RoundtripKind in MyDateTimeConverter and MyNullableDateTimeConverter it works for me 👍.

    Thank you