diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index 33f2412b..3c8df580 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -106,8 +106,7 @@ public void SetupEntities() public void SerializeTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); // Act @@ -124,8 +123,7 @@ public void SerializeTest() public void DeserializePostIntegrationTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); @@ -159,8 +157,7 @@ public void DeserializePostIntegrationTest() public void UnderpostingTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs index 562a7efc..e4a0d259 100644 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ b/JSONAPI.Tests/Core/MetadataManagerTests.cs @@ -15,8 +15,7 @@ public void PropertyWasPresentTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""posts"":{""id"":42,""links"":{""author"":""18""}}}")); diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 7d364777..5bbe7198 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -3,13 +3,15 @@ using JSONAPI.Core; using JSONAPI.Tests.Models; using System.Reflection; +using System.Collections.Generic; +using System.Collections; namespace JSONAPI.Tests.Core { [TestClass] public class ModelManagerTests { - private class InvalidModel + private class InvalidModel // No Id discernable! { public string Data { get; set; } } @@ -18,7 +20,7 @@ private class InvalidModel public void FindsIdNamedId() { // Arrange - var mm = new ModelManager(); + var mm = new ModelManager(new PluralizationService()); // Act PropertyInfo idprop = mm.GetIdProperty(typeof(Author)); @@ -32,7 +34,7 @@ public void FindsIdNamedId() public void DoesntFindMissingId() { // Arrange - var mm = new ModelManager(); + var mm = new ModelManager(new PluralizationService()); // Act PropertyInfo idprop = mm.GetIdProperty(typeof(InvalidModel)); @@ -40,5 +42,111 @@ public void DoesntFindMissingId() // Assert Assert.Fail("An InvalidOperationException should be thrown and we shouldn't get here!"); } + + [TestMethod] + public void GetJsonKeyForTypeTest() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + var postKey = mm.GetJsonKeyForType(typeof(Post)); + var authorKey = mm.GetJsonKeyForType(typeof(Author)); + var commentKey = mm.GetJsonKeyForType(typeof(Comment)); + + // Assert + Assert.AreEqual("posts", postKey); + Assert.AreEqual("authors", authorKey); + Assert.AreEqual("comments", commentKey); + } + + [TestMethod] + public void GetJsonKeyForPropertyTest() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + var idKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Id")); + var nameKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Name")); + var postsKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Posts")); + + // Assert + Assert.AreEqual("id", idKey); + Assert.AreEqual("name", nameKey); + Assert.AreEqual("posts", postsKey); + + } + + [TestMethod] + public void GetPropertyForJsonKeyTest() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + Type authorType = typeof(Author).GetType(); + + // Act + var idProp = mm.GetPropertyForJsonKey(authorType, "id"); + var nameProp = mm.GetPropertyForJsonKey(authorType, "name"); + var postsProp = mm.GetPropertyForJsonKey(authorType, "posts"); + + // Assert + Assert.AreSame(authorType.GetProperty("Id"), idProp); + Assert.AreSame(authorType.GetProperty("Name"), nameProp); + Assert.AreSame(authorType.GetProperty("Posts"), postsProp); + + } + + [TestMethod] + public void IsSerializedAsManyTest() + { + // Arrange + var mm = new ModelManager(new PluralizationService()); + + // Act + bool isArray = mm.IsSerializedAsMany(typeof(Post[])); + bool isGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); + bool isString = mm.IsSerializedAsMany(typeof(string)); + bool isAuthor = mm.IsSerializedAsMany(typeof(Author)); + bool isNonGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); + + // Assert + Assert.IsTrue(isArray); + Assert.IsTrue(isGenericEnumerable); + Assert.IsFalse(isString); + Assert.IsFalse(isAuthor); + Assert.IsFalse(isNonGenericEnumerable); + } + + [TestMethod] + public void GetElementTypeTest() + { + // Arrange + var mm = new ModelManager(new PluralizationService()); + + // Act + Type postTypeFromArray = mm.GetElementType(typeof(Post[])); + Type postTypeFromEnumerable = mm.GetElementType(typeof(IEnumerable)); + + // Assert + Assert.AreSame(typeof(Post), postTypeFromArray); + Assert.AreSame(typeof(Post), postTypeFromEnumerable); + } + + [TestMethod] + public void GetElementTypeInvalidArgumentTest() + { + // Arrange + var mm = new ModelManager(new PluralizationService()); + + // Act + Type x = mm.GetElementType(typeof(Author)); + + // Assert + Assert.IsNull(x, "Return value of GetElementType should be null for a non-Many type argument!"); + } } } diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs index 1a572d10..1fd8a6e8 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs @@ -107,7 +107,7 @@ private enum TestEnum public void CanWritePrimitiveTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); // Act // Assert Assert.IsTrue(formatter.CanWriteTypeAsPrimitive(typeof(Int32)), "CanWriteTypeAsPrimitive returned wrong answer for Integer!"); @@ -133,8 +133,7 @@ public void SerializerIntegrationTest() //ModelConverter mc = new ModelConverter(); //ContractResolver.PluralizationService = new PluralizationService(); - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); // Act @@ -158,8 +157,7 @@ public void SerializeArrayIntegrationTest() //ModelConverter mc = new ModelConverter(); //ContractResolver.PluralizationService = new PluralizationService(); - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); // Act @@ -180,7 +178,6 @@ public void Should_serialize_error() { // Arrange var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer()); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); var stream = new MemoryStream(); // Act @@ -199,8 +196,7 @@ public void Should_serialize_error() public void SerializeErrorIntegrationTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); // Act @@ -224,8 +220,7 @@ public void SerializeErrorIntegrationTest() public void DeserializeCollectionIntegrationTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); formatter.WriteToStreamAsync(typeof(Post), new List {p, p2}, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); @@ -246,8 +241,7 @@ public void DeserializeCollectionIntegrationTest() [TestMethod(), Timeout(1000)] public void DeserializeExtraPropertyTest() { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":[]}}}")); @@ -264,8 +258,7 @@ public void DeserializeExtraPropertyTest() [TestMethod(), Timeout(1000)] public void DeserializeExtraRelationshipTest() { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); - formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":[],""bogus"":[""PANIC!""]}}}")); diff --git a/JSONAPI.Tests/Json/LinkTemplateTests.cs b/JSONAPI.Tests/Json/LinkTemplateTests.cs index c74c4943..e5eb0d83 100644 --- a/JSONAPI.Tests/Json/LinkTemplateTests.cs +++ b/JSONAPI.Tests/Json/LinkTemplateTests.cs @@ -50,9 +50,9 @@ public void SetupModels() public void GetResourceWithLinkTemplateRelationship() { var formatter = new JsonApiFormatter - { - PluralizationService = new JSONAPI.Core.PluralizationService() - }; + ( + new JSONAPI.Core.PluralizationService() + ); var stream = new MemoryStream(); formatter.WriteToStreamAsync(typeof(Post), ThePost, stream, null, null); diff --git a/JSONAPI/Core/IModelManager.cs b/JSONAPI/Core/IModelManager.cs index 78c21d83..8f5a0305 100644 --- a/JSONAPI/Core/IModelManager.cs +++ b/JSONAPI/Core/IModelManager.cs @@ -9,6 +9,70 @@ namespace JSONAPI.Core { public interface IModelManager { + IPluralizationService PluralizationService { get; } + + /// + /// Returns the property that is treated as the unique identifier in a given class. + /// This is used most importantly by JsonApiFormatter to determine what value to + /// write when serializing a "Many" relationship as an array of Ids. It is also + /// used to make dummy related objects (with only the Id property set) when + /// deserializing a JSON payload that specifies a related object only by Id. + /// + /// Rules for determining this may vary by implementation. + /// + /// + /// The property determined to represent the Id. PropertyInfo GetIdProperty(Type type); + + /// + /// Returns the key that will be used to represent a collection of objects of a + /// given type, for example in the top-level of a JSON API document or within + /// the "linked" objects section of a payload. + /// + /// The serializable Type + /// The string denoting the given type in JSON documents. + string GetJsonKeyForType(Type type); + + /// + /// Returns the key that will be used to represent the given property in serialized + /// JSON. Inverse of GetPropertyForJsonKey. + /// + /// The serializable property + /// The string denoting the given property within a JSON document. + string GetJsonKeyForProperty(PropertyInfo propInfo); //TODO: Do we need to have a type parameter here, in case the property is inherited? + + /// + /// Returns the property corresponding to a given JSON Key. Inverse of GetJsonKeyForProperty. + /// + /// The Type to find the property on + /// The JSON key representing a property + /// + PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey); + + /// + /// Analogue to System.Type.GetProperties(), but made available so that any caching done + /// by an IModelManager can be leveraged to return the results faster. + /// + /// The type to get properties from + /// All properties recognized by the IModelManager. + //TODO: This needs to include JsonIgnore'd properties, so that they can be found and explicitly included at runtime...confusing? Add another method that excludes these? + PropertyInfo[] GetProperties(Type type); + + /// + /// Determines whether or not the given type will be treated as a "Many" relationship. + /// + /// The serializable Type + /// True for Array and IEnumerable<T> types, false otherwise. + bool IsSerializedAsMany(Type type); + + /// + /// Analogue for System.Type.GetElementType, but works for arrays or IEnumerable<T>, + /// and provides a capture point to cache potentially expensive reflection operations that + /// have to occur repeatedly in JsonApiFormatter. + /// + /// A type which must be either an Array type or implement IEnumerable<T>. + /// The element type of an Array, or the first generic parameter of an IEnumerable<T>. + Type GetElementType(Type manyType); + } } diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index 93292a79..5dc49bcc 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -1,4 +1,5 @@ -using System; +using JSONAPI.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -9,15 +10,47 @@ namespace JSONAPI.Core { public class ModelManager : IModelManager { - public ModelManager() { } + public ModelManager(IPluralizationService pluralizationService) + { + _pluralizationService = pluralizationService; + } + + protected IPluralizationService _pluralizationService = null; + public IPluralizationService PluralizationService + { + get + { + return _pluralizationService; + } + } #region Cache storage - private Lazy> _idProperties + protected Lazy> _idProperties = new Lazy>( () => new Dictionary() ); + protected Lazy>> _propertyMaps + = new Lazy>>( + () => new Dictionary>() + ); + + protected Lazy> _jsonKeysForType + = new Lazy>( + () => new Dictionary() + ); + + protected Lazy> _isSerializedAsMany + = new Lazy>( + () => new Dictionary() + ); + + protected Lazy> _getElementType + = new Lazy>( + () => new Dictionary() + ); + #endregion #region Id property determination @@ -46,5 +79,129 @@ public PropertyInfo GetIdProperty(Type type) } #endregion + + #region Property Maps + + protected IDictionary GetPropertyMap(Type type) + { + Dictionary propMap = null; + + var propMapCache = _propertyMaps.Value; + + lock (propMapCache) + { + if (propMapCache.TryGetValue(type, out propMap)) return propMap; + + propMap = new Dictionary(); + PropertyInfo[] props = type.GetProperties(); + foreach (PropertyInfo prop in props) + { + propMap[GetJsonKeyForProperty(prop)] = prop; + } + + propMapCache.Add(type, propMap); + } + + return propMap; + } + + public PropertyInfo[] GetProperties(Type type) + { + return GetPropertyMap(type).Values.ToArray(); + } + + public PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey) + { + PropertyInfo propInfo; + if (GetPropertyMap(type).TryGetValue(jsonKey, out propInfo)) return propInfo; + else return null; // Or, throw an exception here?? + } + + #endregion + + public string GetJsonKeyForType(Type type) + { + string key = null; + + var keyCache = _jsonKeysForType.Value; + + lock (keyCache) + { + if (keyCache.TryGetValue(type, out key)) return key; + + if (IsSerializedAsMany(type)) + type = GetElementType(type); + + var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList(); + + string title = type.Name; + if (attrs.Any()) + { + var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") + .Select(arg => arg.TypedValue.Value.ToString()).ToList(); + if (titles.Any()) title = titles.First(); + } + + key = FormatPropertyName(PluralizationService.Pluralize(title)); + + keyCache.Add(type, key); + } + + return key; + } + + public string GetJsonKeyForProperty(PropertyInfo propInfo) + { + return FormatPropertyName(propInfo.Name); + //TODO: Respect [JsonProperty(PropertyName = "FooBar")], and probably cache the result. + } + + protected static string FormatPropertyName(string propertyName) + { + string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1); + return result; + } + + public bool IsSerializedAsMany(Type type) + { + bool isMany; + + var isManyCache = _isSerializedAsMany.Value; + + lock (isManyCache) + { + if (isManyCache.TryGetValue(type, out isMany)) return isMany; + + isMany = + type.IsArray || + (type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType); + + isManyCache.Add(type, isMany); + } + + return isMany; + } + + public Type GetElementType(Type manyType) + { + Type etype = null; + + var etypeCache = _getElementType.Value; + + lock (etypeCache) + { + if (etypeCache.TryGetValue(manyType, out etype)) return etype; + + if (manyType.IsGenericType) + etype = manyType.GetGenericArguments()[0]; + else + etype = manyType.GetElementType(); + + etypeCache.Add(manyType, etype); + } + + return etype; + } + } } diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index e1f62f4c..c678b402 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -19,26 +19,49 @@ namespace JSONAPI.Json { public class JsonApiFormatter : JsonMediaTypeFormatter { - public JsonApiFormatter() - : this(new ErrorSerializer()) + public JsonApiFormatter(IModelManager modelManager) : + this(modelManager, new ErrorSerializer()) + { + } + + public JsonApiFormatter(IPluralizationService pluralizationService) : + this(new ModelManager(pluralizationService)) { - if (_modelManager == null) _modelManager = new ModelManager(); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json")); } + // Currently for tests only. internal JsonApiFormatter(IErrorSerializer errorSerializer) + : this(new ModelManager(new PluralizationService()), errorSerializer) { - _errorSerializer = errorSerializer; + } - public JsonApiFormatter(IModelManager modelManager) : this() + internal JsonApiFormatter(IModelManager modelManager, IErrorSerializer errorSerializer) { _modelManager = modelManager; + _errorSerializer = errorSerializer; + SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json")); + } + + [Obsolete("Use ModelManager.PluralizationService instead")] + public IPluralizationService PluralizationService //FIXME: Deprecated, will be removed shortly + { + get + { + return _modelManager.PluralizationService; + } } - public IPluralizationService PluralizationService { get; set; } //FIXME: Deprecated, will be removed shortly private readonly IErrorSerializer _errorSerializer; + private readonly IModelManager _modelManager; + public IModelManager ModelManager + { + get + { + return _modelManager; + } + } private Lazy> _relationAggregators = new Lazy>( @@ -106,18 +129,18 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w else { Type valtype = GetSingleType(value.GetType()); - if (IsMany(value.GetType())) + if (_modelManager.IsSerializedAsMany(value.GetType())) aggregator.AddPrimary(valtype, (IEnumerable) value); else aggregator.AddPrimary(valtype, value); //writer.Formatting = Formatting.Indented; - var root = GetPropertyName(type, value); + var root = _modelManager.GetJsonKeyForType(type); writer.WriteStartObject(); writer.WritePropertyName(root); - if (IsMany(value.GetType())) + if (_modelManager.IsSerializedAsMany(value.GetType())) this.SerializeMany(value, writeStream, writer, serializer, aggregator); else this.Serialize(value, writeStream, writer, serializer, aggregator); @@ -162,7 +185,9 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js var idProp = _modelManager.GetIdProperty(value.GetType()); writer.WriteValue(GetValueForIdProperty(idProp, value)); - PropertyInfo[] props = value.GetType().GetProperties(); + // 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 modelProps = new List(); @@ -177,7 +202,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js continue; // numbers, strings, dates... - writer.WritePropertyName(FormatPropertyName(prop.Name)); + writer.WritePropertyName(_modelManager.GetJsonKeyForProperty(prop)); serializer.Serialize(writer, prop.GetValue(value, null)); } else @@ -218,7 +243,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js } if (skip) continue; - writer.WritePropertyName(FormatPropertyName(prop.Name)); + 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! @@ -234,7 +259,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js switch (sa) { case SerializeAsOptions.Ids: - //writer.WritePropertyName(ContractResolver.FormatPropertyName(prop.Name)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); IEnumerable items = (IEnumerable)prop.GetValue(value, null); if (items == null) { @@ -261,7 +286,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js 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.FormatPropertyName(prop.Name)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); writer.WritePropertyName("href"); @@ -270,7 +295,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js break; case SerializeAsOptions.Embedded: // Not really supported by Ember Data yet, incidentally...but easy to implement here. - //writer.WritePropertyName(ContractResolver.FormatPropertyName(prop.Name)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //serializer.Serialize(writer, prop.GetValue(value, null)); this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); break; @@ -290,7 +315,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js switch (sa) { case SerializeAsOptions.Ids: - //writer.WritePropertyName(ContractResolver.FormatPropertyName(prop.Name)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); serializer.Serialize(writer, objId); if (iip) if (aggregator != null) @@ -300,9 +325,10 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js 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, - value.GetType().GetProperty("Id").GetValue(value, null)); - //writer.WritePropertyName(ContractResolver.FormatPropertyName(prop.Name)); + 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); @@ -310,7 +336,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js break; case SerializeAsOptions.Embedded: // Not really supported by Ember Data yet, incidentally...but easy to implement here. - //writer.WritePropertyName(ContractResolver.FormatPropertyName(prop.Name)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //serializer.Serialize(writer, prop.GetValue(value, null)); this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); break; @@ -406,7 +432,7 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J foreach (KeyValuePair> apair in writers) { apair.Value.Key.WriteEnd(); // close off the array - writer.WritePropertyName(GetPropertyName(apair.Key)); + 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 } @@ -429,7 +455,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, { object retval = null; Type singleType = GetSingleType(type); - var pripropname = GetPropertyName(type); + var pripropname = _modelManager.GetJsonKeyForType(type); var contentHeaders = content == null ? null : content.Headers; // If content length is 0 then return default value for this type @@ -506,7 +532,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, */ if (retval != null) { - if (!type.IsAssignableFrom(retval.GetType()) && IsMany(type)) + if (!type.IsAssignableFrom(retval.GetType()) && _modelManager.IsSerializedAsMany(type)) { IList list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(singleType)); list.Add(retval); @@ -556,14 +582,6 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, public object Deserialize(Type objectType, Stream readStream, JsonReader reader, JsonSerializer serializer) { object retval = Activator.CreateInstance(objectType); - PropertyInfo[] props = objectType.GetProperties(); - - //TODO: This could get expensive...cache these maps per type, so we only build the map once? - IDictionary propMap = new Dictionary(); - foreach (PropertyInfo prop in props) - { - propMap[FormatPropertyName(prop.Name)] = prop; - } if (reader.TokenType != JsonToken.StartObject) throw new JsonReaderException(String.Format("Expected JsonToken.StartObject, got {0}", reader.TokenType.ToString())); reader.Read(); // Burn the StartObject token @@ -579,7 +597,7 @@ public object Deserialize(Type objectType, Stream readStream, JsonReader reader, //TODO: linked resources (Done??) DeserializeLinkedResources(retval, readStream, reader, serializer); } - else if (propMap.TryGetValue(value, out prop)) + else if ((prop = _modelManager.GetPropertyForJsonKey(objectType, value)) != null) { reader.Read(); // burn the PropertyName token //TODO: Embedded would be dropped here! @@ -623,7 +641,7 @@ select prop { if (links == null) break; // Well, apparently we don't have any data for the relationships! - string btId = (string)links[FormatPropertyName(prop.Name)]; + 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! @@ -647,13 +665,7 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade //reader.Read(); if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("'links' property is not an object!"); - //TODO: Redundant, already done in Deserialize method...optimize? - PropertyInfo[] props = obj.GetType().GetProperties(); - IDictionary propMap = new Dictionary(); - foreach (PropertyInfo prop in props) - { - propMap[FormatPropertyName(prop.Name)] = prop; - } + Type objectType = obj.GetType(); while (reader.Read()) { @@ -661,8 +673,8 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade { string value = (string)reader.Value; reader.Read(); // burn the PropertyName token - PropertyInfo prop; - if (propMap.TryGetValue(value, out prop) && !CanWriteTypeAsPrimitive(prop.PropertyType)) + PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); + if (prop != null && !CanWriteTypeAsPrimitive(prop.PropertyType)) { //FIXME: We're really assuming they're ICollections...but testing for that doesn't work for some reason. Break prone! if (prop.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)) && prop.PropertyType.IsGenericType) @@ -768,58 +780,9 @@ private object DeserializePrimitive(Type type, JsonReader reader) #endregion - private string GetPropertyName(Type type, dynamic value = null) - { - if (IsMany(type)) - type = GetSingleType(type); - - var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList(); - - string title = type.Name; - if (attrs.Any()) - { - var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") - .Select(arg => arg.TypedValue.Value.ToString()).ToList(); - if (titles.Any()) title = titles.First(); - } - - return FormatPropertyName(this.PluralizationService.Pluralize(title)); - } - - //private string GetPropertyName(Type type) - //{ - // return FormatPropertyName(PluralizationService.Pluralize(type.Name)); - //} - - private bool IsMany(dynamic value = null) - { - if (value != null && (value is IEnumerable || value.GetType().IsArray)) - return true; - else - return false; - } - - private bool IsMany(Type type) - { - return - type.IsArray || - (type.GetInterfaces().Contains(typeof(IEnumerable)) && type.IsGenericType); - } - private Type GetSingleType(Type type)//dynamic value = null) { - if (IsMany(type)) - if (type.IsGenericType) - return type.GetGenericArguments()[0]; - else - return type.GetElementType(); - return type; - } - - public static string FormatPropertyName(string propertyName) - { - string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1); - return result; + return _modelManager.IsSerializedAsMany(type) ? _modelManager.GetElementType(type) : type; } protected object GetById(Type type, string id) @@ -831,13 +794,6 @@ protected object GetById(Type type, string id) return retval; } - /* - protected PropertyInfo GetIdProperty(Type type) - { - return type.GetProperty("Id"); - } - */ - protected string GetValueForIdProperty(PropertyInfo idprop, object obj) { if (idprop != null)