2010-10-26 10 views
48

Tengo problemas con un servicio WCF REST. El objeto de conexión que trato de devolver tiene ciertas propiedades no establecidas, lo que da como resultado DateTime.MinValue para las propiedades de tipo DateTime. El servicio devuelve un documento vacío (con el estado HTTP 200 ???). Cuando intento llamar la serialización JSON a mí mismo, la excepción que se produce es:¿Por qué DateTime.MinValue no se serializa en zonas horarias antes de UTC?

SerializationException: valores DateTime que son mayores que DateTime.MaxValue o menor que DateTime.MinValue cuando se convierte a UTC no puede ser serializado a JSON.

Esto puede ser reproducido mediante la ejecución del siguiente código en una aplicación de consola:

DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(DateTime)); 
MemoryStream m = new MemoryStream(); 
DateTime dt = DateTime.MinValue; 

// throws SerializationException in my timezone 
ser.WriteObject(m, dt); 
string json = Encoding.ASCII.GetString(m.GetBuffer()); 
Console.WriteLine(json); 

¿Por qué es este comportamiento? Creo que está relacionado con mi zona horaria (GMT + 1). Como DateTime.MinValue es predeterminado (DateTime), esperaría que esto se pueda serializar sin problemas.

¿Algún consejo sobre cómo hacer que mi servicio REST se comporte? No quiero cambiar mi DataContract.

+0

¿Puede hacer que su DateTime se pueda anular y usar null como valor predeterminado? – Gabe

+0

@Gabe: creo que podría. Se siente estúpido al cambiar mi tipo para trabajar con un detalle de serialización. Pero es probablemente el camino más pragmático. –

Respuesta

61

El principal problema es DateTime.MinValue tiene DateTimeKind.Unspecified tipo. Se define como:

MinValue = new DateTime(0L, DateTimeKind.Unspecified); 

Pero esto no es un problema real, esta definición conduce a un problema durante la serialización. JSON DateTime serialización realiza a través de:

System.Runtime.Serialization.Json.JsonWriterDelegator.WriteDateTime(DateTime value) 

Por desgracia, se define como:

... 

if (value.Kind != DateTimeKind.Utc) 
{ 
    long num = value.Ticks - TimeZone.CurrentTimeZone.GetUtcOffset(value).Ticks; 
    if ((num > DateTime.MaxValue.Ticks) || (num < DateTime.MinValue.Ticks)) 
    { 
     throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(XmlObjectSerializer.CreateSerializationException(SR.GetString("JsonDateTimeOutOfRange"), new ArgumentOutOfRangeException("value"))); 
    } 
} 

... 

Por lo tanto, no tiene en cuenta Unspecified y lo trata como Local. Para evitar esta situación, puede definir su propio constante:

MinValueUtc = new DateTime(0L, DateTimeKind.Utc); 

o

MinValueUtc = DateTime.MinValue.ToUniversalTime(); 

Se ve raro, por supuesto, pero ayuda.

+0

Esa es una buena explicación. Esto también explica por qué la serialización XML funciona y JSON no. Mi problema es que MinValue está ahí porque no está configurado. En lugar de agregar un atributo simple, ahora tendré que configurar este MinValueUtc personalizado para todas las propiedades de fecha y hora. –

+0

Puede considerar trabajar (o simplemente almacenar) con las fechas siempre en UTC convirtiéndolas a LocalTime justo antes de que deba mostrarlas. Por lo tanto, después de la inicialización, ya sea en el constructor o simplemente en el formador adecuado, convierta el valor de la fecha y hora 'entrante' en UTC mediante .ToUniversalTime(). Ayuda a resolver el problema de valor predeterminado. –

+0

esto no funcionó para mí, estableciendo el valor en el nuevo DateTime (0L, DateTimeKind.Utc); también bloquea la serialización – Matus

6

Si su zona horaria es GMT + 1, entonces el valor UTC de DateTime.MinValue en su zona horaria será de una hora menos que DateTime.MinValue.

+7

Sí, pensé que sí. ¿Pero qué hacer? ¿No es raro que el valor predeterminado de una clase muy común del marco no se pueda serializar en la mitad del mundo? –

5

usar este constructor:

public DataContractJsonSerializer(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, IDataContractSurrogate dataContractSurrogate, bool alwaysEmitTypeInformation) 

ejemplo de código:

DataContractJsonSerializer serializer = new DataContractJsonSerializer(o.GetType(), null, int.MaxValue, false, new DateTimeSurrogate(), false); 

public class DateTimeSurrogate : IDataContractSurrogate 
    { 

     #region IDataContractSurrogate 成员 

     public object GetCustomDataToExport(Type clrType, Type dataContractType) 
     { 
      return null; 
     } 

     public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType) 
     { 
      return null; 
     } 

     public Type GetDataContractType(Type type) 
     { 
      return type; 
     } 

     public object GetDeserializedObject(object obj, Type targetType) 
     { 
        return obj; 
     } 

     public void GetKnownCustomDataTypes(System.Collections.ObjectModel.Collection<Type> customDataTypes) 
     { 

     } 

     public object GetObjectToSerialize(object obj, Type targetType) 
     { 
      if (obj.GetType() == typeof(DateTime)) 
      { 
       DateTime dt = (DateTime)obj; 
       if (dt == DateTime.MinValue) 
       { 
        dt = DateTime.MinValue.ToUniversalTime(); 
        return dt; 
       } 
       return dt; 
      } 
      if (obj == null) 
      { 
       return null; 
      } 
      var q = from p in obj.GetType().GetProperties() 
        where (p.PropertyType == typeof(DateTime)) && (DateTime)p.GetValue(obj, null) == DateTime.MinValue 
        select p; 
      q.ToList().ForEach(p => 
      { 
       p.SetValue(obj, DateTime.MinValue.ToUniversalTime(), null); 
      }); 
      return obj; 
     } 

     public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData) 
     { 
      return null; 
     } 

     public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit) 
     { 
      return typeDeclaration; 
     } 

     #endregion 
    } 
+0

Entonces, ¿dónde usaría este constructor? Actualmente solo agrego WebMessageFormat.Json al atributo WebInvoke. ¿Alguna pista sobre cómo combinar esta técnica en el estilo declaritivo? –

13

intenta agregar esto en cualquier miembro DateTime

[DataMember(IsRequired = false, EmitDefaultValue = false)] 

La mayoría de estos erros sucede debido a que el valor por defecto de el datetime es DateTime.MinValue que es del año de 1 y el JSON se La rialización es del año 1970.

0

Se podría arreglar eso durante la serialización a través del atributo OnSerializing y una reflexión:

[OnSerializing] 
public void OnSerializing(StreamingContext context) 
{ 
    var properties = this.GetType().GetProperties(); 
    foreach (PropertyInfo property in properties) 
    { 
    if (property.PropertyType == typeof(DateTime) && property.GetValue(this).Equals(DateTime.MinValue)) 
    { 
     property.SetValue(this, DateTime.MinValue.ToUniversalTime()); 
    } 
    } 
} 
2

creo una forma más elegante es dar instrucciones al serializador no para emitir el valor por defecto para los campos de fecha y hora. Esto ahorrará un byte durante la transferencia y algo de procesamiento al serializar para los campos que no tiene ningún valor para ellos. Ejemplo:

[DataContract] 
public class Document { 
    [DataMember] 
    public string Title { get; set; } 
    [DataMember(IsRequired = false, EmitDefaultValue = false)] 
    public DateTime Modified { get; set; } 
} 

o puede utilizar nullables. Ejemplo:

[DataContract] 
public class Document { 
    [DataMember] 
    public string Title { get; set; } 
    [DataMember] 
    public DateTime? Modified { get; set; } 
} 

Todo depende de los requisitos y restricciones que pueda tener en su proyecto. A veces no puedes simplemente cambiar los tipos de datos. En ese caso, puede aprovechar el atributo DataMember y mantener intactos los tipos de datos.

En el ejemplo anterior si tiene new Document() { Title = "Test Document" } en el lado del servidor, cuando se serializa a JSON le dará {"Title": "Test Document"}, por lo que será más fácil tratar con JavaScript o cualquier otro cliente en el otro lado del cable. En JavaScript si usa JSON.Parse() y trata de leerlo, obtendrá undefined. En los idiomas escritos, tendrá el valor predeterminado para esa propiedad según el tipo (que generalmente es el comportamiento esperado).

library.GetDocument(id).success(function(raw){ 
    var document = JSON.Parse(raw); 
    var date = document.date; // date will be *undefined* 
    ... 
} 
+0

Gracias. EmitDefaultValue funcionó para mí. – Johan

+0

de nada.Mejoré la respuesta para hablar sobre el lado del cliente también. Alguien podría encontrarlo útil algún día;) – Reza

Cuestiones relacionadas