From ae91f4e3ec5952d9e2c141af95bc2a1dd34bbac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=BChlmeyer?= Date: Wed, 11 Dec 2019 15:39:15 +0100 Subject: [PATCH 1/2] Fixing csproj file. --- WebApi.Hal/WebApi.Hal.csproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/WebApi.Hal/WebApi.Hal.csproj b/WebApi.Hal/WebApi.Hal.csproj index e739381..9d4826f 100644 --- a/WebApi.Hal/WebApi.Hal.csproj +++ b/WebApi.Hal/WebApi.Hal.csproj @@ -5,14 +5,12 @@ netstandard2.0 3.1.0 -+ 3.1.0 + 3.1.0 Copyright © Jake Ginnivan 2018 Adds support for the Hal Media Type (and Hypermedia) to Asp.net https://github.com/JakeGinnivan/WebApi.Hal true - 3.10 updates to support multithreaded usage -+ 3.0.0 first .net standard release -+ + 3.10 updates to support multithreaded usage From eca47222aebecc804449876d5b5ab267361b5620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=BChlmeyer?= Date: Wed, 11 Dec 2019 17:18:06 +0100 Subject: [PATCH 2/2] Issue #153 (https://github.com/JakeGinnivan/WebApi.Hal/issues/153): Support for rendering a multi-link link relation always as an array. 1.) Link class has a new property "IsMultiLink". If true, then the link rel is always rendered as a JSON array, even if there's only one actual link. 2.) When having an enumeration of IResource as property (to represent a list of embedded resources), this also defines the link rel as multi-link link rel, meaning that the links for those embedded resources are also rendered as a JSON array even if there's only one element. 3.) When parsing JSON, the IsMultiLink flag is set depending on the JSON token for a link rel (true for a JSON array, false for a JSON object) 4.) Added unit tests for new functionality. Additionally, the changes result in a different behavior for one unit test (one_item_organisation_list_get_json_test), which was adapted to the new behavior. --- .gitignore | 1 + ...ganisation_list_get_json_test.approved.txt | 8 +- WebApi.Hal.Tests/HalResourceTest.cs | 152 +++++++++++++++++- ...json_with_multi_link_embedded.approved.txt | 35 ++++ ..._json_with_multi_link_linkrel.approved.txt | 31 ++++ WebApi.Hal/JsonConverters/LinksConverter.cs | 6 +- .../JsonConverters/ResourceConverter.cs | 1 + WebApi.Hal/Link.cs | 1 + WebApi.Hal/Representation.cs | 3 + WebApi.Hal/WebApi.Hal.csproj | 6 +- 10 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_embedded.approved.txt create mode 100644 WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_linkrel.approved.txt diff --git a/.gitignore b/.gitignore index ee2d5a3..07551fd 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,4 @@ WebApi.Hal*.nupkg .vscode/ .DS_Store *~ +*.bak diff --git a/WebApi.Hal.Tests/HalResourceListTests.one_item_organisation_list_get_json_test.approved.txt b/WebApi.Hal.Tests/HalResourceListTests.one_item_organisation_list_get_json_test.approved.txt index cef92db..e8aecd8 100644 --- a/WebApi.Hal.Tests/HalResourceListTests.one_item_organisation_list_get_json_test.approved.txt +++ b/WebApi.Hal.Tests/HalResourceListTests.one_item_organisation_list_get_json_test.approved.txt @@ -3,9 +3,11 @@ "self": { "href": "/api/organisations" }, - "organisation": { - "href": "/api/organisations/1" - } + "organisation": [ + { + "href": "/api/organisations/1" + } + ] }, "_embedded": { "organisation": [ diff --git a/WebApi.Hal.Tests/HalResourceTest.cs b/WebApi.Hal.Tests/HalResourceTest.cs index 7f5828b..d8c8670 100644 --- a/WebApi.Hal.Tests/HalResourceTest.cs +++ b/WebApi.Hal.Tests/HalResourceTest.cs @@ -1,7 +1,14 @@ -using System.Buffers; +using System; +using System.Buffers; using System.IO; +using System.Linq; using Assent; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using WebApi.Hal.Tests.Representations; using Xunit; @@ -112,5 +119,148 @@ public void organisation_get_xml_test() this.Assent(serialisedResult); } } + + [Fact] + public void organisation_get_json_with_multi_link_linkrel() + { + // arrange + var mediaFormatter = new JsonHalMediaTypeOutputFormatter( + new JsonSerializerSettings { Formatting = Formatting.Indented }, ArrayPool.Shared); + var resourceWithAppPath = new OrganisationWithLinkTitleRepresentation(1, "Org Name"); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-single-link", "~/api/organisations/test") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links", "~/api/organisations/test1") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links", "~/api/organisations/test2") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links-with-is-multilink-false", "~/api/organisations/test-f1")); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links-with-is-multilink-false", "~/api/organisations/test-f2")); + + // act + using (var stream = new StringWriter()) + { + mediaFormatter.WriteObject(stream, resourceWithAppPath); + + string serialisedResult = stream.ToString(); + + // assert + this.Assent(serialisedResult); + } + } + + public class OrganisationRepresentationWithEmbeddedResources : OrganisationWithAppPathRepresentation + { + public OrganisationRepresentationWithEmbeddedResources(int id, string name) : base(id, name) + { + } + + public TestRepresentation[] EmbeddedMultiResource { get; set; } + + public TestRepresentation EmbeddedSingleResource { get; set; } + } + + public class TestRepresentation : Representation + { + } + + [Fact] + public void organisation_get_json_with_multi_link_embedded() + { + // arrange + var mediaFormatter = new JsonHalMediaTypeOutputFormatter( + new JsonSerializerSettings { Formatting = Formatting.Indented }, ArrayPool.Shared); + var org = new OrganisationRepresentationWithEmbeddedResources(1, "Org Name"); + + org.EmbeddedSingleResource = new TestRepresentation + { + Rel = "single-resource", + Href = "~/single" + }; + org.EmbeddedMultiResource = new[] { new TestRepresentation + { + Rel = "multi-resource", + Href = "~/multi" + } }; + + // act + using (var stream = new StringWriter()) + { + mediaFormatter.WriteObject(stream, org); + + string serialisedResult = stream.ToString(); + + // assert + this.Assent(serialisedResult); + } + } + + private class JsonHalMediaTypeInputFormatterWithCreateSerializer : JsonHalMediaTypeInputFormatter + { + public JsonHalMediaTypeInputFormatterWithCreateSerializer(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool charPool, ObjectPoolProvider objectPoolProvider, MvcOptions mvcOptions, MvcJsonOptions mvcJsonOptions) + : base(logger, serializerSettings, charPool, objectPoolProvider, mvcOptions, mvcJsonOptions) + { + } + + public new JsonSerializer CreateJsonSerializer() + { + return base.CreateJsonSerializer(); + } + } + + [Fact] + public void organisation_parse_json_with_multi_link_linkrel() + { + // arrange + var mediaFormatter = new JsonHalMediaTypeOutputFormatter( + new JsonSerializerSettings { Formatting = Formatting.Indented }, ArrayPool.Shared); + var resourceWithAppPath = new OrganisationWithLinkTitleRepresentation(1, "Org Name"); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-single-link", "~/api/organisations/test") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links", "~/api/organisations/test1") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links", "~/api/organisations/test2") + { + IsMultiLink = true + }); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links-with-is-multilink-false", "~/api/organisations/test1")); + resourceWithAppPath.Links.Add(new Link("multi-rel-with-multiple-links-with-is-multilink-false", "~/api/organisations/test2")); + + string serialisedResource; + // serialize + using (var stream = new StringWriter()) + { + mediaFormatter.WriteObject(stream, resourceWithAppPath); + + serialisedResource = stream.ToString(); + } + + // parse again + var inputFormatter = new JsonHalMediaTypeInputFormatterWithCreateSerializer( + NullLogger.Instance, + new JsonSerializerSettings { Formatting = Formatting.Indented }, ArrayPool.Shared, + new DefaultObjectPoolProvider(), new MvcOptions(), new MvcJsonOptions()); + var inputSerializer = inputFormatter.CreateJsonSerializer(); + using (var stream = new StringReader(serialisedResource)) + { + using (var jsonReader = new JsonTextReader(stream)) + { + var parsedResource = inputSerializer.Deserialize(jsonReader); + Assert.True(parsedResource.Links.Where(l => l.Rel == "multi-rel-with-single-link").All(l => l.IsMultiLink)); + Assert.True(parsedResource.Links.Where(l => l.Rel == "multi-rel-with-multiple-links").All(l => l.IsMultiLink)); + Assert.True(parsedResource.Links.Where(l => l.Rel == "multi-rel-with-multiple-links-with-is-multilink-false").All(l => l.IsMultiLink)); + Assert.True(parsedResource.Links.Where(l => l.Rel == "someRel").All(l => !l.IsMultiLink)); + } + } + } } } \ No newline at end of file diff --git a/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_embedded.approved.txt b/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_embedded.approved.txt new file mode 100644 index 0000000..4ba3a3c --- /dev/null +++ b/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_embedded.approved.txt @@ -0,0 +1,35 @@ +{ + "Id": 1, + "Name": "Org Name", + "_links": { + "self": { + "href": "/api/organisations/1" + }, + "multi-resource": [ + { + "href": "/multi" + } + ], + "single-resource": { + "href": "/single" + } + }, + "_embedded": { + "multi-resource": [ + { + "_links": { + "self": { + "href": "/multi" + } + } + } + ], + "single-resource": { + "_links": { + "self": { + "href": "/single" + } + } + } + } +} \ No newline at end of file diff --git a/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_linkrel.approved.txt b/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_linkrel.approved.txt new file mode 100644 index 0000000..b81b922 --- /dev/null +++ b/WebApi.Hal.Tests/HalResourceTest.organisation_get_json_with_multi_link_linkrel.approved.txt @@ -0,0 +1,31 @@ +{ + "Id": 1, + "Name": "Org Name", + "_links": { + "multi-rel-with-single-link": [ + { + "href": "/api/organisations/test" + } + ], + "multi-rel-with-multiple-links": [ + { + "href": "/api/organisations/test1" + }, + { + "href": "/api/organisations/test2" + } + ], + "multi-rel-with-multiple-links-with-is-multilink-false": [ + { + "href": "/api/organisations/test-f1" + }, + { + "href": "/api/organisations/test-f2" + } + ], + "somerel": { + "href": "someHref", + "title": "someTitle" + } + } +} \ No newline at end of file diff --git a/WebApi.Hal/JsonConverters/LinksConverter.cs b/WebApi.Hal/JsonConverters/LinksConverter.cs index 084572e..1bdc2a4 100644 --- a/WebApi.Hal/JsonConverters/LinksConverter.cs +++ b/WebApi.Hal/JsonConverters/LinksConverter.cs @@ -24,13 +24,15 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s writer.WritePropertyName(rel.Key); - if ((count > 1) || (rel.Key == Link.RelForCuries)) + bool serializeAsArray = (count > 1) || (rel.Any(l => l.IsMultiLink)) || (rel.Key == Link.RelForCuries); + + if (serializeAsArray) writer.WriteStartArray(); foreach (var link in rel) WriteLink(writer, link); - if ((count > 1) || (rel.Key == Link.RelForCuries)) + if ((count > 1) || (rel.Any(l => l.IsMultiLink)) || (rel.Key == Link.RelForCuries)) writer.WriteEndArray(); } diff --git a/WebApi.Hal/JsonConverters/ResourceConverter.cs b/WebApi.Hal/JsonConverters/ResourceConverter.cs index 5970b69..269d861 100644 --- a/WebApi.Hal/JsonConverters/ResourceConverter.cs +++ b/WebApi.Hal/JsonConverters/ResourceConverter.cs @@ -161,6 +161,7 @@ static void CreateLinks(JProperty rel, IResource resource) foreach (var link in arr.Select(item => item.ToObject())) { link.Rel = rel.Name; + link.IsMultiLink = true; resource.Links.Add(link); } } diff --git a/WebApi.Hal/Link.cs b/WebApi.Hal/Link.cs index d22deec..714b25e 100644 --- a/WebApi.Hal/Link.cs +++ b/WebApi.Hal/Link.cs @@ -76,6 +76,7 @@ public string Rel public string Name { get; set; } public string Profile { get; set; } public string HrefLang { get; set; } + public bool IsMultiLink { get; set; } public bool IsTemplated => !string.IsNullOrEmpty(Href) && isTemplatedRegex.IsMatch(Href); diff --git a/WebApi.Hal/Representation.cs b/WebApi.Hal/Representation.cs index e5cdc6d..65d9848 100644 --- a/WebApi.Hal/Representation.cs +++ b/WebApi.Hal/Representation.cs @@ -146,7 +146,10 @@ private void ProcessPropertyValue(IHypermediaResolver resolver, List var link = representation.ToLink(resolver); if (link != null) + { + link.IsMultiLink = true; Links.Add(link); // add a link to embedded to the container ... + } } Embedded.Add(embeddedResource); diff --git a/WebApi.Hal/WebApi.Hal.csproj b/WebApi.Hal/WebApi.Hal.csproj index 9d4826f..254c5ea 100644 --- a/WebApi.Hal/WebApi.Hal.csproj +++ b/WebApi.Hal/WebApi.Hal.csproj @@ -4,13 +4,13 @@ netstandard2.0 - 3.1.0 - 3.1.0 + 3.2.0 + 3.2.0 Copyright © Jake Ginnivan 2018 Adds support for the Hal Media Type (and Hypermedia) to Asp.net https://github.com/JakeGinnivan/WebApi.Hal true - 3.10 updates to support multithreaded usage + 3.2.0 ability to mark a link-rel as multi-link to ensure that it always serialzies to an array, even if there's only one link at runtime