开发者

How To Set DateTime.Kind for all DateTime Properties on an Object using Reflection

开发者 https://www.devze.com 2023-02-16 01:34 出处:网络
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

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开发者_如何学Go 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?


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);
                }
            }


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);
                }
            }
        }


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消