Skip to content

Commit 547de42

Browse files
committed
Finished refactoring ModelManager, now there is no circular dependency with JsonApiFormatter, and the interface boundary is much more clear.
1 parent eece47c commit 547de42

File tree

4 files changed

+132
-55
lines changed

4 files changed

+132
-55
lines changed

JSONAPI.Tests/Core/ModelManagerTests.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
using JSONAPI.Core;
44
using JSONAPI.Tests.Models;
55
using System.Reflection;
6+
using System.Collections.Generic;
7+
using System.Collections;
68

79
namespace JSONAPI.Tests.Core
810
{
911
[TestClass]
1012
public class ModelManagerTests
1113
{
12-
private class InvalidModel
14+
private class InvalidModel // No Id discernable!
1315
{
1416
public string Data { get; set; }
1517
}
@@ -97,5 +99,54 @@ public void GetPropertyForJsonKeyTest()
9799
Assert.AreSame(authorType.GetProperty("Posts"), postsProp);
98100

99101
}
102+
103+
[TestMethod]
104+
public void IsSerializedAsManyTest()
105+
{
106+
// Arrange
107+
var mm = new ModelManager();
108+
109+
// Act
110+
bool isArray = mm.IsSerializedAsMany(typeof(Post[]));
111+
bool isGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable<Post>));
112+
bool isString = mm.IsSerializedAsMany(typeof(string));
113+
bool isAuthor = mm.IsSerializedAsMany(typeof(Author));
114+
bool isNonGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable));
115+
116+
// Assert
117+
Assert.IsTrue(isArray);
118+
Assert.IsTrue(isGenericEnumerable);
119+
Assert.IsFalse(isString);
120+
Assert.IsFalse(isAuthor);
121+
Assert.IsFalse(isNonGenericEnumerable);
122+
}
123+
124+
[TestMethod]
125+
public void GetElementTypeTest()
126+
{
127+
// Arrange
128+
var mm = new ModelManager();
129+
130+
// Act
131+
Type postTypeFromArray = mm.GetElementType(typeof(Post[]));
132+
Type postTypeFromEnumerable = mm.GetElementType(typeof(IEnumerable<Post>));
133+
134+
// Assert
135+
Assert.AreSame(typeof(Post), postTypeFromArray);
136+
Assert.AreSame(typeof(Post), postTypeFromEnumerable);
137+
}
138+
139+
[TestMethod]
140+
public void GetElementTypeInvalidArgumentTest()
141+
{
142+
// Arrange
143+
var mm = new ModelManager();
144+
145+
// Act
146+
Type x = mm.GetElementType(typeof(Author));
147+
148+
// Assert
149+
Assert.IsNull(x, "Return value of GetElementType should be null for a non-Many type argument!");
150+
}
100151
}
101152
}

JSONAPI/Core/IModelManager.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,42 @@ public interface IModelManager
1111
{
1212
IPluralizationService PluralizationService { get; }
1313

14+
/// <summary>
15+
/// Returns the property that is treated as the unique identifier in a given class.
16+
/// This is used most importantly by JsonApiFormatter to determine what value to
17+
/// write when serializing a "Many" relationship as an array of Ids. It is also
18+
/// used to make dummy related objects (with only the Id property set) when
19+
/// deserializing a JSON payload that specifies a related object only by Id.
20+
///
21+
/// Rules for determining this may vary by implementation.
22+
/// </summary>
23+
/// <param name="type"></param>
24+
/// <returns>The property determined to represent the Id.</returns>
1425
PropertyInfo GetIdProperty(Type type);
26+
27+
/// <summary>
28+
/// Returns the key that will be used to represent a collection of objects of a
29+
/// given type, for example in the top-level of a JSON API document or within
30+
/// the "linked" objects section of a payload.
31+
/// </summary>
32+
/// <param name="type">The serializable Type</param>
33+
/// <returns>The string denoting the given type in JSON documents.</returns>
1534
string GetJsonKeyForType(Type type);
35+
36+
/// <summary>
37+
/// Returns the key that will be used to represent the given property in serialized
38+
/// JSON. Inverse of GetPropertyForJsonKey.
39+
/// </summary>
40+
/// <param name="propInfo">The serializable property</param>
41+
/// <returns>The string denoting the given property within a JSON document.</returns>
1642
string GetJsonKeyForProperty(PropertyInfo propInfo); //TODO: Do we need to have a type parameter here, in case the property is inherited?
43+
44+
/// <summary>
45+
/// Returns the property corresponding to a given JSON Key. Inverse of GetJsonKeyForType.
46+
/// </summary>
47+
/// <param name="type">The Type to find the property on</param>
48+
/// <param name="jsonKey">The JSON key representing a property</param>
49+
/// <returns></returns>
1750
PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey);
1851

1952
/// <summary>
@@ -25,7 +58,21 @@ public interface IModelManager
2558
//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?
2659
PropertyInfo[] GetProperties(Type type);
2760

28-
//[Obsolete]
29-
//IDictionary<string, PropertyInfo> GetPropertyMap(Type type);
61+
/// <summary>
62+
/// Determines whether or not the given type will be treated as a "Many" relationship.
63+
/// </summary>
64+
/// <param name="type">The serializable Type</param>
65+
/// <returns>True for Array and IEnumerable&lt;T&gt; types, false otherwise.</returns>
66+
bool IsSerializedAsMany(Type type);
67+
68+
/// <summary>
69+
/// Analogue for System.Type.GetElementType, but works for arrays or IEnumerable&lt;T&gt;,
70+
/// and provides a capture point to cache potentially expensive reflection operations that
71+
/// have to occur repeatedly in JsonApiFormatter.
72+
/// </summary>
73+
/// <param name="manyType">A type which must be either an Array type or implement IEnumerable&lt;T&gt;.</param>
74+
/// <returns>The element type of an Array, or the first generic parameter of an IEnumerable&lt;T&gt;.</returns>
75+
Type GetElementType(Type manyType);
76+
3077
}
3178
}

JSONAPI/Core/ModelManager.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,6 @@ public PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey)
113113

114114
#endregion
115115

116-
//TODO: This has been "moved" here so we can cache the results and improve performance...but
117-
// it raises the question of whether the various methods called within here should belong
118-
// to JsonApiFormatter at all...should they move here also? Should the IPluralizationService
119-
// instance belong to ModelManager instead?
120116
public string GetJsonKeyForType(Type type)
121117
{
122118
string key = null;
@@ -127,8 +123,8 @@ public string GetJsonKeyForType(Type type)
127123
{
128124
if (keyCache.TryGetValue(type, out key)) return key;
129125

130-
if (JsonApiFormatter.IsMany(type))
131-
type = JsonApiFormatter.GetSingleType(type);
126+
if (IsSerializedAsMany(type))
127+
type = GetElementType(type);
132128

133129
var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList();
134130

@@ -159,5 +155,26 @@ protected static string FormatPropertyName(string propertyName)
159155
string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1);
160156
return result;
161157
}
158+
159+
public bool IsSerializedAsMany(Type type)
160+
{
161+
bool isMany =
162+
type.IsArray ||
163+
(type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType);
164+
165+
return isMany;
166+
}
167+
168+
public Type GetElementType(Type manyType)
169+
{
170+
Type etype = null;
171+
if (manyType.IsGenericType)
172+
etype = manyType.GetGenericArguments()[0];
173+
else
174+
etype = manyType.GetElementType();
175+
176+
return etype;
177+
}
178+
162179
}
163180
}

JSONAPI/Json/JsonApiFormatter.cs

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,18 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w
119119
else
120120
{
121121
Type valtype = GetSingleType(value.GetType());
122-
if (IsMany(value.GetType()))
122+
if (_modelManager.IsSerializedAsMany(value.GetType()))
123123
aggregator.AddPrimary(valtype, (IEnumerable<object>) value);
124124
else
125125
aggregator.AddPrimary(valtype, value);
126126

127127
//writer.Formatting = Formatting.Indented;
128128

129-
var root = GetJsonKeyForType(type, value);
129+
var root = _modelManager.GetJsonKeyForType(type);
130130

131131
writer.WriteStartObject();
132132
writer.WritePropertyName(root);
133-
if (IsMany(value.GetType()))
133+
if (_modelManager.IsSerializedAsMany(value.GetType()))
134134
this.SerializeMany(value, writeStream, writer, serializer, aggregator);
135135
else
136136
this.Serialize(value, writeStream, writer, serializer, aggregator);
@@ -422,7 +422,7 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J
422422
foreach (KeyValuePair<Type, KeyValuePair<JsonWriter, StringWriter>> apair in writers)
423423
{
424424
apair.Value.Key.WriteEnd(); // close off the array
425-
writer.WritePropertyName(GetJsonKeyForType(apair.Key));
425+
writer.WritePropertyName(_modelManager.GetJsonKeyForType(apair.Key));
426426
writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter
427427
}
428428

@@ -445,7 +445,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content,
445445
{
446446
object retval = null;
447447
Type singleType = GetSingleType(type);
448-
var pripropname = GetJsonKeyForType(type);
448+
var pripropname = _modelManager.GetJsonKeyForType(type);
449449
var contentHeaders = content == null ? null : content.Headers;
450450

451451
// If content length is 0 then return default value for this type
@@ -522,7 +522,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content,
522522
*/
523523
if (retval != null)
524524
{
525-
if (!type.IsAssignableFrom(retval.GetType()) && IsMany(type))
525+
if (!type.IsAssignableFrom(retval.GetType()) && _modelManager.IsSerializedAsMany(type))
526526
{
527527
IList list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(singleType));
528528
list.Add(retval);
@@ -770,42 +770,11 @@ private object DeserializePrimitive(Type type, JsonReader reader)
770770

771771
#endregion
772772

773-
//TODO: Remove shell function, call _modelManager.GetJsonKeyForType directly.
774-
private string GetJsonKeyForType(Type type, dynamic value = null)
773+
private Type GetSingleType(Type type)//dynamic value = null)
775774
{
776-
return _modelManager.GetJsonKeyForType(type);
775+
return _modelManager.IsSerializedAsMany(type) ? _modelManager.GetElementType(type) : type;
777776
}
778777

779-
//private string GetPropertyName(Type type)
780-
//{
781-
// return FormatPropertyName(PluralizationService.Pluralize(type.Name));
782-
//}
783-
784-
internal static bool IsMany(Type type)
785-
{
786-
return
787-
type.IsArray ||
788-
(type.GetInterfaces().Contains(typeof(IEnumerable)) && type.IsGenericType);
789-
}
790-
791-
internal static Type GetSingleType(Type type)//dynamic value = null)
792-
{
793-
if (IsMany(type))
794-
if (type.IsGenericType)
795-
return type.GetGenericArguments()[0];
796-
else
797-
return type.GetElementType();
798-
return type;
799-
}
800-
801-
/*
802-
public static string FormatPropertyName(string propertyName)
803-
{
804-
string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1);
805-
return result;
806-
}
807-
*/
808-
809778
protected object GetById(Type type, string id)
810779
{
811780
// Only good for creating dummy relationship objects...
@@ -815,13 +784,6 @@ protected object GetById(Type type, string id)
815784
return retval;
816785
}
817786

818-
/*
819-
protected PropertyInfo GetIdProperty(Type type)
820-
{
821-
return type.GetProperty("Id");
822-
}
823-
*/
824-
825787
protected string GetValueForIdProperty(PropertyInfo idprop, object obj)
826788
{
827789
if (idprop != null)

0 commit comments

Comments
 (0)