Skip to content

Commit 9d3f1ce

Browse files
committed
Merge branch 'id-prop-attribute-issue-21'
2 parents 2cfe6e5 + a9dd682 commit 9d3f1ce

File tree

9 files changed

+159
-9
lines changed

9 files changed

+159
-9
lines changed

JSONAPI.Tests/Core/ModelManagerTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ private class InvalidModel // No Id discernable!
1616
public string Data { get; set; }
1717
}
1818

19+
private class CustomIdModel
20+
{
21+
[JSONAPI.Attributes.UseAsId]
22+
public Guid Uuid { get; set; }
23+
24+
public string Data { get; set; }
25+
}
26+
1927
[TestMethod]
2028
public void FindsIdNamedId()
2129
{
@@ -43,6 +51,18 @@ public void DoesntFindMissingId()
4351
Assert.Fail("An InvalidOperationException should be thrown and we shouldn't get here!");
4452
}
4553

54+
[TestMethod]
55+
public void FindsIdFromAttribute()
56+
{
57+
// Arrange
58+
var mm = new ModelManager(new PluralizationService());
59+
60+
// Act
61+
PropertyInfo idprop = mm.GetIdProperty(typeof(CustomIdModel));
62+
// Assert
63+
Assert.AreSame(typeof(CustomIdModel).GetProperty("Uuid"), idprop);
64+
}
65+
4666
[TestMethod]
4767
public void GetJsonKeyForTypeTest()
4868
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"nonStandardIdThings": [
3+
{
4+
"id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f",
5+
"uuid": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f",
6+
"data": "Swap"
7+
}
8+
]
9+
}

JSONAPI.Tests/JSONAPI.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
<None Include="Data\DeserializeRawJsonTest.json">
107107
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
108108
</None>
109+
<None Include="Data\NonStandardIdTest.json">
110+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
111+
</None>
109112
<None Include="Data\SerializerIntegrationTest.json">
110113
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
111114
</None>

JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ public void SerializeError(object error, Stream writeStream, JsonWriter writer,
3737
}
3838
}
3939

40+
private class NonStandardIdThing
41+
{
42+
[JSONAPI.Attributes.UseAsId]
43+
public Guid Uuid { get; set; }
44+
public string Data { get; set; }
45+
}
46+
4047
[TestInitialize]
4148
public void SetupModels()
4249
{
@@ -291,5 +298,86 @@ public void DeserializeExtraRelationshipTest()
291298
// Assert
292299
Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout!
293300
}
301+
302+
[TestMethod]
303+
[DeploymentItem(@"Data\NonStandardIdTest.json")]
304+
public void SerializeNonStandardIdTest()
305+
{
306+
var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService());
307+
var stream = new MemoryStream();
308+
var payload = new List<NonStandardIdThing> {
309+
new NonStandardIdThing { Uuid = new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"), Data = "Swap" }
310+
};
311+
312+
// Act
313+
formatter.WriteToStreamAsync(typeof(List<NonStandardIdThing>), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null);
314+
315+
// Assert
316+
var expectedJson = File.ReadAllText("NonStandardIdTest.json");
317+
var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson);
318+
var output = System.Text.Encoding.ASCII.GetString(stream.ToArray());
319+
output.Should().Be(minifiedExpectedJson);
320+
}
321+
322+
#region Non-standard Id attribute tests
323+
324+
[TestMethod]
325+
[DeploymentItem(@"Data\NonStandardIdTest.json")]
326+
public void DeserializeNonStandardIdTest()
327+
{
328+
var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService());
329+
var stream = new FileStream("NonStandardIdTest.json",FileMode.Open);
330+
331+
// Act
332+
IList<NonStandardIdThing> things;
333+
things = (IList<NonStandardIdThing>)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result;
334+
stream.Close();
335+
336+
// Assert
337+
things.Count.Should().Be(1);
338+
things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"));
339+
}
340+
341+
[TestMethod]
342+
[DeploymentItem(@"Data\NonStandardIdTest.json")]
343+
public void DeserializeNonStandardIdWithIdOnly()
344+
{
345+
var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService());
346+
string json = File.ReadAllText("NonStandardIdTest.json");
347+
json = Regex.Replace(json, @"""uuid"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,",""); // remove the uuid attribute
348+
var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json));
349+
350+
// Act
351+
IList<NonStandardIdThing> things;
352+
things = (IList<NonStandardIdThing>)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result;
353+
354+
// Assert
355+
json.Should().NotContain("uuid", "The \"uuid\" attribute was supposed to be removed, test methodology problem!");
356+
things.Count.Should().Be(1);
357+
things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"));
358+
}
359+
360+
[TestMethod]
361+
[DeploymentItem(@"Data\NonStandardIdTest.json")]
362+
public void DeserializeNonStandardIdWithoutId()
363+
{
364+
var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService());
365+
string json = File.ReadAllText("NonStandardIdTest.json");
366+
json = Regex.Replace(json, @"""id"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,", ""); // remove the uuid attribute
367+
var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json));
368+
369+
// Act
370+
IList<NonStandardIdThing> things;
371+
things = (IList<NonStandardIdThing>)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result;
372+
373+
// Assert
374+
json.Should().NotContain("\"id\"", "The \"id\" attribute was supposed to be removed, test methodology problem!");
375+
things.Count.Should().Be(1);
376+
things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"));
377+
378+
}
379+
380+
#endregion
381+
294382
}
295383
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace JSONAPI.Attributes
4+
{
5+
[System.AttributeUsage(System.AttributeTargets.Property)]
6+
public class UseAsIdAttribute : System.Attribute
7+
{
8+
public UseAsIdAttribute() { }
9+
}
10+
}

JSONAPI/Core/MetadataManager.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ internal void SetMetaForProperty(object deserialized, PropertyInfo prop, object
4444
meta = new Dictionary<string, object>();
4545
cwt.Add(deserialized, meta);
4646
}
47-
meta.Add(prop.Name, value);
47+
if (!meta.ContainsKey(prop.Name)) // Temporary fix for non-standard Id reprecussions...this internal implementation will change soon anyway.
48+
meta.Add(prop.Name, value);
49+
4850
}
4951

5052

JSONAPI/Core/ModelManager.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using JSONAPI.Json;
1+
using JSONAPI.Attributes;
2+
using JSONAPI.Json;
23
using System;
34
using System.Collections.Generic;
45
using System.Linq;
@@ -65,9 +66,14 @@ public PropertyInfo GetIdProperty(Type type)
6566
{
6667
if (idPropCache.TryGetValue(type, out idprop)) return idprop;
6768

68-
//TODO: Enable attribute-based determination
69-
70-
idprop = type.GetProperty("Id");
69+
// First, look for UseAsIdAttribute
70+
idprop = type.GetProperties()
71+
.Where(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute)))
72+
.FirstOrDefault();
73+
if (idprop == null)
74+
{
75+
idprop = type.GetProperty("Id");
76+
}
7177

7278
if (idprop == null)
7379
throw new InvalidOperationException(String.Format("Unable to determine Id property for type {0}", type));

JSONAPI/JSONAPI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<Compile Include="Attributes\LinkTemplate.cs" />
7070
<Compile Include="Attributes\SerializeAs.cs" />
7171
<Compile Include="Attributes\SerializeStringAsRawJsonAttribute.cs" />
72+
<Compile Include="Attributes\UseAsIdAttribute.cs" />
7273
<Compile Include="Core\IModelManager.cs" />
7374
<Compile Include="Core\IPluralizationService.cs" />
7475
<Compile Include="Core\IMaterializer.cs" />

JSONAPI/Json/JsonApiFormatter.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js
180180
{
181181
writer.WriteStartObject();
182182

183+
// The spec no longer requires that the ID key be "id":
184+
// "An ID SHOULD be represented by an 'id' key..." :-/
185+
// But Ember Data does. So, we'll add "id" to the document
186+
// always, and also serialize the property under its given
187+
// name, for now at least.
188+
//TODO: Partly because of this, we should probably disallow updates to Id properties where practical.
189+
183190
// Do the Id now...
184191
writer.WritePropertyName("id");
185192
var idProp = _modelManager.GetIdProperty(value.GetType());
@@ -194,15 +201,16 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js
194201

195202
foreach (PropertyInfo prop in props)
196203
{
197-
if (prop == idProp) continue;
204+
string propKey = _modelManager.GetJsonKeyForProperty(prop);
205+
if (propKey == "id") continue; // Don't write the "id" property twice, see above!
198206

199207
if (this.CanWriteTypeAsPrimitive(prop.PropertyType))
200208
{
201209
if (prop.GetCustomAttributes().Any(attr => attr is JsonIgnoreAttribute))
202210
continue;
203211

204212
// numbers, strings, dates...
205-
writer.WritePropertyName(_modelManager.GetJsonKeyForProperty(prop));
213+
writer.WritePropertyName(propKey);
206214

207215
var propertyValue = prop.GetValue(value, null);
208216

@@ -609,14 +617,17 @@ public object Deserialize(Type objectType, Stream readStream, JsonReader reader,
609617
if (reader.TokenType == JsonToken.PropertyName)
610618
{
611619
string value = (string)reader.Value;
612-
PropertyInfo prop;
620+
PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value);
621+
// If the model object has a non-standard Id property, but the "id" key is being used...
622+
if (prop == null && value == "id") prop = _modelManager.GetIdProperty(objectType);
623+
613624
if (value == "links")
614625
{
615626
reader.Read(); // burn the PropertyName token
616627
//TODO: linked resources (Done??)
617628
DeserializeLinkedResources(retval, readStream, reader, serializer);
618629
}
619-
else if ((prop = _modelManager.GetPropertyForJsonKey(objectType, value)) != null)
630+
else if (prop != null)
620631
{
621632
reader.Read(); // burn the PropertyName token
622633
//TODO: Embedded would be dropped here!

0 commit comments

Comments
 (0)