Skip to content

Commit 99b226f

Browse files
committed
added support for resource without links (issue #98)
1 parent 89a2a94 commit 99b226f

6 files changed

+131
-68
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ ipch/
4343
*.opensdf
4444
*.sdf
4545

46+
# Visual Studio
47+
.vs/
4648
# Visual Studio profiler
4749
*.psess
4850
*.vsp
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
22
"Id": 1,
3-
"Name": "Org Name",
4-
"_links": null
3+
"Name": "Org Name"
54
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Id": 9,
3+
"Title": "Morpheus in a chair statuette",
4+
"Price": 20.14,
5+
"_embedded": {
6+
"unknownRel-CategoryRepresentation": {
7+
"Title": "Action Figures",
8+
"_links": null
9+
}
10+
}
11+
}

WebApi.Hal.Tests/ResolvingHalResourceTest.cs

+42-24
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ namespace WebApi.Hal.Tests
1111
public class ResolvingHalResourceTest
1212
{
1313
readonly ProductRepresentation representation;
14-
readonly IHypermediaResolver config;
1514

1615
public ResolvingHalResourceTest()
1716
{
@@ -29,34 +28,30 @@ public ResolvingHalResourceTest()
2928
Title = "Action Figures"
3029
}
3130
};
32-
33-
//
34-
// Build hypermedia configuration
35-
36-
var example = new CuriesLink("example-namespace", "http://api.example.com/docs/{rel}");
37-
var productLink = example.CreateLink("product", "http://api.example.com/products/{id}");
38-
var relatedProductLink = example.CreateLink("related-product", productLink.Href);
39-
var saleProductLink = example.CreateLink("product-on-sale", "http://api.example.com/products/sale/{id}");
40-
var categoryLink = example.CreateLink("category", "http://api.example.com/categories/{id}");
41-
42-
var builder = Hypermedia.CreateBuilder();
43-
44-
builder.RegisterAppender(new ProductRepresentationHypermediaAppender());
45-
builder.RegisterAppender(new CategoryRepresentationHypermediaAppender());
46-
47-
builder.RegisterSelf<ProductRepresentation>(productLink);
48-
builder.RegisterSelf<CategoryRepresentation>(categoryLink);
49-
builder.RegisterLinks<ProductRepresentation>(relatedProductLink, saleProductLink);
50-
51-
config = builder.Build();
5231
}
5332

5433
[Fact]
5534
[UseReporter(typeof(DiffReporter))]
5635
public void ProperlySerializesRepresentationToJson()
5736
{
58-
// arrange
59-
var mediaFormatter = new JsonHalMediaTypeFormatter(config) { Indent = true };
37+
// arrange
38+
var example = new CuriesLink("example-namespace", "http://api.example.com/docs/{rel}");
39+
var productLink = example.CreateLink("product", "http://api.example.com/products/{id}");
40+
var relatedProductLink = example.CreateLink("related-product", productLink.Href);
41+
var saleProductLink = example.CreateLink("product-on-sale", "http://api.example.com/products/sale/{id}");
42+
var categoryLink = example.CreateLink("category", "http://api.example.com/categories/{id}");
43+
44+
var builder = Hypermedia.CreateBuilder();
45+
46+
builder.RegisterAppender(new ProductRepresentationHypermediaAppender());
47+
builder.RegisterAppender(new CategoryRepresentationHypermediaAppender());
48+
49+
builder.RegisterSelf<ProductRepresentation>(productLink);
50+
builder.RegisterSelf<CategoryRepresentation>(categoryLink);
51+
builder.RegisterLinks<ProductRepresentation>(relatedProductLink, saleProductLink);
52+
53+
var config = builder.Build();
54+
var mediaFormatter = new JsonHalMediaTypeFormatter(config) { Indent = true };
6055
var content = new StringContent(string.Empty);
6156
var type = representation.GetType();
6257

@@ -71,5 +66,28 @@ public void ProperlySerializesRepresentationToJson()
7166
Approvals.Verify(serialisedResult);
7267
}
7368
}
74-
}
69+
70+
[Fact]
71+
[UseReporter(typeof(DiffReporter))]
72+
public void ProperlySerializesRepresentationWithoutLinksToJson()
73+
{
74+
// arrange
75+
var builder = Hypermedia.CreateBuilder();
76+
var config = builder.Build();
77+
var mediaFormatter = new JsonHalMediaTypeFormatter(config) { Indent = true };
78+
var content = new StringContent(string.Empty);
79+
var type = representation.GetType();
80+
81+
// act
82+
using (var stream = new MemoryStream())
83+
{
84+
mediaFormatter.WriteToStreamAsync(type, representation, stream, content, null);
85+
stream.Seek(0, SeekOrigin.Begin);
86+
var serialisedResult = new StreamReader(stream).ReadToEnd();
87+
88+
// assert
89+
Approvals.Verify(serialisedResult);
90+
}
91+
}
92+
}
7593
}

WebApi.Hal/JsonConverters/ResourceConverter.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,21 @@ private StreamingContext GetResourceConverterContext()
4545
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
4646
{
4747
var resource = (IResource)value;
48+
var linksBackup = resource.Links;
4849

49-
var saveContext = serializer.Context;
50+
if (!linksBackup.Any())
51+
resource.Links = null; // avoid serialization
52+
53+
var saveContext = serializer.Context;
5054
serializer.Context = GetResourceConverterContext();
5155
serializer.Converters.Remove(this);
5256
serializer.Serialize(writer, resource);
5357
serializer.Converters.Add(this);
5458
serializer.Context = saveContext;
55-
}
59+
60+
if (!linksBackup.Any())
61+
resource.Links = linksBackup;
62+
}
5663

5764
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
5865
JsonSerializer serializer)

WebApi.Hal/Representation.cs

+66-40
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ private void OnSerialize(StreamingContext context)
4141

4242
RepopulateRecursively(ctx.HypermediaResolver, curies);
4343

44-
Links = curies
45-
.Distinct(CuriesLink.EqualityComparer)
46-
.Select(x => x.ToLink())
47-
.Union(Links)
48-
.ToList();
44+
if (Links != null)
45+
Links = curies
46+
.Distinct(CuriesLink.EqualityComparer)
47+
.Select(x => x.ToLink())
48+
.Union(Links)
49+
.ToList();
4950

5051
ctx.IsRoot = false;
5152
}
@@ -123,7 +124,8 @@ private void RepopulateRecursively(IHypermediaResolver resolver, List<CuriesLink
123124
property.SetValue(this, null, null);
124125
}
125126

126-
curies.AddRange(Links.Where(l => l.Curie != null).Select(l => l.Curie));
127+
if (Links != null)
128+
curies.AddRange(Links.Where(l => l.Curie != null).Select(l => l.Curie));
127129

128130
if (Embedded.Count == 0)
129131
Embedded = null; // avoid the property from being serialized ...
@@ -138,39 +140,45 @@ void ProcessPropertyValue(IHypermediaResolver resolver, List<CuriesLink> curies,
138140

139141
var embeddedResource = new EmbeddedResource {IsSourceAnArray = true};
140142

141-
foreach (var resourceItem in resourceList)
142-
{
143-
embeddedResource.Resources.Add(resourceItem);
143+
foreach (var resourceItem in resourceList)
144+
{
145+
embeddedResource.Resources.Add(resourceItem);
144146

145-
var representation = resourceItem as Representation;
147+
var representation = resourceItem as Representation;
146148

147-
if (representation == null)
148-
continue;
149+
if (representation == null)
150+
continue;
149151

150-
representation.RepopulateRecursively(resolver, curies); // traverse ...
151-
Links.Add(representation.ToLink(resolver)); // add a link to embedded to the container ...
152-
}
152+
representation.RepopulateRecursively(resolver, curies); // traverse ...
153+
var link = representation.ToLink(resolver);
154+
155+
if (link != null)
156+
Links.Add(link); // add a link to embedded to the container ...
157+
}
153158

154-
Embedded.Add(embeddedResource);
159+
Embedded.Add(embeddedResource);
155160
}
156161

157-
void ProcessPropertyValue(IHypermediaResolver resolver, List<CuriesLink> curies, IResource resource)
158-
{
159-
var embeddedResource = new EmbeddedResource {IsSourceAnArray = false};
160-
embeddedResource.Resources.Add(resource);
162+
void ProcessPropertyValue(IHypermediaResolver resolver, List<CuriesLink> curies, IResource resource)
163+
{
164+
var embeddedResource = new EmbeddedResource {IsSourceAnArray = false};
165+
embeddedResource.Resources.Add(resource);
161166

162-
Embedded.Add(embeddedResource);
167+
Embedded.Add(embeddedResource);
163168

164-
var representation = resource as Representation;
169+
var representation = resource as Representation;
165170

166-
if (representation == null)
167-
return;
171+
if (representation == null)
172+
return;
168173

169-
representation.RepopulateRecursively(resolver, curies); // traverse ...
170-
Links.Add(representation.ToLink(resolver)); // add a link to embedded to the container ...
171-
}
174+
representation.RepopulateRecursively(resolver, curies); // traverse ...
175+
var link = representation.ToLink(resolver);
176+
177+
if (link != null)
178+
Links.Add(link); // add a link to embedded to the container ...
179+
}
172180

173-
void ResolveAndAppend(IHypermediaResolver resolver, Type type)
181+
void ResolveAndAppend(IHypermediaResolver resolver, Type type)
174182
{
175183
// We need reflection here, because appenders are of type IHypermediaAppender<T> whilst we define this logic in the base class of T
176184

@@ -180,31 +188,49 @@ void ResolveAndAppend(IHypermediaResolver resolver, Type type)
180188
genericMethod.Invoke(this, new object[] {this, resolver});
181189
}
182190

183-
protected static void Append<T>(IResource resource, IHypermediaResolver resolver) where T : class, IResource // called using reflection ...
184-
{
185-
var typed = resource as T;
191+
protected static void Append<T>(IResource resource, IHypermediaResolver resolver) where T : class, IResource
192+
// called using reflection ...
193+
{
194+
var typed = resource as T;
186195

187-
var appender = resolver.ResolveAppender(typed);
188-
var configured = resolver.ResolveLinks(typed).ToList();
196+
var appender = resolver.ResolveAppender(typed);
197+
var configured = resolver.ResolveLinks(typed).ToList();
198+
var link = resolver.ResolveSelf(typed);
189199

190-
configured.Insert(0, resolver.ResolveSelf(typed));
200+
if (link != null)
201+
configured.Insert(0, link);
191202

192-
appender.Append(typed, configured);
193-
}
203+
if (configured.Any() && (appender != null))
204+
{
205+
if (typed.Links == null)
206+
typed.Links = new List<Link>(); // make sure resource.Links.Add() can safely be called inside the appender
207+
208+
appender.Append(typed, configured);
194209

195-
internal static bool IsEmbeddedResourceType(Type type)
210+
if ((typed.Links != null) && !typed.Links.Any())
211+
typed.Links = null; // prevent _links property serialisation
212+
}
213+
}
214+
215+
internal static bool IsEmbeddedResourceType(Type type)
196216
{
197217
return typeof (IResource).IsAssignableFrom(type) ||
198218
typeof (IEnumerable<IResource>).IsAssignableFrom(type);
199219
}
200220

201221
public void RepopulateHyperMedia()
202222
{
203-
CreateHypermedia();
223+
if (Links == null)
224+
Links = new List<Link>(); // make sure resource.Links.Add() can safely be called inside the overload
204225

205-
if (!string.IsNullOrEmpty(Href) && Links.Count(l=>l.Rel == "self") == 0)
226+
CreateHypermedia();
227+
228+
if (!string.IsNullOrEmpty(Href) && Links.Count(l=>l.Rel == "self") == 0)
206229
Links.Insert(0, new Link { Rel = "self", Href = Href });
207-
}
230+
231+
if ((Links != null) && !Links.Any())
232+
Links = null; // prevent _links property serialisation
233+
}
208234

209235
[JsonIgnore]
210236
public virtual string Rel { get; set; }

0 commit comments

Comments
 (0)