Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateTime deserialization handling #61

Open
Prinsn opened this issue Oct 13, 2022 · 7 comments
Open

DateTime deserialization handling #61

Prinsn opened this issue Oct 13, 2022 · 7 comments

Comments

@Prinsn
Copy link

Prinsn commented Oct 13, 2022

I'm currently trying to use Newtonsoft JSON converters to intercept incoming data to ensure UTC for DateTime, as this gets lost on DB commit.

Currently, I can get WriteJson to be called as expected for DateTime, but not ReadJson where I expect to intercept it, so I at least know it's loaded but not sure how to handle this.

Trying to figure out if there's a standard way or feature that Breeze conducts serialization that is causing the read to never happen when deserializing objects, or if there's otherwise a way to ensure that the time gets serialized to UTC.

While this can be approached from the front end to try to pre-convert, I'm trying to just handle the problem rather than requiring the client be required to create correctly formatted data (because various frameworks are not doing it and having to figure them all out is just more of what I'm doing here)

@Prinsn
Copy link
Author

Prinsn commented Oct 13, 2022

Related stack I'm looking at its


   at Newtonsoft.Json.Serialization.ExpressionValueProvider.SetValue(Object target, Object value)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Breeze.Persistence.PersistenceManager.CreateEntityInfoFromJson(Object jo, Type entityType)
   at Breeze.Persistence.SaveWorkState.<>c__DisplayClass0_0.<.ctor>b__3(Object jo)
   at System.Linq.Enumerable.SelectIListIterator`2.MoveNext()
   at System.Linq.Enumerable.CastIterator[TResult](IEnumerable source)+MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Breeze.Persistence.SaveWorkState.<.ctor>b__0_2(IGrouping`2 g)
   at System.Linq.Enumerable.SelectListIterator`2.ToList()
   at Breeze.Persistence.SaveWorkState..ctor(PersistenceManager persistenceManager, JArray entitiesArray)
   at Breeze.Persistence.PersistenceManager.InitializeSaveState(JObject saveBundle)
   at Breeze.Persistence.PersistenceManager.SaveChanges(JObject saveBundle, TransactionSettings transactionSettings)

As to when the DateTime foo {set;} is called

@Prinsn
Copy link
Author

Prinsn commented Oct 13, 2022

I have also tried

public class DateTimeExpressionProvider : ExpressionValueProvider
    {
        public DateTimeExpressionProvider(MemberInfo memberInfo) : base(memberInfo)
        {
        }

        public new void SetValue(object target, object value)
        {
            var dv = (DateTime)value;
            base.SetValue(target, dv);
        }
    }

    public class SpecialContractResolver : DefaultContractResolver
    {
        protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
        {
            if (member.MemberType == MemberTypes.Property)
            {
                var pi = (PropertyInfo)member;
                if (typeof(DateTime).IsAssignableFrom(pi.PropertyType))
                {
                    return new DateTimeExpressionProvider(member);
                }
            }

            return base.CreateMemberValueProvider(member);
        }
    }

But this also never gets called for data provided from BreezeJS, only on serialization of data from BreezeSharp

@marcelgood
Copy link
Contributor

DateTime handling can be done via NewtonSoft configuration, and critically making sure EF sets DateTime.Kind, which it doesn't by default.

NewtonSoft configuration:

            services.AddControllers().AddNewtonsoftJson(opt =>
            {
                // Set Breeze defaults for entity serialization
                var ss = JsonSerializationFns.UpdateWithDefaults(opt.SerializerSettings);
                ss.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
                ...
            });

EF configuration. Call the following with the ModelBuilder in your DBContext.

    public static class EFDateHandling
    {
        public static void ApplyValueConverters(ModelBuilder modelBuilder)
        {
            // Set DateTime.Kind to DateTimeKind.Utc
            var dateTimeConverter = new ValueConverter<DateTime, DateTime>(
                v => v.ToUniversalTime(),
                v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

            var nullableDateTimeConverter = new ValueConverter<DateTime?, DateTime?>(
                v => v.HasValue ? v.Value.ToUniversalTime() : v,
                v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                foreach (var property in entityType.GetProperties())
                {
                    if (property.ClrType == typeof(DateTime))
                    {
                        property.SetValueConverter(dateTimeConverter);
                    }
                    else if (property.ClrType == typeof(DateTime?))
                    {
                        property.SetValueConverter(nullableDateTimeConverter);
                    }
                }
            }
        }
    }

This should ensure that all inbound and outbound DateTime are correctly handled as UTC.

@Prinsn
Copy link
Author

Prinsn commented Oct 13, 2022

Grazi, you're a saint.

Hope it's this simple

Outbound seems to be fine, it doesn't set Kind but it serializes outbound correctly with no issue (BreezeJS receives the value as UTC)

@marcelgood
Copy link
Contributor

marcelgood commented Oct 14, 2022

With BreezeJS, you have the additional challenge that the browsers always convert dates to local time, so you have to handle that and shift the offset, so that the dates display consistently as UTC. You can do that by overriding the date parsing and serialization in BreezeJS.

    // change Breeze date parsing and serialization to UTC
    breeze.DataType.parseDateFromServer = (source: any) => {
       const date = new Date(source);
       return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
    };
    this.metadataStore.setProperties({
      serializerFn: (dp: DataProperty, val) => {
        if (dp.isDataProperty && val && dp.dataType === DataType.DateTime) {
          return new Date(val.getTime() - val.getTimezoneOffset() * 60000);
        }

        return val;
      },
    });

Keep in mind, this only works for entitles. If you return projections from the server, then you have to parse/map dates in the JSON as Breeze will just pass them through to the caller as-is.

@Prinsn
Copy link
Author

Prinsn commented Oct 14, 2022

The current plan was to leave js Dates as is and normalize them on display, but I'll definitely play around with this to see if it opens up some freedom.

@Prinsn Prinsn closed this as completed Oct 14, 2022
@Prinsn Prinsn reopened this Oct 27, 2022
@Prinsn Prinsn closed this as completed Oct 27, 2022
@Prinsn Prinsn reopened this Oct 27, 2022
@Prinsn Prinsn closed this as completed Oct 27, 2022
@Prinsn Prinsn reopened this Oct 27, 2022
@Prinsn Prinsn closed this as completed Oct 27, 2022
@Prinsn Prinsn reopened this Oct 27, 2022
@Prinsn
Copy link
Author

Prinsn commented Oct 27, 2022

Okay, so, as observable above, I've been iterating through this and making progress on questions I would have asked, but I'm stumped.

We use both DateTime and DateTimeOffset in our system and Offset doesn't seem to be handled.

I tried updating the snippet above, which appears to work, but it doesn't seem to make any difference, and I can't seem to make any headway.

        this.metadataStore.setProperties({
            serializerFn: (dp: breeze.DataProperty, val) => {
                if (dp.isDataProperty && val) {
                    console.log(dp.nameOnServer);
                    console.log(dp.dataType)
                    if (dp.dataType === breeze.DataType.DateTime
                        || dp.dataType === breeze.DataType.DateTimeOffset)
                    {
                        console.log(val);
                        return new Date(val.getTime() - val.getTimezoneOffset() * 60000);
                    }
                }   

                return val;
            },
        });

None of these attempts to log output anything, so I cannot seem to interrogate what's going on to make any kind of headway without intervention.

2022-10-27T17:51:58.36+00:00
becomes
Thu Oct 27 2022 17:51:58 GMT-0400 (Eastern Daylight Time)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants