protobuf-net / protobuf-net

Protocol Buffers library for idiomatic .NET
Other
4.68k stars 1.05k forks source link

[Question] DateTimeOffset? protobuf serialization/deserialization in ASP Core #226

Open odysseus1973 opened 7 years ago

odysseus1973 commented 7 years ago

Hi!

After some investigation and reading posts on SO I found two ways to serialize DateTimeOffset?

  1. Shim properties (working, but does not fit in my case)
  2. Surrogates. Not working in my case In my StartUp file I use

    RuntimeTypeModel.Default.Add(typeof(DateTimeOffset),false).SetSurrogate(typeof(DateTimeOffsetSurrogate));
    RuntimeTypeModel.Default.Add(typeof(DateTimeOffset?),false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

    My surrogate class

    [ProtoContract]
    public class DateTimeOffsetSurrogate
    {
        [ProtoMember(1)]
        public string Date { get; set; }
    
        //public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset? value)
        //{
        //    return (value.HasValue) ? new DateTimeOffsetSurrogate { Date = value.Value.ToString("o") } : new DateTimeOffsetSurrogate { Date = null };
        //}
    
        //public static implicit operator DateTimeOffset? (DateTimeOffsetSurrogate value)
        //{
        //    DateTimeOffset? result = null;
        //    if (DateTimeOffset.TryParse(value.Date, out DateTimeOffset date))
        //    {
        //        result = date;
        //    }
        //    return result;
        //}
    
        public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value)
        {
            return (value != null) ? new DateTimeOffsetSurrogate { Date = value.ToString("o") } : new DateTimeOffsetSurrogate { Date = null };
        }
    
        public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value)
        {
            // DateTimeOffset? result = null;
            // if (
            DateTimeOffset.TryParse(value.Date, out DateTimeOffset date);
            //{
            //    result = date;
            //}
            return date;
        }
    }

    My serialized class

    [ProtoContract]
    public class StocksDto
    {
        [JsonIgnore]
        public Guid? StoreId { get; set; }
        [Required]
        public DateTimeOffset? Date { get; set; }
        [JsonIgnore]
        public DateTime DateUtc { get { return Date.HasValue ? Date.Value.UtcDateTime : DateTime.MinValue; } }
        [JsonIgnore]
        public double TimeZoneOffset { get { return Date.HasValue ? Date.Value.Offset.TotalMinutes : 0; } }
        [ProtoMember(1)]
        [Required]
        public bool? Full { get; set; }
        [HasElements(ErrorMessage = "The field Stocks must contain non-empty array of elements")]
        [ProtoMember(2)]
        public IEnumerable<Stock> Stocks { get; set; }
    }

    When I try serialize/deserialize StocksDto, Date property return no value (null). As I can see implicit operator in surrogate does not called. How to use surrogate for DateTimeOffset? in my case?

rushfrisby commented 7 years ago

Your surrogate should be

using System;
using System.Runtime.Serialization;

namespace ProtobufNetTest
{
    [DataContract(Name = nameof(DateTimeOffset))]
    public class DateTimeOffsetSurrogate
    {
        [DataMember(Order = 1)]
        public long? Value { get; set; }

        public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate surrogate)
        {
            return DateTimeOffset.FromUnixTimeMilliseconds(surrogate.Value.Value);
        }

        public static implicit operator DateTimeOffset?(DateTimeOffsetSurrogate surrogate)
        {
            return surrogate != null ? DateTimeOffset.FromUnixTimeMilliseconds(surrogate.Value.Value) : (DateTimeOffset?)null;
        }

        public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset source)
        {
            return new DateTimeOffsetSurrogate
            {
                Value = source.ToUnixTimeMilliseconds()
            };
        }

        public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset? source)
        {
            return new DateTimeOffsetSurrogate
            {
                Value = source?.ToUnixTimeMilliseconds()
            };
        }
    }
}
odysseus1973 commented 7 years ago

Thanks! I will try it, but why surrogate can't use string and how to register surrogate correctly? When I was debugging I did not see that the surrogate was called, so i think my problem with surrogate registration.

rushfrisby commented 7 years ago

The way you registered surrogates is correct however your class has a DateTimeOffset? property and your surrogate has no operators for converting between the two. Your surrogate only has operators for DateTimeOffset which is a different type.

Sure you can serialize the value as a string. I used long and the unix time value because the output is smaller than the string representation and faster than converting to/from a string.

odysseus1973 commented 7 years ago

Thanks for answer! I specifically did not delete the commented lines in code block in my question, I tried it, but without success. How does converting to UnixTime support the time zone?

rushfrisby commented 7 years ago

By definition it takes into account the time zone since unix time starts at 1/1/1970 00:00:00 (UTC)

paraboxx commented 7 years ago

The purpose of DateTimeOffset is to preserve TimeZone information so it's possible to see at white Offset the timestamp was generated. By convertig to unixtime this information is lost.