public object Deserialize(Type objectType, Stream readStream, JsonReader reader, JsonSerializer serializer) { object retval = Activator.CreateInstance(objectType); if (reader.TokenType != JsonToken.StartObject) { throw new JsonReaderException(String.Format("Expected JsonToken.StartObject, got {0}", reader.TokenType.ToString())); } reader.Read(); // Burn the StartObject token do { if (reader.TokenType == JsonToken.PropertyName) { string value = (string)reader.Value; PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); // If the model object has a non-standard Id property, but the "id" key is being used... if (prop == null && value == "id") { prop = _modelManager.GetIdProperty(objectType); } if (value == "links") { reader.Read(); // burn the PropertyName token //TODO: linked resources (Done??) DeserializeLinkedResources(retval, readStream, reader, serializer); } else if (prop != null) { if (!prop.PropertyType.CanWriteAsJsonApiAttribute()) { reader.Read(); // burn the PropertyName token //TODO: Embedded would be dropped here! continue; // These aren't supposed to be here, they're supposed to be in "links"! } object propVal; Type enumType; if (prop.PropertyType == typeof(string) && prop.GetCustomAttributes().Any(attr => attr is SerializeStringAsRawJsonAttribute)) { reader.Read(); if (reader.TokenType == JsonToken.Null) { propVal = null; } else { var token = JToken.Load(reader); var rawPropVal = token.ToString(); propVal = JsonHelpers.MinifyJson(rawPropVal); } } else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable <>) && (enumType = prop.PropertyType.GetGenericArguments()[0]).IsEnum) { // Nullable enums need special handling reader.Read(); propVal = reader.TokenType == JsonToken.Null ? null : Enum.Parse(enumType, reader.Value.ToString()); } else if (prop.PropertyType == typeof(DateTimeOffset) || prop.PropertyType == typeof(DateTimeOffset?)) { // For some reason reader.ReadAsString(); propVal = reader.TokenType == JsonToken.Null ? (object)null : DateTimeOffset.Parse(reader.Value.ToString()); } else { reader.Read(); propVal = DeserializeAttribute(prop.PropertyType, reader); } prop.SetValue(retval, propVal, null); // Tell the MetadataManager that we deserialized this property MetadataManager.Instance.SetMetaForProperty(retval, prop, true); // pop the value off the reader, so we catch the EndObject token below!. reader.Read(); } else { // Unexpected/unknown property--Skip the propertyname and its value reader.Skip(); if (reader.TokenType == JsonToken.StartArray || reader.TokenType == JsonToken.StartObject) { reader.Skip(); } else { reader.Read(); } } } } while (reader.TokenType != JsonToken.EndObject); reader.Read(); // burn the EndObject token before returning back up the call stack /* * // Suss out all the relationship members, and which ones have what cardinality... * IEnumerable<PropertyInfo> relations = ( * from prop in objectType.GetProperties() * where !CanWriteTypeAsJsonApiAttribute(prop.PropertyType) * && prop.GetCustomAttributes(true).Any(attribute => attribute is System.Runtime.Serialization.DataMemberAttribute) * select prop * ); * IEnumerable<PropertyInfo> hasManys = relations.Where(prop => typeof(IEnumerable<object>).IsAssignableFrom(prop.PropertyType)); * IEnumerable<PropertyInfo> belongsTos = relations.Where(prop => !typeof(IEnumerable<object>).IsAssignableFrom(prop.PropertyType)); * * JObject links = (JObject)jo["links"]; * * // Lets deal with belongsTos first, that should be simpler... * foreach (PropertyInfo prop in belongsTos) * { * if (links == null) break; // Well, apparently we don't have any data for the relationships! * * string btId = (string)links[_modelManager.GetJsonKeyForProperty(prop)]; * if (btId == null) * { * prop.SetValue(retval, null, null); // Important that we set--the value may have been cleared! * continue; // breaking early! * } * Type relType = prop.PropertyType; * //if (typeof(EntityObject).IsAssignableFrom(relType)) * if (resolver.CanIncludeTypeAsObject(relType)) * { * prop.SetValue(retval, resolver.GetById(relType, btId), null); * //throw new ApplicationException(String.Format("Could not assign BelongsTo property \"{0}\" on object of type {1} by ID {2} because no object of type {3} could be retrieved by that ID.", prop.Name, objectType, btId, prop.PropertyType)); * } * } */ return(retval); }
protected void Serialize(object value, Stream writeStream, JsonWriter writer, JsonSerializer serializer, RelationAggregator aggregator) { writer.WriteStartObject(); // The spec no longer requires that the ID key be "id": // "An ID SHOULD be represented by an 'id' key..." :-/ // But Ember Data does. So, we'll add "id" to the document // always, and also serialize the property under its given // name, for now at least. //TODO: Partly because of this, we should probably disallow updates to Id properties where practical. // Do the Id now... writer.WritePropertyName("id"); var idProp = _modelManager.GetIdProperty(value.GetType()); writer.WriteValue(GetValueForIdProperty(idProp, value)); // Leverage the cached map to avoid another costly call to System.Type.GetProperties() PropertyInfo[] props = _modelManager.GetProperties(value.GetType()); // Do non-model properties first, everything else goes in "links" //TODO: Unless embedded??? IList <PropertyInfo> modelProps = new List <PropertyInfo>(); foreach (PropertyInfo prop in props) { string propKey = _modelManager.GetJsonKeyForProperty(prop); if (propKey == "id") { continue; // Don't write the "id" property twice, see above! } if (prop.PropertyType.CanWriteAsJsonApiAttribute()) { if (prop.GetCustomAttributes().Any(attr => attr is JsonIgnoreAttribute)) { continue; } // numbers, strings, dates... writer.WritePropertyName(propKey); var propertyValue = prop.GetValue(value, null); if (prop.PropertyType == typeof(Decimal) || prop.PropertyType == typeof(Decimal?)) { if (propertyValue == null) { writer.WriteNull(); } else { writer.WriteValue(propertyValue.ToString()); } } else if (prop.PropertyType == typeof(string) && prop.GetCustomAttributes().Any(attr => attr is SerializeStringAsRawJsonAttribute)) { if (propertyValue == null) { writer.WriteNull(); } else { var json = (string)propertyValue; if (ValidateRawJsonStrings) { try { var token = JToken.Parse(json); json = token.ToString(); } catch (Exception) { json = "{}"; } } var valueToSerialize = JsonHelpers.MinifyJson(json); writer.WriteRawValue(valueToSerialize); } } else { serializer.Serialize(writer, propertyValue); } } else { modelProps.Add(prop); continue; } } // Now do other stuff if (modelProps.Count() > 0) { writer.WritePropertyName("links"); writer.WriteStartObject(); } foreach (PropertyInfo prop in modelProps) { bool skip = false, iip = false; string lt = null; SerializeAsOptions sa = SerializeAsOptions.Ids; object[] attrs = prop.GetCustomAttributes(true); foreach (object attr in attrs) { Type attrType = attr.GetType(); if (typeof(JsonIgnoreAttribute).IsAssignableFrom(attrType)) { skip = true; continue; } if (typeof(IncludeInPayload).IsAssignableFrom(attrType)) { iip = ((IncludeInPayload)attr).Include; } if (typeof(SerializeAs).IsAssignableFrom(attrType)) { sa = ((SerializeAs)attr).How; } if (typeof(LinkTemplate).IsAssignableFrom(attrType)) { lt = ((LinkTemplate)attr).TemplateString; } } if (skip) { continue; } writer.WritePropertyName(_modelManager.GetJsonKeyForProperty(prop)); // Look out! If we want to SerializeAs a link, computing the property is probably // expensive...so don't force it just to check for null early! if (sa != SerializeAsOptions.Link && prop.GetValue(value, null) == null) { writer.WriteNull(); continue; } // Now look for enumerable-ness: if (typeof(IEnumerable <Object>).IsAssignableFrom(prop.PropertyType)) { switch (sa) { case SerializeAsOptions.Ids: //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); IEnumerable <object> items = (IEnumerable <object>)prop.GetValue(value, null); if (items == null) { writer.WriteValue((IEnumerable <object>)null); //TODO: Is it okay with the spec and Ember Data to return null for an empty array? break; // LOOK OUT! Ending this case block early here!!! } this.WriteIdsArrayJson(writer, items, serializer); if (iip) { Type itemType; if (prop.PropertyType.IsGenericType) { itemType = prop.PropertyType.GetGenericArguments()[0]; } else { // Must be an array at this point, right?? itemType = prop.PropertyType.GetElementType(); } if (aggregator != null) { aggregator.Add(itemType, items); // should call the IEnumerable one...right? } } break; case SerializeAsOptions.Link: if (lt == null) { throw new JsonSerializationException("A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); } //TODO: Check for "{0}" in linkTemplate and (only) if it's there, get the Ids of all objects and "implode" them. string href = String.Format(lt, null, GetIdFor(value)); //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); writer.WritePropertyName("href"); writer.WriteValue(href); writer.WriteEndObject(); break; case SerializeAsOptions.Embedded: // Not really supported by Ember Data yet, incidentally...but easy to implement here. //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //serializer.Serialize(writer, prop.GetValue(value, null)); this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); break; } } else { var propertyValue = prop.GetValue(value, null); if (propertyValue == null) { writer.WriteNull(); } else { string objId = GetIdFor(propertyValue); switch (sa) { case SerializeAsOptions.Ids: //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); serializer.Serialize(writer, objId); if (iip) { if (aggregator != null) { aggregator.Add(prop.PropertyType, prop.GetValue(value, null)); } } break; case SerializeAsOptions.Link: if (lt == null) { throw new JsonSerializationException( "A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); } string link = String.Format(lt, objId, GetIdFor(value)); //value.GetType().GetProperty("Id").GetValue(value, null)); //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); writer.WriteStartObject(); writer.WritePropertyName("href"); writer.WriteValue(link); writer.WriteEndObject(); break; case SerializeAsOptions.Embedded: // Not really supported by Ember Data yet, incidentally...but easy to implement here. //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //serializer.Serialize(writer, prop.GetValue(value, null)); this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); break; } } } } if (modelProps.Count() > 0) { writer.WriteEndObject(); } writer.WriteEndObject(); }