protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, JsonSerializer serializer, RelationAggregator aggregator) { /* This is a bit messy, because we may add items of a given type to the * set we are currently processing. Not only is this an issue because you * can't modify a set while you're enumerating it (hence why we make a * copy first), but we need to catch the newly added objects and process * them as well. So, we have to keep making passes until we detect that * we haven't added any new objects to any of the appendices. */ Dictionary <Type, ISet <object> > processed = new Dictionary <Type, ISet <object> >(), toBeProcessed = new Dictionary <Type, ISet <object> >(); // is this actually necessary? /* On top of that, we need a new JsonWriter for each appendix--because we * may write objects of type A, then while processing type B find that * we need to write more objects of type A! So we can't keep appending * to the same writer. */ /* Oh, and we have to keep a reference to the TextWriter of the JsonWriter * because there's no member to get it back out again. ?!? * */ Dictionary <Type, KeyValuePair <JsonWriter, StringWriter> > writers = new Dictionary <Type, KeyValuePair <JsonWriter, StringWriter> >(); int numAdditions; do { numAdditions = 0; Dictionary <Type, ISet <object> > appxs = new Dictionary <Type, ISet <object> >(aggregator.Appendices); // shallow clone, in case we add a new type during enumeration! foreach (KeyValuePair <Type, ISet <object> > apair in appxs) { Type type = apair.Key; ISet <object> appendix = apair.Value; JsonWriter jw; if (writers.ContainsKey(type)) { jw = writers[type].Key; } else { // Setup and start the writer for this type... StringWriter sw = new StringWriter(); jw = new JsonTextWriter(sw); writers[type] = new KeyValuePair <JsonWriter, StringWriter>(jw, sw); jw.WriteStartArray(); } HashSet <object> tbp; if (processed.ContainsKey(type)) { toBeProcessed[type] = tbp = new HashSet <object>(appendix.Except(processed[type])); } else { toBeProcessed[type] = tbp = new HashSet <object>(appendix); processed[type] = new HashSet <object>(); } if (tbp.Count > 0) { numAdditions += tbp.Count; foreach (object obj in tbp) { Serialize(obj, writeStream, jw, serializer, aggregator); // Note, not writer, but jw--we write each type to its own JsonWriter and combine them later. } processed[type].UnionWith(tbp); } //TODO: Add traversal depth limit?? } } while (numAdditions > 0); if (aggregator.Appendices.Count > 0) { writer.WritePropertyName("linked"); writer.WriteStartObject(); // Okay, we should have captured everything now. Now combine the type writers into the main writer... foreach (KeyValuePair <Type, KeyValuePair <JsonWriter, StringWriter> > apair in writers) { apair.Value.Key.WriteEnd(); // close off the array writer.WritePropertyName(_modelManager.GetJsonKeyForType(apair.Key)); writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter } writer.WriteEndObject(); } }
protected void SerializeMany(object value, Stream writeStream, JsonWriter writer, JsonSerializer serializer, RelationAggregator aggregator) { writer.WriteStartArray(); foreach (object singleVal in (IEnumerable)value) { this.Serialize(singleVal, writeStream, writer, serializer, aggregator); } writer.WriteEndArray(); }
protected void Serialize(object value, Stream writeStream, JsonWriter writer, JsonSerializer serializer, RelationAggregator aggregator) { writer.WriteStartObject(); // 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) { if (prop == idProp) { continue; } if (this.CanWriteTypeAsPrimitive(prop.PropertyType)) { if (prop.GetCustomAttributes().Any(attr => attr is JsonIgnoreAttribute)) { continue; } // numbers, strings, dates... writer.WritePropertyName(_modelManager.GetJsonKeyForProperty(prop)); var propertyValue = prop.GetValue(value, null); if (prop.PropertyType == typeof(string) && prop.GetCustomAttributes().Any(attr => attr is SerializeStringAsRawJsonAttribute)) { if (propertyValue == null) { writer.WriteNull(); } else { var minifiedValue = JsonHelpers.MinifyJson((string)propertyValue); writer.WriteRawValue(minifiedValue); } } 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(); }
public override Task WriteToStreamAsync(System.Type type, object value, Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext) { RelationAggregator aggregator; lock (this.RelationAggregators) { if (this.RelationAggregators.ContainsKey(writeStream)) { aggregator = this.RelationAggregators[writeStream]; } else { aggregator = new RelationAggregator(); this.RelationAggregators[writeStream] = aggregator; } } var contentHeaders = content == null ? null : content.Headers; var effectiveEncoding = SelectCharacterEncoding(contentHeaders); JsonWriter writer = this.CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); JsonSerializer serializer = this.CreateJsonSerializer(); if (_errorSerializer.CanSerialize(type)) { // `value` is an error _errorSerializer.SerializeError(value, writeStream, writer, serializer); } else { Type valtype = GetSingleType(value.GetType()); if (_modelManager.IsSerializedAsMany(value.GetType())) { aggregator.AddPrimary(valtype, (IEnumerable <object>)value); } else { aggregator.AddPrimary(valtype, value); } //writer.Formatting = Formatting.Indented; var root = _modelManager.GetJsonKeyForType(type); writer.WriteStartObject(); writer.WritePropertyName(root); if (_modelManager.IsSerializedAsMany(value.GetType())) { this.SerializeMany(value, writeStream, writer, serializer, aggregator); } else { this.Serialize(value, writeStream, writer, serializer, aggregator); } // Include links from aggregator SerializeLinkedResources(writeStream, writer, serializer, aggregator); writer.WriteEndObject(); } writer.Flush(); lock (this.RelationAggregators) { this.RelationAggregators.Remove(writeStream); } //return base.WriteToStreamAsync(type, obj as object, writeStream, content, transportContext); //TODO: For now we won't worry about optimizing this down into smaller async parts, we'll just do it all synchronous. So... // Just return a completed task... from http://stackoverflow.com/a/17527551/489116 var tcs = new TaskCompletionSource <object>(); tcs.SetResult(null); return(tcs.Task); }