Skip to content

Commit fdf49e3

Browse files
committed
Implemented an important part of the spec, "Attributes omitted from the resource object should not be updated." This requires us to be able to communicate to the IMaterializer (which does actual updating) that attributes were or were not specified on the original JSON resource object. Therefore, we created the new MetadataManager singleton, and its public method PropertyWasPresent(object deserialized, PropertyInfo prop). It is tightly coupled with JsonApiFormatter, but loosely with an IMaterializer such as EntityFrameworkMaterializer.
The ability to check for the presence of a specified value in the JSON also makes it possible to use default values on a POSTed object--before we could not tell if there was underposting going on! This will bump the version to 0.2a, because the behavior in an underposting scenario has changed.
1 parent 2ebfcc3 commit fdf49e3

File tree

11 files changed

+171
-8
lines changed

11 files changed

+171
-8
lines changed

JSONAPI.EntityFramework.Tests/EntityConverterTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,30 @@ public void DeserializePostIntegrationTest()
155155
//Debug.WriteLine(sw.ToString());
156156
}
157157

158+
[TestMethod]
159+
public void UnderpostingTest()
160+
{
161+
// Arrange
162+
JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter();
163+
formatter.PluralizationService = new JSONAPI.Core.PluralizationService();
164+
MemoryStream stream = new MemoryStream();
165+
166+
EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context);
167+
168+
string underpost = @"{""posts"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}";
169+
stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(underpost));
170+
171+
int previousCommentsCount = p.Comments.Count;
172+
173+
// Act
174+
Post pUpdated;
175+
pUpdated = (Post)formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result;
176+
pUpdated = materializer.MaterializeUpdate<Post>(pUpdated);
177+
178+
// Assert
179+
Assert.AreEqual(previousCommentsCount, pUpdated.Comments.Count, "Comments were wiped out!");
180+
Assert.AreEqual("Not at all linkbait!", pUpdated.Title, "Title was not updated.");
181+
}
182+
158183
}
159184
}

JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ private void Merge (Type type, object ephemeral, object material)
251251
PropertyInfo[] props = type.GetProperties();
252252
foreach (PropertyInfo prop in props)
253253
{
254+
// Comply with the spec, if a key was not set, it should not be updated!
255+
if (!MetadataManager.Instance.PropertyWasPresent(ephemeral, prop)) continue;
256+
254257
if (IsMany(prop.PropertyType))
255258
{
256259
Type elementType = GetSingleType(prop.PropertyType);

JSONAPI.EntityFramework/JSONAPI.EntityFramework.nuspec

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package >
33
<metadata>
44
<id>$id$</id>
5-
<version>0.1.0.0</version>
5+
<version>0.2.0.0</version>
66
<title>JSONAPI.NET Entity Framework Integration</title>
77
<authors>S&apos;pht&apos;Kr</authors>
88
<owners>S&apos;pht&apos;Kr</owners>
@@ -11,8 +11,11 @@
1111
<iconUrl>http://download-codeplex.sec.s-msft.com/Download?ProjectName=jsonapi&amp;DownloadId=939025</iconUrl>
1212
<requireLicenseAcceptance>false</requireLicenseAcceptance>
1313
<description>A toolkit for using WebAPI and (optionally) EntityFramework to quickly build REST services complying with the JSON API spec (jsonapi.org).</description>
14-
<releaseNotes>First release, v0.1a.</releaseNotes>
14+
<releaseNotes>Updated to take advantage of PropertyWasPresent(...) in JSONAPI--see release notes for JSONAPI.</releaseNotes>
1515
<copyright>Copyright 2014</copyright>
1616
<tags>JSON WebAPI REST ember emberjs ember.js ember-data Entity Framework EF entity-framework</tags>
17+
<dependencies>
18+
<dependency id="JSONAPI" version="0.2.0"></dependency>
19+
</dependencies>
1720
</metadata>
1821
</package>

JSONAPI.EntityFramework/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@
3232
// You can specify all the values or you can default the Build and Revision Numbers
3333
// by using the '*' as shown below:
3434
// [assembly: AssemblyVersion("1.0.*")]
35-
[assembly: AssemblyVersion("0.1.0.0")]
36-
[assembly: AssemblyFileVersion("0.1.0.0")]
35+
[assembly: AssemblyVersion("0.2.0.0")]
36+
[assembly: AssemblyFileVersion("0.2.0.0")]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using JSONAPI.Json;
4+
using System.IO;
5+
using JSONAPI.Tests.Models;
6+
using JSONAPI.Core;
7+
8+
namespace JSONAPI.Tests.Core
9+
{
10+
[TestClass]
11+
public class MetadataManagerTests
12+
{
13+
[TestMethod]
14+
public void PropertyWasPresentTest()
15+
{
16+
// Arrange
17+
18+
JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter();
19+
formatter.PluralizationService = new JSONAPI.Core.PluralizationService();
20+
MemoryStream stream = new MemoryStream();
21+
22+
stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""posts"":{""id"":42,""links"":{""author"":""18""}}}"));
23+
24+
Post p;
25+
p = (Post)formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result;
26+
27+
// Act
28+
bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id"));
29+
bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title"));
30+
bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author"));
31+
bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments"));
32+
33+
// Assert
34+
Assert.IsTrue(idWasSet, "Id was not reported as set, but was.");
35+
Assert.IsFalse(titleWasSet, "Title was reported as set, but was not.");
36+
Assert.IsTrue(authorWasSet, "Author was not reported as set, but was.");
37+
Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not.");
38+
}
39+
}
40+
}

JSONAPI.Tests/JSONAPI.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
</CodeAnalysisDependentAssemblyPaths>
6868
</ItemGroup>
6969
<ItemGroup>
70+
<Compile Include="Core\MetadataManagerTests.cs" />
7071
<Compile Include="Json\JsonApiMediaFormaterTests.cs" />
7172
<Compile Include="Models\Author.cs" />
7273
<Compile Include="Models\Comment.cs" />

JSONAPI/Core/MetadataManager.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Runtime.CompilerServices;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace JSONAPI.Core
10+
{
11+
public sealed class MetadataManager
12+
{
13+
#region Singleton pattern
14+
15+
private static readonly MetadataManager instance = new MetadataManager();
16+
17+
private MetadataManager() { }
18+
19+
public static MetadataManager Instance
20+
{
21+
get
22+
{
23+
return instance;
24+
}
25+
}
26+
27+
#endregion
28+
29+
private readonly ConditionalWeakTable<object, Dictionary<string, object>> cwt
30+
= new ConditionalWeakTable<object, Dictionary<string, object>>();
31+
32+
/*
33+
internal void SetDeserializationMetadata(object deserialized, Dictionary<string, object> meta)
34+
{
35+
cwt.Add(deserialized, meta);
36+
}
37+
*/
38+
39+
internal void SetMetaForProperty(object deserialized, PropertyInfo prop, object value)
40+
{
41+
Dictionary<string, object> meta;
42+
if (!cwt.TryGetValue(deserialized, out meta))
43+
{
44+
meta = new Dictionary<string, object>();
45+
cwt.Add(deserialized, meta);
46+
}
47+
meta.Add(prop.Name, value);
48+
}
49+
50+
51+
internal Dictionary<String, object> DeserializationMetadata(object deserialized)
52+
{
53+
Dictionary<string, object> retval;
54+
if (cwt.TryGetValue(deserialized, out retval))
55+
{
56+
return retval;
57+
}
58+
else
59+
{
60+
//TODO: Throw an exception here? If you asked for metadata for an object and it's not found, something has probably gone pretty badly wrong!
61+
return null;
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Find whether or not a given property was
67+
/// posted in the original JSON--i.e. to determine whether an update operation should be
68+
/// performed, and/or if a default value should be used.
69+
/// </summary>
70+
/// <param name="deserialized">The object deserialized by JsonApiFormatter</param>
71+
/// <param name="prop">The property to check</param>
72+
/// <returns>Whether or not the property was found in the original JSON and set by the deserializer</returns>
73+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
74+
public bool PropertyWasPresent(object deserialized, PropertyInfo prop)
75+
{
76+
object throwaway;
77+
return this.DeserializationMetadata(deserialized).TryGetValue(prop.Name, out throwaway);
78+
}
79+
}
80+
}

JSONAPI/JSONAPI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
<Compile Include="Attributes\SerializeAs.cs" />
8282
<Compile Include="Core\IPluralizationService.cs" />
8383
<Compile Include="Core\IMaterializer.cs" />
84+
<Compile Include="Core\MetadataManager.cs" />
8485
<Compile Include="Http\ApiController.cs" />
8586
<Compile Include="Json\JsonApiFormatter.cs" />
8687
<Compile Include="Json\RelationAggregator.cs" />

JSONAPI/JSONAPI.nuspec

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package >
33
<metadata>
44
<id>$id$</id>
5-
<version>0.1.0.0</version>
5+
<version>0.2.0.0</version>
66
<title>JSONAPI.NET</title>
77
<authors>S&apos;pht&apos;Kr</authors>
88
<owners>S&apos;pht&apos;Kr</owners>
@@ -11,7 +11,11 @@
1111
<iconUrl>http://download-codeplex.sec.s-msft.com/Download?ProjectName=jsonapi&amp;DownloadId=939025</iconUrl>
1212
<requireLicenseAcceptance>false</requireLicenseAcceptance>
1313
<description>A toolkit for using WebAPI and (optionally) EntityFramework to quickly build REST services complying with the JSON API spec (jsonapi.org).</description>
14-
<releaseNotes>First release, v0.1a.</releaseNotes>
14+
<releaseNotes>
15+
Implemented an important part of the spec, "Attributes omitted from the resource object should not be updated." This requires us to be able to communicate to the IMaterializer (which does actual updating) that attributes were or were not specified on the original JSON resource object. Therefore, we created the new MetadataManager singleton, and its public method PropertyWasPresent(object deserialized, PropertyInfo prop). It is tightly coupled with JsonApiFormatter, but loosely with an IMaterializer such as EntityFrameworkMaterializer.
16+
17+
The ability to check for the presence of a specified value in the JSON also makes it possible to use default values on a POSTed object--before we could not tell if there was underposting going on!
18+
</releaseNotes>
1519
<copyright>Copyright 2014</copyright>
1620
<tags>JSON WebAPI REST ember emberjs ember.js ember-data</tags>
1721
</metadata>

JSONAPI/Json/JsonApiFormatter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,9 @@ public object Deserialize(Type objectType, Stream readStream, JsonReader reader,
543543

544544
prop.SetValue(retval, DeserializePrimitive(prop.PropertyType, reader), null);
545545

546+
// Tell the MetadataManager that we deserialized this property
547+
MetadataManager.Instance.SetMetaForProperty(retval, prop, true);
548+
546549
// pop the value off the reader, so we catch the EndObject token below!.
547550
reader.Read();
548551
}
@@ -689,6 +692,9 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade
689692

690693
prop.SetValue(obj, GetById(relType, (string)reader.Value));
691694
}
695+
696+
// Tell the MetadataManager that we deserialized this property
697+
MetadataManager.Instance.SetMetaForProperty(obj, prop, true);
692698
}
693699
else
694700
reader.Skip();

0 commit comments

Comments
 (0)