28

In my application I retrieve domain objects via a web service. In the web service data, I know all the date values are UTC, but the web service does not format its xs:dateTime values as UTC dates. (In other words the letter Z is not appended to the end of each date to indicate UTC.)

I cannot change the way the web service behaves at this time, but as a workaround I have created a method which I call immediately after the objects from the web service have been deserialized.

    private void ExplicitlyMarkDateTimesAsUtc<T>(T obj) where T : class
    {
        Type t = obj.GetType();

        // Loop through the properties.
        PropertyInfo[] props = t.GetProperties();
        for (int i = 0; i < props.Length; i++)
        {
            PropertyInfo p = props[i];
            // If a property is DateTime or DateTime?, set DateTimeKind to DateTimeKind.Utc.
            if (p.PropertyType == typeof(DateTime))
            {
                DateTime date = (DateTime)p.GetValue(obj, null);
                date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
                p.SetValue(obj, date, null);
            }
            // Same check for nullable DateTime.
            else if (p.PropertyType == typeof(Nullable<DateTime>))
            {
                DateTime? date = (DateTime?)p.GetValue(obj, null);
                DateTime? newDate = DateTime.SpecifyKind(date.Value, DateTimeKind.Utc);
                p.SetValue(obj, newDate, null);
            }
        }
    }

The method takes an object and loops through its properties, finding the properties that are either DateTime or Nullable<DateTime>, and then (is supposed to) explicitly sets the DateTime.Kind property for each of the property values to DateTimeKind.Utc.

The code does not throw any exceptions, but obj never gets its DateTime properties changed. In the debugger p.SetValue(obj, date, null); is called, but obj never gets modified.

Why aren't changes getting applied to obj?

RunnerRick
  • 949
  • 2
  • 12
  • 21
  • 1
    How do you determine that obj is not updated? – Stefan Steinegger Mar 09 '11 at 22:34
  • @Stefan I can see it in the debugger. Its property values are not modified. – RunnerRick Mar 09 '11 at 22:35
  • Try changing the value not just the kind, perhaps adding up to see if it changes. Changing the kind should not change the value. – Aliostad Mar 09 '11 at 22:38
  • Do you expect that the time gets changed by the UTC offset? Can you see if the setter of the property in question is called using the debugger (put a breakpoint there)? – Stefan Steinegger Mar 09 '11 at 22:39
  • Added a "preamble" to give background as to why I'm doing this. – RunnerRick Mar 09 '11 at 23:09
  • 1
    @Stefan, @Aliostad I don't expect the value of the dates to change. I just want them marked as UTC dates so when I need to convert to other time zones, I can check the DateTime.Kind property and know that I'm dealing with either a `UTC` or `Local` date. – RunnerRick Mar 09 '11 at 23:11

4 Answers4

37

Works fine when I try it. Beware that you are only changing the Kind, not the time. And you don't handle null dates properly, you cannot use date.Value if date.HasValue is false. Make sure that the exception isn't caught silently and bypassing the rest of the property assignments. Fix:

            // Same check for nullable DateTime.
            else if (p.PropertyType == typeof(Nullable<DateTime>)) {
                DateTime? date = (DateTime?)p.GetValue(obj, null);
                if (date.HasValue) {
                    DateTime? newDate = DateTime.SpecifyKind(date.Value, DateTimeKind.Utc);
                    p.SetValue(obj, newDate, null);
                }
            }
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • That's a good check to add. It, however, doesn't solve my problem. (When I debugged my code it was inside the first `if` block. – RunnerRick Mar 09 '11 at 23:13
  • I'm 99% sure that the code is good. Mistrust the debugger if you have to. – Hans Passant Mar 09 '11 at 23:20
  • It looks good to me as well. It doesn't work though. When my objects are used in the UI, the time zone offset is wrong. When I preprocess the objects before using them in the UI, the time zone problem is fixed. So I could just preprocess the objects before using them in the UI, but I was trying to avoid repeating the same code all over the place. – RunnerRick Mar 09 '11 at 23:31
  • 1
    Erm, you never mentioned UI before. But yes, definitely fix them *before* displaying them. – Hans Passant Mar 09 '11 at 23:42
  • 1
    `SpecifyKind` was the solution to my problem =) – Nebula Aug 15 '16 at 09:32
2

The OP's code is very limited in that it doesn't iterate through child lists and objects looking for DateTime properties, it only looks at the top level object. Derreck Dean's code works, but is very verbose for the purpose it serves. Here is a more concise / working DateTime extension that can be leveraged to handle converting any DateTime or nullable DateTime property in the target object, including in its child lists and objects.

public static void ConvertDatesToUtc(this object obj) {
            foreach (var prop in obj.GetType().GetProperties().Where(p => p.CanWrite)) {
                var t = prop.PropertyType;
                if (t == typeof(DateTime)) {
                    //found datetime, specify its kind as utc.
                    var oldValue = (DateTime)prop.GetValue(obj, null);
                    var newValue = DateTime.SpecifyKind(oldValue, DateTimeKind.Utc);
                    prop.SetValue(obj, newValue, null);
                } else if (t == typeof(DateTime?)) {
                    //found nullable datetime, if populated specify its kind as utc.
                    var oldValue = (DateTime?)prop.GetValue(obj, null);
                    if (oldValue.HasValue) {
                        var newValue = (DateTime)DateTime.SpecifyKind(oldValue.Value, DateTimeKind.Utc);
                        prop.SetValue(obj, newValue, null);
                    }
                } else if (typeof(IEnumerable).IsAssignableFrom(t)) {
                    //traverse child lists recursively.
                    var vals = prop.GetValue(obj, null);
                    if (vals != null) {
                        foreach (object o in (IEnumerable)vals) {
                            ConvertDatesToUtc(o);
                        }
                    }
                } else {
                    //traverse child objects recursively.
                    var val = prop.GetValue(obj, null);
                    if (val != null)
                        ConvertDatesToUtc(val);
                }
            }
        }
Justin
  • 17,670
  • 38
  • 132
  • 201
1

See http://derreckdean.wordpress.com/2013/04/24/converting-all-datetime-properties-of-an-object-graph-to-local-time-from-utc/ for blog post. I use this code to convert a WCF response object graph to have all local times:

/// <summary>
/// Since all dates in the DB are stored as UTC, this converts dates to the local time using the Windows time zone settings.
/// </summary>
public static class AllDateTimesAsUTC
{

    /// <summary>
    /// Specifies that an object's dates are coming in as UTC.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static T AllDatesAreUTC<T>(this T obj)
    {
        if (obj == null)
        {
            return default(T);
        }
        IterateDateTimeProperties(obj);
        return obj;
    }

    private static void IterateDateTimeProperties(object obj)
    {
        if (obj == null)
        {
            return;
        }
        var properties = obj.GetType().GetProperties();
        //Set all DaetTimeKinds to Utc
        foreach (var prop in properties)
        {
            var t = prop.PropertyType;
            if (t == typeof(DateTime) || t == typeof(DateTime?))
            {
                SpecifyUtcKind(prop, obj);
            }
            else if (t.IsEnumerable())
            {
                var vals = prop.GetValue(obj, null);
                if (vals != null)
                {
                    foreach (object o in (IEnumerable)vals)
                    {
                        IterateDateTimeProperties(o);
                    }
                }
            }
            else
            {
                var val = prop.GetValue(obj, null);
                if (val != null)
                {
                    IterateDateTimeProperties(val);
                }
            }
        }
        //properties.ForEach(property => SpecifyUtcKind(property, obj));
        return; // obj;
    }

    private static void SpecifyUtcKind(PropertyInfo property, object value)
    {
        //Get the datetime value
        var datetime = property.GetValue(value, null);
        DateTime output;

        //set DateTimeKind to Utc
        if (property.PropertyType == typeof(DateTime))
        {
            output = DateTime.SpecifyKind((DateTime)datetime, DateTimeKind.Utc);
        }

        else if (property.PropertyType == typeof(DateTime?))
        {
            var nullable = (DateTime?)datetime;
            if (!nullable.HasValue) return;
            output = (DateTime)DateTime.SpecifyKind(nullable.Value, DateTimeKind.Utc);
        }
        else
        {
            return;
        }

        Debug.WriteLine("     ***** Converted date from {0} to {1}.", datetime, output);
        datetime = output.ToLocalTime();

        //And set the Utc DateTime value
        property.SetValue(value, datetime, null);
    }
    internal static Type[] ConvertibleTypes = {typeof(bool), typeof(byte), typeof(char),
typeof(DateTime), typeof(decimal), typeof(double), typeof(float), typeof(int), 
typeof(long), typeof(sbyte), typeof(short), typeof(string), typeof(uint), 
typeof(ulong), typeof(ushort)};

    /// <summary>
    /// Returns true if this Type matches any of a set of Types.
    /// </summary>
    /// <param name="types">The Types to compare this Type to.</param>
    public static bool In(this Type type, params Type[] types)
    {
        foreach (Type t in types) if (t.IsAssignableFrom(type)) return true; return false;
    }

    /// <summary>
    /// Returns true if this Type is one of the types accepted by Convert.ToString() 
    /// (other than object).
    /// </summary>
    public static bool IsConvertible(this Type t) { return t.In(ConvertibleTypes); }

    /// <summary>
    /// Gets whether this type is enumerable.
    /// </summary>
    public static bool IsEnumerable(this Type t)
    {
        return typeof(IEnumerable).IsAssignableFrom(t);
    }

    /// <summary>
    /// Returns true if this property's getter is public, has no arguments, and has no 
    /// generic type parameters.
    /// </summary>
    public static bool SimpleGetter(this PropertyInfo info)
    {
        MethodInfo method = info.GetGetMethod(false);
        return method != null && method.GetParameters().Length == 0 &&
             method.GetGenericArguments().Length == 0;
    }

}

(Some of the code came from other SO posts.)

To use: call .AllDatesAreUTC() from any object. It will walk the graph and do the local time conversions.

    void svc_ZOut_GetZOutSummaryCompleted(object sender, ZOut_GetZOutSummaryCompletedEventArgs e)
    {
        svc.ZOut_GetZOutSummaryCompleted -= new EventHandler<ZOut_GetZOutSummaryCompletedEventArgs>(svc_ZOut_GetZOutSummaryCompleted);
        svc = null;
        var whenDone = (Action<bool, ZOutResult>)e.UserState;
        if (e.Error != null)
        {
            FireOnExceptionRaised(e.Error);
            whenDone(false, null);
        }
        else
        {
            var res = e.Result.AllDatesAreUTC();
            FireOnSessionReceived(res.IsError, res.Session);
            if (res.IsError == true)
            {
                whenDone(false, null);
            }
            else
            {
                whenDone(true, res.Result);
            }
        }
    }

You can change the behavior to mark times as UTC without changing the time itself by modifying the SpecifyUtcKind method.

EDIT: I don't recommend using this on an object graph with circular references, as per the conversation in the comments.

Derreck Dean
  • 3,708
  • 1
  • 26
  • 45
  • This throws a stackoverflow exception. Probably because your recursing through the object graph, going through every property including strings and primitives. – The Muffin Man Mar 04 '14 at 17:05
  • How big is your object graph, and do you have objects that reference other objects that are in the same graph? This code isn't made to handle that; I only needed it to handle simple graphs that don't link back to each other, which would definitely cause the `StackOverflowException`. – Derreck Dean Mar 04 '14 at 17:55
  • Maybe that's the case.. I my graph has a property witch is a paged list and each item in that collection is an EF object with navigation properties. Cannot figure out for the life of me how do traverse the graph, ignore primitives (among other things) just to find the datetimes. – The Muffin Man Mar 04 '14 at 17:59
  • To give you an idea of my use case, this piece of code was used on the rich client app, and when I made a WCF call to get some data that had `DateTime` properties that needed to be turned into a local time, I ran the method `AllDatesAreUTC` on the returned object. Usually the object was fairly flat or had a couple of simple collections on it. Personally I would not recommend sending whole EF object graphs to the server myself, but I'm not assuming that's what you're doing. – Derreck Dean Mar 04 '14 at 21:22
0

I know this is well after the fact but hoping it might help someone. I was attempting to do the same exact thing as RickRunner in the original post and came up with very similar code. I ran into a similar problem, though for me obj.Kind was being set fine if the property was of regular DateTime type; however for nullable DateTime properties, the Kind was not being modified no matter what I did. In the end, I discovered that if I set the property to null and then back to a DateTime, it does reset the Kind properly:

// Same check for nullable DateTime.
else if (p.PropertyType == typeof(Nullable<DateTime>)) {
    DateTime? date = (DateTime?)p.GetValue(obj, null);
    if (date.HasValue) {
        DateTime? newDate = DateTime.SpecifyKind(date.Value, DateTimeKind.Utc);
        p.SetValue(obj, null, null);
        p.SetValue(obj, newDate, null);
    }
}

It is ugly and I did not dig in too deep to try and figure out why SetValue does not set the Kind properly in the first place. I spent quite a bit of time on this and was just glad to have arrived at a solution, however grody.

Matt Zamec
  • 73
  • 6