From b8f4a4195fa9f9430d4a0300318fbe6507432e34 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 12:16:32 -0500 Subject: [PATCH 001/186] add pascal-case controller selector --- .../Http/PascalizedControllerSelectorTests.cs | 74 +++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 5 ++ JSONAPI.Tests/packages.config | 1 + JSONAPI/Extensions/StringExtensions.cs | 16 ++++ JSONAPI/Http/PascalizedControllerSelector.cs | 24 ++++++ JSONAPI/JSONAPI.csproj | 2 + 6 files changed, 122 insertions(+) create mode 100644 JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs create mode 100644 JSONAPI/Extensions/StringExtensions.cs create mode 100644 JSONAPI/Http/PascalizedControllerSelector.cs diff --git a/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs b/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs new file mode 100644 index 00000000..1c28e78b --- /dev/null +++ b/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Routing; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class PascalizedControllerSelectorTests + { + [TestMethod] + public void Camelizes_controller_name_without_dashes() + { + TestForDefaultString("foo", "Foo"); + } + + [TestMethod] + public void Camelizes_controller_name_with_underscores() + { + TestForDefaultString("foo_bar_baz", "FooBarBaz"); + } + + [TestMethod] + public void Camelizes_controller_name_with_dots() + { + TestForDefaultString("foo.bar.baz", "FooBarBaz"); + } + + [TestMethod] + public void Camelizes_controller_name_with_dashes() + { + TestForDefaultString("foo-bar-baz", "FooBarBaz"); + } + + [TestMethod] + public void Camelizes_controller_name_with_all_three() + { + TestForDefaultString("foo.bar-baz_qux", "FooBarBazQux"); + } + + private void TestForDefaultString(string defaultString, string expectedTransformation) + { + // Arrange + var routeDataDict = new Dictionary + { + {"controller", defaultString} + }; + + var mockRouteData = new Mock(MockBehavior.Strict); + mockRouteData.Setup(m => m.Values).Returns(routeDataDict); + + var mockRequestContext = new Mock(MockBehavior.Strict); + mockRequestContext.Setup(m => m.RouteData).Returns(mockRouteData.Object); + + var request = new HttpRequestMessage(); + request.SetRequestContext(mockRequestContext.Object); + + var mockHttpConfig = new Mock(MockBehavior.Strict); + + var selector = new PascalizedControllerSelector(mockHttpConfig.Object); + + // Act + var actual = selector.GetControllerName(request); + + // Assert + actual.Should().Be(expectedTransformation); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 7242fe08..06de30f2 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -45,6 +45,10 @@ ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll + + False + ..\packages\Moq.4.2.1502.0911\lib\net40\Moq.dll + False ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll @@ -79,6 +83,7 @@ + diff --git a/JSONAPI.Tests/packages.config b/JSONAPI.Tests/packages.config index be589f3d..ac30f5f4 100644 --- a/JSONAPI.Tests/packages.config +++ b/JSONAPI.Tests/packages.config @@ -4,5 +4,6 @@ + \ No newline at end of file diff --git a/JSONAPI/Extensions/StringExtensions.cs b/JSONAPI/Extensions/StringExtensions.cs new file mode 100644 index 00000000..8b7893d6 --- /dev/null +++ b/JSONAPI/Extensions/StringExtensions.cs @@ -0,0 +1,16 @@ +using System.Text.RegularExpressions; + +namespace JSONAPI.Extensions +{ + internal static class StringExtensions + { + private static readonly Regex PascalizeRegex = new Regex(@"(?:^|_|\-|\.)(.)"); + + public static string Pascalize(this string word) + { + return PascalizeRegex.Replace( + word, + match => match.Groups[1].Value.ToUpper()); + } + } +} diff --git a/JSONAPI/Http/PascalizedControllerSelector.cs b/JSONAPI/Http/PascalizedControllerSelector.cs new file mode 100644 index 00000000..3c4f3c34 --- /dev/null +++ b/JSONAPI/Http/PascalizedControllerSelector.cs @@ -0,0 +1,24 @@ +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Dispatcher; +using JSONAPI.Extensions; + +namespace JSONAPI.Http +{ + /// + /// Chooses a controller based on the pascal-case version of the default controller name + /// + public class PascalizedControllerSelector : DefaultHttpControllerSelector + { + /// The configuration to use + public PascalizedControllerSelector(HttpConfiguration configuration) : base(configuration) + { + } + + public override string GetControllerName(HttpRequestMessage request) + { + var baseControllerName = base.GetControllerName(request); + return baseControllerName.Pascalize(); + } + } +} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index e4815238..1f4e8afa 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -77,8 +77,10 @@ + + From 80f74fd0fa0d685b213a071245e9a5b7b31240cb Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 12:47:18 -0500 Subject: [PATCH 002/186] add acceptance tests for multi-part model name --- .../Controllers/UserGroupsController.cs | 21 +++++++++++++++++++ ...PI.EntityFramework.Tests.TestWebApp.csproj | 2 ++ .../Models/User.cs | 2 ++ .../Models/UserGroup.cs | 15 +++++++++++++ .../Startup.cs | 5 +++++ .../Acceptance/Data/UserGroup.csv | 2 ++ .../Fixtures/UserGroups_GetResponse.json | 11 ++++++++++ .../Acceptance/UserGroupsTests.cs | 19 +++++++++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 5 +++++ 9 files changed, 82 insertions(+) create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs new file mode 100644 index 00000000..4fafe7c2 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs @@ -0,0 +1,21 @@ +using JSONAPI.Core; +using JSONAPI.EntityFramework.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class UserGroupsController : ApiController + { + protected readonly TestDbContext DbContext; + + public UserGroupsController(TestDbContext dbContext) + { + DbContext = dbContext; + } + + protected override IMaterializer MaterializerFactory() + { + return new EntityFrameworkMaterializer(DbContext); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index b4e0f039..e7b67ffb 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -116,6 +116,7 @@ + @@ -124,6 +125,7 @@ + diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs index 5c80a9f1..b2e4034f 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs @@ -13,5 +13,7 @@ public class User public virtual ICollection Comments { get; set; } public virtual ICollection Posts { get; set; } + + public virtual ICollection UserGroups { get; set; } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs new file mode 100644 index 00000000..9d523f66 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public class UserGroup + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public virtual ICollection Users { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index 6dc55d2d..3508fd97 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -3,12 +3,14 @@ using System.Reflection; using System.Web; using System.Web.Http; +using System.Web.Http.Dispatcher; using Autofac; using Autofac.Integration.WebApi; using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; using JSONAPI.Json; using Microsoft.Owin; using Owin; @@ -73,6 +75,9 @@ private static HttpConfiguration GetWebApiConfiguration() config.Filters.Add(new EnumerateQueryableAsyncAttribute()); config.Filters.Add(new EnableFilteringAttribute(modelManager)); + // Override controller selector + config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); + // Web API routes config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv b/JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv new file mode 100644 index 00000000..30ab4160 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv @@ -0,0 +1,2 @@ +Id,Name +"501","Admin users" diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json new file mode 100644 index 00000000..122b7db6 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json @@ -0,0 +1,11 @@ +{ + "user-groups": [ + { + "id": "501", + "name": "Admin users", + "links": { + "users": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs new file mode 100644 index 00000000..8ba3ad6e --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class UserGroupsTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\UserGroup.csv", @"Acceptance\Data")] + public async Task Get() + { + using (var effortConnection = GetEffortConnection()) + { + await TestGet(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups_GetResponse.json"); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index f2593c70..b810e7ea 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -108,6 +108,7 @@ + @@ -126,6 +127,10 @@ + + + Always + Designer From 8c19671e7cef5c8eab65940e0e48fe2cbfa3df27 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 13:10:24 -0500 Subject: [PATCH 003/186] dasherize type names --- JSONAPI.Tests/Data/NonStandardIdTest.json | 2 +- JSONAPI/Core/ModelManager.cs | 3 ++- JSONAPI/Extensions/StringExtensions.cs | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json index 9acfc295..c84f2fcc 100644 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ b/JSONAPI.Tests/Data/NonStandardIdTest.json @@ -1,5 +1,5 @@ { - "nonStandardIdThings": [ + "non-standard-id-things": [ { "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", "uuid": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index a4cf9a97..2990545c 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using JSONAPI.Extensions; namespace JSONAPI.Core { @@ -148,7 +149,7 @@ public string GetJsonKeyForType(Type type) if (titles.Any()) title = titles.First(); } - key = FormatPropertyName(PluralizationService.Pluralize(title)); + key = FormatPropertyName(PluralizationService.Pluralize(title)).Dasherize(); keyCache.Add(type, key); } diff --git a/JSONAPI/Extensions/StringExtensions.cs b/JSONAPI/Extensions/StringExtensions.cs index 8b7893d6..89826301 100644 --- a/JSONAPI/Extensions/StringExtensions.cs +++ b/JSONAPI/Extensions/StringExtensions.cs @@ -12,5 +12,18 @@ public static string Pascalize(this string word) word, match => match.Groups[1].Value.ToUpper()); } + + public static string Depascalize(this string word) + { + return Regex.Replace( + Regex.Replace( + Regex.Replace(word, @"([A-Z]+)([A-Z][a-z])", "$1_$2"), @"([a-z\d])([A-Z])", + "$1_$2"), @"[-\s]", "_").ToLower(); + } + + public static string Dasherize(this string word) + { + return Depascalize(word).Replace('_', '-'); + } } } From 442d82a4c69ef13af6b089e967296f7ebb061211 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 13:16:05 -0500 Subject: [PATCH 004/186] use controller selector in sample project --- JSONAPI.TodoMVC.API/Startup.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index f2edebf4..c6a56ea1 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -1,7 +1,9 @@ using System.Web.Http; +using System.Web.Http.Dispatcher; using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.EntityFramework.ActionFilters; +using JSONAPI.Http; using JSONAPI.Json; using Owin; @@ -31,6 +33,9 @@ private static HttpConfiguration GetWebApiConfiguration() config.Filters.Add(new EnumerateQueryableAsyncAttribute()); config.Filters.Add(new EnableFilteringAttribute(modelManager)); + // Override controller selector + config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); + // Web API routes config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); From 1d92961dc25bd428d4f1ba42244c203f5475ef76 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 13:54:56 -0500 Subject: [PATCH 005/186] test SelectController instead of GetControllerName --- .../Http/PascalizedControllerSelectorTests.cs | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs b/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs index 1c28e78b..6fa032b5 100644 --- a/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs +++ b/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net.Http; using System.Web.Http; using System.Web.Http.Controllers; +using System.Web.Http.Dispatcher; using System.Web.Http.Routing; using FluentAssertions; using JSONAPI.Http; @@ -13,37 +15,65 @@ namespace JSONAPI.Tests.Http [TestClass] public class PascalizedControllerSelectorTests { + private class FooBarBazQuxController + { + + } + + private class TestHttpControllerTypeResolver : IHttpControllerTypeResolver + { + public ICollection GetControllerTypes(IAssembliesResolver assembliesResolver) + { + return new List + { + typeof (FooBarBazQuxController) + }; + } + } + [TestMethod] - public void Camelizes_controller_name_without_dashes() + public void Selects_controller_for_all_lower_case_name() { - TestForDefaultString("foo", "Foo"); + TestForDefaultString("foobarbazqux"); } [TestMethod] - public void Camelizes_controller_name_with_underscores() + public void Selects_controller_for_name_with_underscores() { - TestForDefaultString("foo_bar_baz", "FooBarBaz"); + TestForDefaultString("foo_bar_baz_qux"); } [TestMethod] - public void Camelizes_controller_name_with_dots() + public void Selects_controller_for_name_with_dots() { - TestForDefaultString("foo.bar.baz", "FooBarBaz"); + TestForDefaultString("foo.bar.baz.qux"); } [TestMethod] - public void Camelizes_controller_name_with_dashes() + public void Selects_controller_for_name_with_dashes() { - TestForDefaultString("foo-bar-baz", "FooBarBaz"); + TestForDefaultString("foo-bar-baz-qux"); } [TestMethod] - public void Camelizes_controller_name_with_all_three() + public void Selects_controller_for_name_with_all_three() { - TestForDefaultString("foo.bar-baz_qux", "FooBarBazQux"); + TestForDefaultString("foo.bar-baz_qux"); } - private void TestForDefaultString(string defaultString, string expectedTransformation) + [TestMethod] + public void Selects_controller_for_pascalized_name() + { + TestForDefaultString("FooBarBazQux"); + } + + [TestMethod] + public void Selects_controller_for_all_caps_name() + { + TestForDefaultString("FOOBARBAZQUX"); + } + + private void TestForDefaultString(string defaultString) { // Arrange var routeDataDict = new Dictionary @@ -52,23 +82,26 @@ private void TestForDefaultString(string defaultString, string expectedTransform }; var mockRouteData = new Mock(MockBehavior.Strict); + mockRouteData.Setup(m => m.Route).Returns((IHttpRoute)null); mockRouteData.Setup(m => m.Values).Returns(routeDataDict); + var httpConfig = new HttpConfiguration(); + httpConfig.Services.Replace(typeof(IHttpControllerTypeResolver), new TestHttpControllerTypeResolver()); + var mockRequestContext = new Mock(MockBehavior.Strict); + mockRequestContext.Setup(m => m.Configuration).Returns(httpConfig); mockRequestContext.Setup(m => m.RouteData).Returns(mockRouteData.Object); var request = new HttpRequestMessage(); request.SetRequestContext(mockRequestContext.Object); - var mockHttpConfig = new Mock(MockBehavior.Strict); - - var selector = new PascalizedControllerSelector(mockHttpConfig.Object); + var selector = new PascalizedControllerSelector(httpConfig); // Act - var actual = selector.GetControllerName(request); + var actual = selector.SelectController(request); // Assert - actual.Should().Be(expectedTransformation); + actual.ControllerType.Should().Be(typeof (FooBarBazQuxController)); } } } From 57553a44af48aaa5b992bfd07b5286a283399bb8 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 10 Feb 2015 14:02:48 -0500 Subject: [PATCH 006/186] assert correct dasherized form of pascal-cased type --- JSONAPI.Tests/Core/ModelManagerTests.cs | 2 ++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + JSONAPI.Tests/Models/UserGroup.cs | 8 ++++++++ 3 files changed, 11 insertions(+) create mode 100644 JSONAPI.Tests/Models/UserGroup.cs diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 3773454b..22cfe4d0 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -75,12 +75,14 @@ public void GetJsonKeyForTypeTest() var authorKey = mm.GetJsonKeyForType(typeof(Author)); var commentKey = mm.GetJsonKeyForType(typeof(Comment)); var manyCommentKey = mm.GetJsonKeyForType(typeof(Comment[])); + var userGroupsKey = mm.GetJsonKeyForType(typeof(UserGroup)); // Assert Assert.AreEqual("posts", postKey); Assert.AreEqual("authors", authorKey); Assert.AreEqual("comments", commentKey); Assert.AreEqual("comments", manyCommentKey); + Assert.AreEqual("user-groups", userGroupsKey); } [TestMethod] diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 06de30f2..5b0440ba 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -91,6 +91,7 @@ + diff --git a/JSONAPI.Tests/Models/UserGroup.cs b/JSONAPI.Tests/Models/UserGroup.cs new file mode 100644 index 00000000..ceed0d22 --- /dev/null +++ b/JSONAPI.Tests/Models/UserGroup.cs @@ -0,0 +1,8 @@ +namespace JSONAPI.Tests.Models +{ + class UserGroup + { + public int Id { get; set; } + public string Name { get; set; } + } +} From 73e76928cb6cf1a37ff202520f58b12b5a9d2ae0 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 12 Feb 2015 00:36:26 -0500 Subject: [PATCH 007/186] implement sorting --- .../Models/User.cs | 4 +- .../Startup.cs | 1 + .../Acceptance/AcceptanceTestsBase.cs | 37 +++- .../Acceptance/Data/User.csv | 15 +- .../Users_GetSortedAscendingResponse.json | 104 ++++++++++ ...ortedByColumnMissingDirectionResponse.json | 12 ++ ...ers_GetSortedByMixedDirectionResponse.json | 104 ++++++++++ ..._GetSortedByMultipleAscendingResponse.json | 104 ++++++++++ ...GetSortedByMultipleDescendingResponse.json | 104 ++++++++++ ...rs_GetSortedBySameColumnTwiceResponse.json | 12 ++ ...sers_GetSortedByUnknownColumnResponse.json | 12 ++ .../Users_GetSortedDescendingResponse.json | 104 ++++++++++ .../Acceptance/PostsTests.cs | 3 +- .../Acceptance/SortingTests.cs | 121 ++++++++++++ .../Acceptance/UserGroupsTests.cs | 2 +- .../JSONAPI.EntityFramework.Tests.csproj | 21 +- .../EnableSortingAttributeTests.cs | 181 ++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + JSONAPI.TodoMVC.API/Startup.cs | 1 + .../ActionFilters/EnableSortingAttribute.cs | 128 +++++++++++++ JSONAPI/JSONAPI.csproj | 1 + 21 files changed, 1052 insertions(+), 20 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs create mode 100644 JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs create mode 100644 JSONAPI/ActionFilters/EnableSortingAttribute.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs index b2e4034f..eca0edda 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs @@ -8,7 +8,9 @@ public class User [Key] public string Id { get; set; } - public string Name { get; set; } + public string FirstName { get; set; } + + public string LastName { get; set; } public virtual ICollection Comments { get; set; } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index 3508fd97..513796ab 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -73,6 +73,7 @@ private static HttpConfiguration GetWebApiConfiguration() // Global filters config.Filters.Add(new EnumerateQueryableAsyncAttribute()); + config.Filters.Add(new EnableSortingAttribute(modelManager)); config.Filters.Add(new EnableFilteringAttribute(modelManager)); // Override controller selector diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index e2b8e502..5f67a887 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentAssertions; using JSONAPI.EntityFramework.Tests.TestWebApp; @@ -16,6 +17,7 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance [TestClass] public abstract class AcceptanceTestsBase { + private static readonly Regex GuidRegex = new Regex(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase); private static readonly Uri BaseUri = new Uri("http://localhost"); protected static DbConnection GetEffortConnection() @@ -23,7 +25,33 @@ protected static DbConnection GetEffortConnection() return TestHelpers.GetEffortConnection(@"Acceptance\Data"); } - protected async Task TestGet(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) + protected async Task ExpectGetToSucceed(DbConnection effortConnection, string requestPath, + string expectedResponseTextResourcePath) + { + var response = await GetEndpointResponse(effortConnection, requestPath); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var responseContent = await response.Content.ReadAsStringAsync(); + + var expected = + JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + responseContent.Should().Be(expected); + } + + protected async Task ExpectGetToFail(DbConnection effortConnection, string requestPath, + string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest) + { + var expectedResponse = JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + var response = await GetEndpointResponse(effortConnection, requestPath); + response.StatusCode.Should().Be(expectedStatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); + + redactedResponse.Should().Be(expectedResponse); + } + + protected async Task GetEndpointResponse(DbConnection effortConnection, string requestPath) { using (var server = TestServer.Create(app => { @@ -33,12 +61,7 @@ protected async Task TestGet(DbConnection effortConnection, string requestPath, { var uri = new Uri(BaseUri, requestPath); var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); + return response; } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv b/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv index f21d3f36..d1c22ad6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv @@ -1,4 +1,11 @@ -Id,Name -"401","Alice" -"402","Bob" -"403","Charlie" \ No newline at end of file +Id,FirstName,LastName +"401","Alice","Smith" +"402","Bob","Jones" +"403","Charlie","Michaels" +"404","Richard","Smith" +"405","Michelle","Johnson" +"406","Ed","Burns" +"407","Thomas","Potter" +"408","Pat","Morgan" +"409","Charlie","Burns" +"410","Sally","Burns" \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json new file mode 100644 index 00000000..0d5100ab --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json @@ -0,0 +1,104 @@ +{ + "users": [ + { + "id": "401", + "firstName": "Alice", + "lastName": "Smith", + "links": { + "comments": [ "105" ], + "posts": [ "201", "202", "203" ], + "userGroups": [ ] + } + }, + { + "id": "402", + "firstName": "Bob", + "lastName": "Jones", + "links": { + "comments": [ "102" ], + "posts": [ "204" ], + "userGroups": [ ] + } + }, + { + "id": "403", + "firstName": "Charlie", + "lastName": "Michaels", + "links": { + "comments": [ "101", "103", "104" ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "409", + "firstName": "Charlie", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "406", + "firstName": "Ed", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "405", + "firstName": "Michelle", + "lastName": "Johnson", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "408", + "firstName": "Pat", + "lastName": "Morgan", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "404", + "firstName": "Richard", + "lastName": "Smith", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "410", + "firstName": "Sally", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "407", + "firstName": "Thomas", + "lastName": "Potter", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json new file mode 100644 index 00000000..24e832ab --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "The sort expression \"firstName\" does not begin with a direction indicator (+ or -).", + "detail": null, + "inner": null, + "stackTrace":null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json new file mode 100644 index 00000000..aa304398 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json @@ -0,0 +1,104 @@ +{ + "users": [ + { + "id": "410", + "firstName": "Sally", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "406", + "firstName": "Ed", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "409", + "firstName": "Charlie", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "405", + "firstName": "Michelle", + "lastName": "Johnson", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "402", + "firstName": "Bob", + "lastName": "Jones", + "links": { + "comments": [ "102" ], + "posts": [ "204" ], + "userGroups": [ ] + } + }, + { + "id": "403", + "firstName": "Charlie", + "lastName": "Michaels", + "links": { + "comments": [ "101", "103", "104" ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "408", + "firstName": "Pat", + "lastName": "Morgan", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "407", + "firstName": "Thomas", + "lastName": "Potter", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "404", + "firstName": "Richard", + "lastName": "Smith", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "401", + "firstName": "Alice", + "lastName": "Smith", + "links": { + "comments": [ "105" ], + "posts": [ "201", "202", "203" ], + "userGroups": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json new file mode 100644 index 00000000..5d9d2b8a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json @@ -0,0 +1,104 @@ +{ + "users": [ + { + "id": "409", + "firstName": "Charlie", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "406", + "firstName": "Ed", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "410", + "firstName": "Sally", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "405", + "firstName": "Michelle", + "lastName": "Johnson", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "402", + "firstName": "Bob", + "lastName": "Jones", + "links": { + "comments": [ "102" ], + "posts": [ "204" ], + "userGroups": [ ] + } + }, + { + "id": "403", + "firstName": "Charlie", + "lastName": "Michaels", + "links": { + "comments": [ "101", "103", "104" ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "408", + "firstName": "Pat", + "lastName": "Morgan", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "407", + "firstName": "Thomas", + "lastName": "Potter", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "401", + "firstName": "Alice", + "lastName": "Smith", + "links": { + "comments": [ "105" ], + "posts": [ "201", "202", "203" ], + "userGroups": [ ] + } + }, + { + "id": "404", + "firstName": "Richard", + "lastName": "Smith", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json new file mode 100644 index 00000000..98be56b2 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json @@ -0,0 +1,104 @@ +{ + "users": [ + { + "id": "404", + "firstName": "Richard", + "lastName": "Smith", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "401", + "firstName": "Alice", + "lastName": "Smith", + "links": { + "comments": [ "105" ], + "posts": [ "201", "202", "203" ], + "userGroups": [ ] + } + }, + { + "id": "407", + "firstName": "Thomas", + "lastName": "Potter", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "408", + "firstName": "Pat", + "lastName": "Morgan", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "403", + "firstName": "Charlie", + "lastName": "Michaels", + "links": { + "comments": [ "101", "103", "104" ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "402", + "firstName": "Bob", + "lastName": "Jones", + "links": { + "comments": [ "102" ], + "posts": [ "204" ], + "userGroups": [ ] + } + }, + { + "id": "405", + "firstName": "Michelle", + "lastName": "Johnson", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "410", + "firstName": "Sally", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "406", + "firstName": "Ed", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "409", + "firstName": "Charlie", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json new file mode 100644 index 00000000..5cd207cb --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "The attribute \"firstName\" was specified more than once.", + "detail": null, + "inner": null, + "stackTrace":null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json new file mode 100644 index 00000000..adf2ae49 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "The attribute \"foobar\" does not exist on type \"users\".", + "detail": null, + "inner": null, + "stackTrace":null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json new file mode 100644 index 00000000..c401ec97 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json @@ -0,0 +1,104 @@ +{ + "users": [ + { + "id": "407", + "firstName": "Thomas", + "lastName": "Potter", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "410", + "firstName": "Sally", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "404", + "firstName": "Richard", + "lastName": "Smith", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "408", + "firstName": "Pat", + "lastName": "Morgan", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "405", + "firstName": "Michelle", + "lastName": "Johnson", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "406", + "firstName": "Ed", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "403", + "firstName": "Charlie", + "lastName": "Michaels", + "links": { + "comments": [ "101", "103", "104" ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "409", + "firstName": "Charlie", + "lastName": "Burns", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "id": "402", + "firstName": "Bob", + "lastName": "Jones", + "links": { + "comments": [ "102" ], + "posts": [ "204" ], + "userGroups": [ ] + } + }, + { + "id": "401", + "firstName": "Alice", + "lastName": "Smith", + "links": { + "comments": [ "105" ], + "posts": [ "201", "202", "203" ], + "userGroups": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index e381f898..4dbdceaf 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Post = JSONAPI.EntityFramework.Tests.TestWebApp.Models.Post; namespace JSONAPI.EntityFramework.Tests.Acceptance { @@ -21,7 +20,7 @@ public async Task Get() { using (var effortConnection = GetEffortConnection()) { - await TestGet(effortConnection, "posts", @"Acceptance\Fixtures\Posts_GetResponse.json"); + await ExpectGetToSucceed(effortConnection, "posts", @"Acceptance\Fixtures\Posts_GetResponse.json"); } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs new file mode 100644 index 00000000..78264229 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs @@ -0,0 +1,121 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class SortingTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedAscending() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToSucceed(effortConnection, "users?sort=%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedAscendingResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedDesending() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToSucceed(effortConnection, "users?sort=-firstName", @"Acceptance\Fixtures\Users_GetSortedDescendingResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedByMultipleAscending() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedByMultipleAscendingResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedByMultipleDescending() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToSucceed(effortConnection, "users?sort=-lastName,-firstName", @"Acceptance\Fixtures\Users_GetSortedByMultipleDescendingResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedByMixedDirection() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,-firstName", @"Acceptance\Fixtures\Users_GetSortedByMixedDirectionResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedByUnknownColumn() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToFail(effortConnection, "users?sort=%2Bfoobar", @"Acceptance\Fixtures\Users_GetSortedByUnknownColumnResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedBySameColumnTwice() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToFail(effortConnection, "users?sort=%2BfirstName,%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedBySameColumnTwiceResponse.json"); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetSortedByColumnMissingDirection() + { + using (var effortConnection = GetEffortConnection()) + { + await ExpectGetToFail(effortConnection, "users?sort=firstName", @"Acceptance\Fixtures\Users_GetSortedByColumnMissingDirectionResponse.json"); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs index 8ba3ad6e..2dd518ae 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs @@ -12,7 +12,7 @@ public async Task Get() { using (var effortConnection = GetEffortConnection()) { - await TestGet(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups_GetResponse.json"); + await ExpectGetToSucceed(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups_GetResponse.json"); } } } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index b810e7ea..8f7ac7cf 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -38,7 +38,8 @@ false - + + False ..\packages\Effort.EF6.1.1.4\lib\net45\Effort.dll @@ -75,7 +76,8 @@ False ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - + + False ..\packages\NMemory.1.0.1\lib\net45\NMemory.dll @@ -111,6 +113,7 @@ + @@ -128,9 +131,14 @@ - - Always - + + + + + + + + Designer @@ -156,6 +164,9 @@ Always + + Always + Always diff --git a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs new file mode 100644 index 00000000..e85060c8 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using JSONAPI.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class EnableSortingAttributeTests + { + private class Dummy + { + public string Id { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, + new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, + new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, + new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, + new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, + new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, + new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, + new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, + new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } + }; + _fixturesQuery = _fixtures.AsQueryable(); + } + + private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) + { + var formatter = new JsonApiFormatter(modelManager); + + var httpContent = new ObjectContent(typeof (IQueryable), _fixturesQuery, formatter); + + return new HttpActionExecutedContext + { + ActionContext = new HttpActionContext + { + ControllerContext = new HttpControllerContext + { + Request = new HttpRequestMessage(HttpMethod.Get, new Uri(uri)) + } + }, + Response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = httpContent + } + }; + } + + private HttpResponseMessage GetActionFilterResponse(string uri) + { + var modelManager = new ModelManager(new PluralizationService(new Dictionary + { + { "Dummy", "Dummies" } + })); + + var filter = new EnableSortingAttribute(modelManager); + + var context = CreateActionExecutedContext(modelManager, uri); + + filter.OnActionExecuted(context); + + return context.Response; + } + + private T[] GetArray(string uri) + { + var response = GetActionFilterResponse(uri); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var returnedContent = (ObjectContent)response.Content; + var returnedQueryable = (IQueryable)returnedContent.Value; + return returnedQueryable.ToArray(); + } + + private void Expect400(string uri, string expectedMessage) + { + Action action = () => + { + GetActionFilterResponse(uri); + }; + var response = action.ShouldThrow().Which.Response; + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var value = (HttpError)((ObjectContent)response.Content).Value; + value.Message.Should().Be(expectedMessage); + } + + [TestMethod] + public void Sorts_by_attribute_ascending() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); + array.Should().BeInAscendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_attribute_descending() + { + var array = GetArray("http://api.example.com/dummies?sort=-firstName"); + array.Should().BeInDescendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_two_ascending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Sorts_by_two_descending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); + array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_empty() + { + Expect400("http://api.example.com/dummies?sort=", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_whitespace() + { + Expect400("http://api.example.com/dummies?sort= ", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_missing() + { + Expect400("http://api.example.com/dummies?sort=%2B", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_whitespace() + { + Expect400("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_no_property_exists() + { + Expect400("http://api.example.com/dummies?sort=%2Bfoobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); + } + + [TestMethod] + public void Returns_400_if_the_same_property_is_specified_more_than_once() + { + Expect400("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() + { + Expect400("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 5b0440ba..d062e0dc 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index c6a56ea1..cf1a44b4 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -31,6 +31,7 @@ private static HttpConfiguration GetWebApiConfiguration() // Global filters config.Filters.Add(new EnumerateQueryableAsyncAttribute()); + config.Filters.Add(new EnableSortingAttribute(modelManager)); config.Filters.Add(new EnableFilteringAttribute(modelManager)); // Override controller selector diff --git a/JSONAPI/ActionFilters/EnableSortingAttribute.cs b/JSONAPI/ActionFilters/EnableSortingAttribute.cs new file mode 100644 index 00000000..d467a38d --- /dev/null +++ b/JSONAPI/ActionFilters/EnableSortingAttribute.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Web.Http; +using System.Web.Http.Filters; +using JSONAPI.Core; + +namespace JSONAPI.ActionFilters +{ + /// + /// Sorts the IQueryable response content according to json-api + /// + public class EnableSortingAttribute : ActionFilterAttribute + { + private const string SortQueryParamKey = "sort"; + + private readonly IModelManager _modelManager; + + /// The model manager to use to look up model properties by json key name + public EnableSortingAttribute(IModelManager modelManager) + { + _modelManager = modelManager; + } + + private readonly Lazy _openMakeOrderedQueryMethod = + new Lazy(() => typeof(EnableSortingAttribute).GetMethod("MakeOrderedQuery", BindingFlags.NonPublic | BindingFlags.Instance)); + + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Response != null) + { + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent == null) return; + + var objectType = objectContent.ObjectType; + if (!objectType.IsGenericType || objectType.GetGenericTypeDefinition() != typeof (IQueryable<>)) return; + + var queryParams = actionExecutedContext.Request.GetQueryNameValuePairs(); + var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); + if (sortParam.Key != SortQueryParamKey) return; + + var queryableElementType = objectType.GenericTypeArguments[0]; + var makeOrderedQueryMethod = _openMakeOrderedQueryMethod.Value.MakeGenericMethod(queryableElementType); + + try + { + var orderedQuery = makeOrderedQueryMethod.Invoke(this, new[] {objectContent.Value, sortParam.Value}); + + actionExecutedContext.Response.Content = new ObjectContent(objectType, orderedQuery, + objectContent.Formatter); + } + catch (TargetInvocationException ex) + { + var statusCode = ex.InnerException is SortingException + ? HttpStatusCode.BadRequest + : HttpStatusCode.InternalServerError; + throw new HttpResponseException( + actionExecutedContext.Request.CreateErrorResponse(statusCode, ex.InnerException.Message)); + } + } + } + + // ReSharper disable once UnusedMember.Local + private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sortParam) + { + var selectors = new List>>>(); + + var usedProperties = new Dictionary(); + + var sortExpressions = sortParam.Split(','); + foreach (var sortExpression in sortExpressions) + { + if (string.IsNullOrWhiteSpace(sortExpression)) + throw new SortingException(string.Format("The sort expression \"{0}\" is invalid.", sortExpression)); + + var ascending = sortExpression[0] == '+'; + var descending = sortExpression[0] == '-'; + if (!ascending && !descending) + throw new SortingException(string.Format("The sort expression \"{0}\" does not begin with a direction indicator (+ or -).", sortExpression)); + + var propertyName = sortExpression.Substring(1); + if (string.IsNullOrWhiteSpace(propertyName)) + throw new SortingException("The property name is missing."); + + var property = _modelManager.GetPropertyForJsonKey(typeof(T), propertyName); + + if (property == null) + throw new SortingException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", + propertyName, _modelManager.GetJsonKeyForType(typeof (T)))); + + if (usedProperties.ContainsKey(property)) + throw new SortingException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); + + usedProperties[property] = null; + + var paramExpr = Expression.Parameter(typeof (T)); + var propertyExpr = Expression.Property(paramExpr, property); + var selector = Expression.Lambda>(propertyExpr, paramExpr); + + selectors.Add(Tuple.Create(ascending, selector)); + } + + var firstSelector = selectors.First(); + + IOrderedQueryable workingQuery = + firstSelector.Item1 + ? sourceQuery.OrderBy(firstSelector.Item2) + : sourceQuery.OrderByDescending(firstSelector.Item2); + + return selectors.Skip(1).Aggregate(workingQuery, + (current, selector) => + selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); + } + } + + internal class SortingException : Exception + { + public SortingException(string message) + : base(message) + { + + } + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 1f4e8afa..351fa116 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -67,6 +67,7 @@ + From c2119e9952d812e72495778cd1d3cd2c71376bbb Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 18 Feb 2015 23:24:07 -0500 Subject: [PATCH 008/186] rearrange acceptance test fixtures --- .../Requests/PostRequest.json} | 0 .../Requests/PutRequest.json} | 0 .../Responses/GetAllResponse.json} | 0 .../Responses/GetByIdResponse.json} | 0 .../Responses/GetWithFilterResponse.json} | 0 .../Responses/PostResponse.json} | 0 .../Responses/PutResponse.json} | 0 .../GetSortedAscendingResponse.json} | 0 ...rtedByColumnMissingDirectionResponse.json} | 0 .../GetSortedByMixedDirectionResponse.json} | 0 ...GetSortedByMultipleAscendingResponse.json} | 0 ...etSortedByMultipleDescendingResponse.json} | 0 .../GetSortedBySameColumnTwiceResponse.json} | 0 .../GetSortedByUnknownColumnResponse.json} | 0 .../GetSortedDescendingResponse.json} | 0 .../Responses/GetAllResponse.json} | 0 .../Acceptance/PostsTests.cs | 12 +++---- .../Acceptance/SortingTests.cs | 16 +++++----- .../Acceptance/UserGroupsTests.cs | 2 +- .../JSONAPI.EntityFramework.Tests.csproj | 32 +++++++++---------- 20 files changed, 31 insertions(+), 31 deletions(-) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_PostRequest.json => Posts/Requests/PostRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_PutRequest.json => Posts/Requests/PutRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_GetResponse.json => Posts/Responses/GetAllResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_GetByIdResponse.json => Posts/Responses/GetByIdResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_GetWithFilterResponse.json => Posts/Responses/GetWithFilterResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_PostResponse.json => Posts/Responses/PostResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Posts_PutResponse.json => Posts/Responses/PutResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedAscendingResponse.json => Sorting/Responses/GetSortedAscendingResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedByColumnMissingDirectionResponse.json => Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedByMixedDirectionResponse.json => Sorting/Responses/GetSortedByMixedDirectionResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedByMultipleAscendingResponse.json => Sorting/Responses/GetSortedByMultipleAscendingResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedByMultipleDescendingResponse.json => Sorting/Responses/GetSortedByMultipleDescendingResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedBySameColumnTwiceResponse.json => Sorting/Responses/GetSortedBySameColumnTwiceResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedByUnknownColumnResponse.json => Sorting/Responses/GetSortedByUnknownColumnResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Users_GetSortedDescendingResponse.json => Sorting/Responses/GetSortedDescendingResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{UserGroups_GetResponse.json => UserGroups/Responses/GetAllResponse.json} (100%) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetByIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetWithFilterResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedAscendingResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByColumnMissingDirectionResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMixedDirectionResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleAscendingResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByMultipleDescendingResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedBySameColumnTwiceResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedByUnknownColumnResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Users_GetSortedDescendingResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups_GetResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index 4dbdceaf..ecfe7813 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -16,11 +16,11 @@ public class PostsTests : AcceptanceTestsBase [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get() + public async Task GetAll() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "posts", @"Acceptance\Fixtures\Posts_GetResponse.json"); + await ExpectGetToSucceed(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json"); } } @@ -34,7 +34,7 @@ public async Task GetWithFilter() { using (var effortConnection = GetEffortConnection()) { - await TestGetWithFilter(effortConnection, "posts?title=Post 4", @"Acceptance\Fixtures\Posts_GetWithFilterResponse.json"); + await TestGetWithFilter(effortConnection, "posts?title=Post 4", @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json"); } } @@ -48,7 +48,7 @@ public async Task GetById() { using (var effortConnection = GetEffortConnection()) { - await TestGetById(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts_GetByIdResponse.json"); + await TestGetById(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json"); } } @@ -62,7 +62,7 @@ public async Task Post() { using (var effortConnection = GetEffortConnection()) { - await TestPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts_PostRequest.json", @"Acceptance\Fixtures\Posts_PostResponse.json"); + await TestPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Requests\PostRequest.json", @"Acceptance\Fixtures\Posts\Responses\PostResponse.json"); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -88,7 +88,7 @@ public async Task Put() { using (var effortConnection = GetEffortConnection()) { - await TestPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts_PutRequest.json", @"Acceptance\Fixtures\Posts_PutResponse.json"); + await TestPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutRequest.json", @"Acceptance\Fixtures\Posts\Responses\PutResponse.json"); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs index 78264229..18fdcff5 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs @@ -16,7 +16,7 @@ public async Task GetSortedAscending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedAscendingResponse.json"); + await ExpectGetToSucceed(effortConnection, "users?sort=%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json"); } } @@ -30,7 +30,7 @@ public async Task GetSortedDesending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=-firstName", @"Acceptance\Fixtures\Users_GetSortedDescendingResponse.json"); + await ExpectGetToSucceed(effortConnection, "users?sort=-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json"); } } @@ -44,7 +44,7 @@ public async Task GetSortedByMultipleAscending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedByMultipleAscendingResponse.json"); + await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json"); } } @@ -58,7 +58,7 @@ public async Task GetSortedByMultipleDescending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=-lastName,-firstName", @"Acceptance\Fixtures\Users_GetSortedByMultipleDescendingResponse.json"); + await ExpectGetToSucceed(effortConnection, "users?sort=-lastName,-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json"); } } @@ -72,7 +72,7 @@ public async Task GetSortedByMixedDirection() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,-firstName", @"Acceptance\Fixtures\Users_GetSortedByMixedDirectionResponse.json"); + await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json"); } } @@ -86,7 +86,7 @@ public async Task GetSortedByUnknownColumn() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=%2Bfoobar", @"Acceptance\Fixtures\Users_GetSortedByUnknownColumnResponse.json"); + await ExpectGetToFail(effortConnection, "users?sort=%2Bfoobar", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json"); } } @@ -100,7 +100,7 @@ public async Task GetSortedBySameColumnTwice() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=%2BfirstName,%2BfirstName", @"Acceptance\Fixtures\Users_GetSortedBySameColumnTwiceResponse.json"); + await ExpectGetToFail(effortConnection, "users?sort=%2BfirstName,%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json"); } } @@ -114,7 +114,7 @@ public async Task GetSortedByColumnMissingDirection() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=firstName", @"Acceptance\Fixtures\Users_GetSortedByColumnMissingDirectionResponse.json"); + await ExpectGetToFail(effortConnection, "users?sort=firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json"); } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs index 2dd518ae..764aa59a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs @@ -12,7 +12,7 @@ public async Task Get() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups_GetResponse.json"); + await ExpectGetToSucceed(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json"); } } } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 8f7ac7cf..1e8d203e 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -130,25 +130,25 @@ - - - - - - - - - + + + + + + + + + Designer - - - - - - - + + + + + + + Always From dce10cff551bce509e4779598ca3fb630701a597 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 09:39:34 -0500 Subject: [PATCH 009/186] fix acceptance tests that broken in merge --- .../Responses/GetSortedByColumnMissingDirectionResponse.json | 4 ++-- .../Sorting/Responses/GetSortedBySameColumnTwiceResponse.json | 4 ++-- .../Sorting/Responses/GetSortedByUnknownColumnResponse.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json index 24e832ab..a620e915 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json @@ -5,8 +5,8 @@ "status": "500", "title": "The sort expression \"firstName\" does not begin with a direction indicator (+ or -).", "detail": null, - "inner": null, - "stackTrace":null + "stackTrace": null, + "inner": null } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json index 5cd207cb..972ba06e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json @@ -5,8 +5,8 @@ "status": "500", "title": "The attribute \"firstName\" was specified more than once.", "detail": null, - "inner": null, - "stackTrace":null + "stackTrace": null, + "inner": null } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json index adf2ae49..34a93fbb 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json @@ -5,8 +5,8 @@ "status": "500", "title": "The attribute \"foobar\" does not exist on type \"users\".", "detail": null, - "inner": null, - "stackTrace":null + "stackTrace": null, + "inner": null } ] } \ No newline at end of file From cfd8c3c3d0fb33749066c992fb013192acb3a804 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 10:58:32 -0500 Subject: [PATCH 010/186] refactor JsonApiFormatter tests --- .../Data/DeserializeAttributeRequest.json | 75 +++++++++++++++++++ .../Data/DeserializeCollectionRequest.json | 18 +++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 6 ++ .../Json/JsonApiMediaFormaterTests.cs | 53 +++++++------ 4 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 JSONAPI.Tests/Data/DeserializeAttributeRequest.json create mode 100644 JSONAPI.Tests/Data/DeserializeCollectionRequest.json diff --git a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json new file mode 100644 index 00000000..1a4fa932 --- /dev/null +++ b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json @@ -0,0 +1,75 @@ +{ + "samples": [ + { + "id": "1", + "booleanField": false, + "nullableBooleanField": false, + "sByteField": 0, + "nullableSByteField": null, + "byteField": 0, + "nullableByteField": null, + "int16Field": 0, + "nullableInt16Field": null, + "uInt16Field": 0, + "nullableUInt16Field": null, + "int32Field": 0, + "nullableInt32Field": null, + "uInt32Field": 0, + "nullableUInt32Field": null, + "int64Field": 0, + "nullableInt64Field": null, + "uInt64Field": 0, + "nullableUInt64Field": null, + "doubleField": 0.0, + "nullableDoubleField": null, + "singleField": 0.0, + "nullableSingleField": null, + "decimalField": "0", + "nullableDecimalField": null, + "dateTimeField": "0001-01-01T00:00:00", + "nullableDateTimeField": null, + "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", + "nullableDateTimeOffsetField": null, + "guidField": "00000000-0000-0000-0000-000000000000", + "nullableGuidField": null, + "stringField": null, + "enumField": 0, + "nullableEnumField": null + }, { + "id": "2", + "booleanField": true, + "nullableBooleanField": true, + "sByteField": 123, + "nullableSByteField": 123, + "byteField": 253, + "nullableByteField": 253, + "int16Field": 32000, + "nullableInt16Field": 32000, + "uInt16Field": 64000, + "nullableUInt16Field": 64000, + "int32Field": 2000000000, + "nullableInt32Field": 2000000000, + "uInt32Field": 3000000000, + "nullableUInt32Field": 3000000000, + "int64Field": 9223372036854775807, + "nullableInt64Field": 9223372036854775807, + "uInt64Field": 9223372036854775808, + "nullableUInt64Field": 9223372036854775808, + "doubleField": 1056789.123, + "nullableDoubleField": 1056789.123, + "singleField": 1056789.13, + "nullableSingleField": 1056789.13, + "decimalField": "1056789.123", + "nullableDecimalField": "1056789.123", + "dateTimeField": "1776-07-04T00:00:00", + "nullableDateTimeField": "1776-07-04T00:00:00", + "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", + "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", + "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", + "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", + "stringField": "Some string 156", + "enumField": 1, + "nullableEnumField": 2 + } + ] +} diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json new file mode 100644 index 00000000..717ec39b --- /dev/null +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -0,0 +1,18 @@ +{ + "posts": [ + { + "id": "1", + "title": "Linkbait!", + "links": { + "author": "1" + } + }, + { + "id": "2", + "title": "Rant #1023", + "links": { + "author": "1" + } + } + ] +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 70610ab2..fb2b7653 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -107,6 +107,12 @@ Always + + Always + + + Always + Always diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs index 3e52e3dd..b669d786 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs @@ -378,40 +378,45 @@ public void SerializeErrorIntegrationTest() } [TestMethod] - public void DeserializeCollectionIntegrationTest() + [DeploymentItem(@"Data\DeserializeCollectionRequest.json")] + public void Deserializes_collections_properly() { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - formatter.WriteToStreamAsync(typeof(Post), new List {p, p2}, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - stream.Seek(0, SeekOrigin.Begin); + using (var inputStream = File.OpenRead("DeserializeCollectionRequest.json")) + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - // Act - IList posts; - posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + // Act + var posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - // Assert - Assert.AreEqual(2, posts.Count); - Assert.AreEqual(p.Id, posts[0].Id); // Order matters, right? + // Assert + posts.Count.Should().Be(2); + posts[0].Id.Should().Be(p.Id); + posts[0].Title.Should().Be(p.Title); + posts[0].Author.Id.Should().Be(a.Id); + posts[1].Id.Should().Be(p2.Id); + posts[1].Title.Should().Be(p2.Title); + posts[1].Author.Id.Should().Be(a.Id); + } } [TestMethod] + [DeploymentItem(@"Data\DeserializeAttributeRequest.json")] public async Task Deserializes_attributes_properly() { - // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - MemoryStream stream = new MemoryStream(); - - await formatter.WriteToStreamAsync(typeof(Sample), new List { s1, s2 }, stream, null, null); - stream.Seek(0, SeekOrigin.Begin); + using (var inputStream = File.OpenRead("DeserializeAttributeRequest.json")) + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), stream, null, null); + // Act + var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), inputStream, null, null); - // Assert - deserialized.Count.Should().Be(2); - deserialized[0].ShouldBeEquivalentTo(s1); - deserialized[1].ShouldBeEquivalentTo(s2); + // Assert + deserialized.Count.Should().Be(2); + deserialized[0].ShouldBeEquivalentTo(s1); + deserialized[1].ShouldBeEquivalentTo(s2); + } } [TestMethod] From dc1dc202e08539e3e7d4ee34591becbf0a5baba5 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 11:05:44 -0500 Subject: [PATCH 011/186] rename file --- JSONAPI.Tests/JSONAPI.Tests.csproj | 2 +- ...Tests.cs => JsonApiMediaFormatterTests.cs} | 1112 ++++++++--------- 2 files changed, 557 insertions(+), 557 deletions(-) rename JSONAPI.Tests/Json/{JsonApiMediaFormaterTests.cs => JsonApiMediaFormatterTests.cs} (97%) diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index fb2b7653..f2a56b01 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -86,7 +86,7 @@ - + diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs similarity index 97% rename from JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs rename to JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index b669d786..8c561428 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -1,556 +1,556 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Web.Http; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.Tests.Models; -using Newtonsoft.Json; -using JSONAPI.Json; -using JSONAPI.Core; -using System.Collections.Generic; -using System.IO; -using System.Diagnostics; -using Moq; - -namespace JSONAPI.Tests.Json -{ - [TestClass] - public class JsonApiMediaFormaterTests - { - Author a; - Post p, p2, p3, p4; - Sample s1, s2; - Tag t1, t2, t3; - - private class MockErrorSerializer : IErrorSerializer - { - public bool CanSerialize(Type type) - { - return true; - } - - public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer) - { - writer.WriteStartObject(); - writer.WritePropertyName("test"); - serializer.Serialize(writer, "foo"); - writer.WriteEndObject(); - } - } - - private class NonStandardIdThing - { - [JSONAPI.Attributes.UseAsId] - public Guid Uuid { get; set; } - public string Data { get; set; } - } - - [TestInitialize] - public void SetupModels() - { - a = new Author - { - Id = 1, - Name = "Jason Hater", - }; - - t1 = new Tag - { - Id = 1, - Text = "Ember" - }; - t2 = new Tag - { - Id = 2, - Text = "React" - }; - t3 = new Tag - { - Id = 3, - Text = "Angular" - }; - - p = new Post() - { - Id = 1, - Title = "Linkbait!", - Author = a - }; - p2 = new Post - { - Id = 2, - Title = "Rant #1023", - Author = a - }; - p3 = new Post - { - Id = 3, - Title = "Polemic in E-flat minor #824", - Author = a - }; - p4 = new Post - { - Id = 4, - Title = "This post has no author." - }; - - a.Posts = new List { p, p2, p3 }; - - p.Comments = new List() { - new Comment() { - Id = 2, - Body = "Nuh uh!", - Post = p - }, - new Comment() { - Id = 3, - Body = "Yeah huh!", - Post = p - }, - new Comment() { - Id = 4, - Body = "Third Reich.", - Post = p, - CustomData = "{ \"foo\": \"bar\" }" - } - }; - p2.Comments = new List { - new Comment { - Id = 5, - Body = "I laughed, I cried!", - Post = p2 - } - }; - - s1 = new Sample - { - Id = "1", - BooleanField = false, - NullableBooleanField = false, - SByteField = default(SByte), - NullableSByteField = null, - ByteField = default(Byte), - NullableByteField = null, - Int16Field = default(Int16), - NullableInt16Field = null, - UInt16Field = default(UInt16), - NullableUInt16Field = null, - Int32Field = default(Int32), - NullableInt32Field = null, - UInt32Field = default(Int32), - NullableUInt32Field = null, - Int64Field = default(Int64), - NullableInt64Field = null, - UInt64Field = default(UInt64), - NullableUInt64Field = null, - DoubleField = default(Double), - NullableDoubleField = null, - SingleField = default(Single), - NullableSingleField = null, - DecimalField = default(Decimal), - NullableDecimalField = null, - DateTimeField = default(DateTime), - NullableDateTimeField = null, - DateTimeOffsetField = default(DateTimeOffset), - NullableDateTimeOffsetField = null, - GuidField = default(Guid), - NullableGuidField = null, - StringField = default(String), - EnumField = default(SampleEnum), - NullableEnumField = null, - }; - s2 = new Sample - { - Id = "2", - BooleanField = true, - NullableBooleanField = true, - SByteField = 123, - NullableSByteField = 123, - ByteField = 253, - NullableByteField = 253, - Int16Field = 32000, - NullableInt16Field = 32000, - UInt16Field = 64000, - NullableUInt16Field = 64000, - Int32Field = 2000000000, - NullableInt32Field = 2000000000, - UInt32Field = 3000000000, - NullableUInt32Field = 3000000000, - Int64Field = 9223372036854775807, - NullableInt64Field = 9223372036854775807, - UInt64Field = 9223372036854775808, - NullableUInt64Field = 9223372036854775808, - DoubleField = 1056789.123, - NullableDoubleField = 1056789.123, - SingleField = 1056789.123f, - NullableSingleField = 1056789.123f, - DecimalField = 1056789.123m, - NullableDecimalField = 1056789.123m, - DateTimeField = new DateTime(1776, 07, 04), - NullableDateTimeField = new DateTime(1776, 07, 04), - DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), - NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), - GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), - NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), - StringField = "Some string 156", - EnumField = SampleEnum.Value1, - NullableEnumField = SampleEnum.Value2, - }; - } - - [TestMethod] - [DeploymentItem(@"Data\SerializerIntegrationTest.json")] - public void SerializerIntegrationTest() - { - // Arrange - //PayloadConverter pc = new PayloadConverter(); - //ModelConverter mc = new ModelConverter(); - //ContractResolver.PluralizationService = new PluralizationService(); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - //Payload payload = new Payload(a.Posts); - //js.Serialize(jw, payload); - formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }.ToList(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); - Assert.AreEqual(expected, output.Trim()); - //Assert.AreEqual("[2,3,4]", sw.ToString()); - } - - [TestMethod] - [DeploymentItem(@"Data\SerializerIntegrationTest.json")] - public void SerializeArrayIntegrationTest() - { - // Arrange - //PayloadConverter pc = new PayloadConverter(); - //ModelConverter mc = new ModelConverter(); - //ContractResolver.PluralizationService = new PluralizationService(); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - //Payload payload = new Payload(a.Posts); - //js.Serialize(jw, payload); - formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); - Assert.AreEqual(expected, output.Trim()); - //Assert.AreEqual("[2,3,4]", sw.ToString()); - } - - [TestMethod] - [DeploymentItem(@"Data\AttributeSerializationTest.json")] - public void Serializes_attributes_properly() - { - // Arrang - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - formatter.WriteToStreamAsync(typeof(Sample), new[] { s1, s2 }, stream, null, null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - var expected = JsonHelpers.MinifyJson(File.ReadAllText("AttributeSerializationTest.json")); - Assert.AreEqual(expected, output.Trim()); - } - - [TestMethod] - [DeploymentItem(@"Data\ByteIdSerializationTest.json")] - public void Serializes_byte_ids_properly() - { - // Arrang - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - formatter.WriteToStreamAsync(typeof(Tag), new[] { t1, t2, t3 }, stream, null, null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - var expected = JsonHelpers.MinifyJson(File.ReadAllText("ByteIdSerializationTest.json")); - Assert.AreEqual(expected, output.Trim()); - } - - [TestMethod] - [DeploymentItem(@"Data\ReformatsRawJsonStringWithUnquotedKeys.json")] - public void Reformats_raw_json_string_with_unquoted_keys() - { - // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - var payload = new [] { new Comment { Id = 5, CustomData = "{ unquotedKey: 5 }"}}; - formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); - - // Assert - var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("ReformatsRawJsonStringWithUnquotedKeys.json")); - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - output.Should().Be(minifiedExpectedJson); - } - - [TestMethod] - [DeploymentItem(@"Data\MalformedRawJsonString.json")] - public void Does_not_serialize_malformed_raw_json_string() - { - // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - var payload = new[] { new Comment { Id = 5, CustomData = "{ x }" } }; - formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); - - // Assert - var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("MalformedRawJsonString.json")); - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - output.Should().Be(minifiedExpectedJson); - } - - [TestMethod] - [DeploymentItem(@"Data\FormatterErrorSerializationTest.json")] - public void Should_serialize_error() - { - // Arrange - var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer()); - var stream = new MemoryStream(); - - // Act - var payload = new HttpError(new Exception(), true); - formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - var expectedJson = File.ReadAllText("FormatterErrorSerializationTest.json"); - var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - output.Should().Be(minifiedExpectedJson); - } - - [TestMethod] - [DeploymentItem(@"Data\ErrorSerializerTest.json")] - public void SerializeErrorIntegrationTest() - { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - var mockInnerException = new Mock(MockBehavior.Strict); - mockInnerException.Setup(m => m.Message).Returns("Inner exception message"); - mockInnerException.Setup(m => m.StackTrace).Returns("Inner stack trace"); - - var outerException = new Exception("Outer exception message", mockInnerException.Object); - - var payload = new HttpError(outerException, true) - { - StackTrace = "Outer stack trace" - }; - - // Act - formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - var expectedJson = File.ReadAllText("ErrorSerializerTest.json"); - var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - - // We don't know what the GUIDs will be, so replace them - var regex = new Regex(@"[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}"); - output = regex.Replace(output, "OUTER-ID", 1); - output = regex.Replace(output, "INNER-ID", 1); - output.Should().Be(minifiedExpectedJson); - } - - [TestMethod] - [DeploymentItem(@"Data\DeserializeCollectionRequest.json")] - public void Deserializes_collections_properly() - { - using (var inputStream = File.OpenRead("DeserializeCollectionRequest.json")) - { - // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - - // Act - var posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - - // Assert - posts.Count.Should().Be(2); - posts[0].Id.Should().Be(p.Id); - posts[0].Title.Should().Be(p.Title); - posts[0].Author.Id.Should().Be(a.Id); - posts[1].Id.Should().Be(p2.Id); - posts[1].Title.Should().Be(p2.Title); - posts[1].Author.Id.Should().Be(a.Id); - } - } - - [TestMethod] - [DeploymentItem(@"Data\DeserializeAttributeRequest.json")] - public async Task Deserializes_attributes_properly() - { - using (var inputStream = File.OpenRead("DeserializeAttributeRequest.json")) - { - // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - - // Act - var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), inputStream, null, null); - - // Assert - deserialized.Count.Should().Be(2); - deserialized[0].ShouldBeEquivalentTo(s1); - deserialized[1].ShouldBeEquivalentTo(s2); - } - } - - [TestMethod] - [DeploymentItem(@"Data\DeserializeRawJsonTest.json")] - public async Task DeserializeRawJsonTest() - { - using (var inputStream = File.OpenRead("DeserializeRawJsonTest.json")) - { - // Arrange - var formatter = new JsonApiFormatter(new PluralizationService()); - - // Act - var comments = ((IEnumerable)await formatter.ReadFromStreamAsync(typeof (Comment), inputStream, null, null)).ToArray(); - - // Assert - Assert.AreEqual(2, comments.Count()); - Assert.AreEqual(null, comments[0].CustomData); - Assert.AreEqual("{\"foo\":\"bar\"}", comments[1].CustomData); - } - } - - // Issue #1 - [TestMethod(), Timeout(1000)] - public void DeserializeExtraPropertyTest() - { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":[]}}}")); - - // Act - Author a; - a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Assert - Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! - } - - // Issue #1 - [TestMethod(), Timeout(1000)] - public void DeserializeExtraRelationshipTest() - { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":[],""bogus"":[""PANIC!""]}}}")); - - // Act - Author a; - a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Assert - Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! - } - - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void SerializeNonStandardIdTest() - { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); - var stream = new MemoryStream(); - var payload = new List { - new NonStandardIdThing { Uuid = new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"), Data = "Swap" } - }; - - // Act - formatter.WriteToStreamAsync(typeof(List), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - var expectedJson = File.ReadAllText("NonStandardIdTest.json"); - var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - output.Should().Be(minifiedExpectedJson); - } - - #region Non-standard Id attribute tests - - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdTest() - { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); - var stream = new FileStream("NonStandardIdTest.json",FileMode.Open); - - // Act - IList things; - things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - stream.Close(); - - // Assert - things.Count.Should().Be(1); - things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - } - - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdWithIdOnly() - { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); - string json = File.ReadAllText("NonStandardIdTest.json"); - json = Regex.Replace(json, @"""uuid"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,",""); // remove the uuid attribute - var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); - - // Act - IList things; - things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Assert - json.Should().NotContain("uuid", "The \"uuid\" attribute was supposed to be removed, test methodology problem!"); - things.Count.Should().Be(1); - things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - } - - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdWithoutId() - { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); - string json = File.ReadAllText("NonStandardIdTest.json"); - json = Regex.Replace(json, @"""id"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,", ""); // remove the uuid attribute - var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); - - // Act - IList things; - things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Assert - json.Should().NotContain("\"id\"", "The \"id\" attribute was supposed to be removed, test methodology problem!"); - things.Count.Should().Be(1); - things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - - } - - #endregion - - } -} +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web.Http; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using JSONAPI.Tests.Models; +using Newtonsoft.Json; +using JSONAPI.Json; +using JSONAPI.Core; +using System.Collections.Generic; +using System.IO; +using System.Diagnostics; +using Moq; + +namespace JSONAPI.Tests.Json +{ + [TestClass] + public class JsonApiMediaFormatterTests + { + Author a; + Post p, p2, p3, p4; + Sample s1, s2; + Tag t1, t2, t3; + + private class MockErrorSerializer : IErrorSerializer + { + public bool CanSerialize(Type type) + { + return true; + } + + public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("test"); + serializer.Serialize(writer, "foo"); + writer.WriteEndObject(); + } + } + + private class NonStandardIdThing + { + [JSONAPI.Attributes.UseAsId] + public Guid Uuid { get; set; } + public string Data { get; set; } + } + + [TestInitialize] + public void SetupModels() + { + a = new Author + { + Id = 1, + Name = "Jason Hater", + }; + + t1 = new Tag + { + Id = 1, + Text = "Ember" + }; + t2 = new Tag + { + Id = 2, + Text = "React" + }; + t3 = new Tag + { + Id = 3, + Text = "Angular" + }; + + p = new Post() + { + Id = 1, + Title = "Linkbait!", + Author = a + }; + p2 = new Post + { + Id = 2, + Title = "Rant #1023", + Author = a + }; + p3 = new Post + { + Id = 3, + Title = "Polemic in E-flat minor #824", + Author = a + }; + p4 = new Post + { + Id = 4, + Title = "This post has no author." + }; + + a.Posts = new List { p, p2, p3 }; + + p.Comments = new List() { + new Comment() { + Id = 2, + Body = "Nuh uh!", + Post = p + }, + new Comment() { + Id = 3, + Body = "Yeah huh!", + Post = p + }, + new Comment() { + Id = 4, + Body = "Third Reich.", + Post = p, + CustomData = "{ \"foo\": \"bar\" }" + } + }; + p2.Comments = new List { + new Comment { + Id = 5, + Body = "I laughed, I cried!", + Post = p2 + } + }; + + s1 = new Sample + { + Id = "1", + BooleanField = false, + NullableBooleanField = false, + SByteField = default(SByte), + NullableSByteField = null, + ByteField = default(Byte), + NullableByteField = null, + Int16Field = default(Int16), + NullableInt16Field = null, + UInt16Field = default(UInt16), + NullableUInt16Field = null, + Int32Field = default(Int32), + NullableInt32Field = null, + UInt32Field = default(Int32), + NullableUInt32Field = null, + Int64Field = default(Int64), + NullableInt64Field = null, + UInt64Field = default(UInt64), + NullableUInt64Field = null, + DoubleField = default(Double), + NullableDoubleField = null, + SingleField = default(Single), + NullableSingleField = null, + DecimalField = default(Decimal), + NullableDecimalField = null, + DateTimeField = default(DateTime), + NullableDateTimeField = null, + DateTimeOffsetField = default(DateTimeOffset), + NullableDateTimeOffsetField = null, + GuidField = default(Guid), + NullableGuidField = null, + StringField = default(String), + EnumField = default(SampleEnum), + NullableEnumField = null, + }; + s2 = new Sample + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SByteField = 123, + NullableSByteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + UInt16Field = 64000, + NullableUInt16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + UInt32Field = 3000000000, + NullableUInt32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + UInt64Field = 9223372036854775808, + NullableUInt64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + }; + } + + [TestMethod] + [DeploymentItem(@"Data\SerializerIntegrationTest.json")] + public void SerializerIntegrationTest() + { + // Arrange + //PayloadConverter pc = new PayloadConverter(); + //ModelConverter mc = new ModelConverter(); + //ContractResolver.PluralizationService = new PluralizationService(); + + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + //Payload payload = new Payload(a.Posts); + //js.Serialize(jw, payload); + formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }.ToList(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); + Assert.AreEqual(expected, output.Trim()); + //Assert.AreEqual("[2,3,4]", sw.ToString()); + } + + [TestMethod] + [DeploymentItem(@"Data\SerializerIntegrationTest.json")] + public void SerializeArrayIntegrationTest() + { + // Arrange + //PayloadConverter pc = new PayloadConverter(); + //ModelConverter mc = new ModelConverter(); + //ContractResolver.PluralizationService = new PluralizationService(); + + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + //Payload payload = new Payload(a.Posts); + //js.Serialize(jw, payload); + formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); + Assert.AreEqual(expected, output.Trim()); + //Assert.AreEqual("[2,3,4]", sw.ToString()); + } + + [TestMethod] + [DeploymentItem(@"Data\AttributeSerializationTest.json")] + public void Serializes_attributes_properly() + { + // Arrang + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + formatter.WriteToStreamAsync(typeof(Sample), new[] { s1, s2 }, stream, null, null); + + // Assert + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + var expected = JsonHelpers.MinifyJson(File.ReadAllText("AttributeSerializationTest.json")); + Assert.AreEqual(expected, output.Trim()); + } + + [TestMethod] + [DeploymentItem(@"Data\ByteIdSerializationTest.json")] + public void Serializes_byte_ids_properly() + { + // Arrang + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + formatter.WriteToStreamAsync(typeof(Tag), new[] { t1, t2, t3 }, stream, null, null); + + // Assert + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + var expected = JsonHelpers.MinifyJson(File.ReadAllText("ByteIdSerializationTest.json")); + Assert.AreEqual(expected, output.Trim()); + } + + [TestMethod] + [DeploymentItem(@"Data\ReformatsRawJsonStringWithUnquotedKeys.json")] + public void Reformats_raw_json_string_with_unquoted_keys() + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + var payload = new [] { new Comment { Id = 5, CustomData = "{ unquotedKey: 5 }"}}; + formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); + + // Assert + var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("ReformatsRawJsonStringWithUnquotedKeys.json")); + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\MalformedRawJsonString.json")] + public void Does_not_serialize_malformed_raw_json_string() + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + MemoryStream stream = new MemoryStream(); + + // Act + var payload = new[] { new Comment { Id = 5, CustomData = "{ x }" } }; + formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); + + // Assert + var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("MalformedRawJsonString.json")); + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\FormatterErrorSerializationTest.json")] + public void Should_serialize_error() + { + // Arrange + var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer()); + var stream = new MemoryStream(); + + // Act + var payload = new HttpError(new Exception(), true); + formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + var expectedJson = File.ReadAllText("FormatterErrorSerializationTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\ErrorSerializerTest.json")] + public void SerializeErrorIntegrationTest() + { + // Arrange + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + MemoryStream stream = new MemoryStream(); + + var mockInnerException = new Mock(MockBehavior.Strict); + mockInnerException.Setup(m => m.Message).Returns("Inner exception message"); + mockInnerException.Setup(m => m.StackTrace).Returns("Inner stack trace"); + + var outerException = new Exception("Outer exception message", mockInnerException.Object); + + var payload = new HttpError(outerException, true) + { + StackTrace = "Outer stack trace" + }; + + // Act + formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + var expectedJson = File.ReadAllText("ErrorSerializerTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + + // We don't know what the GUIDs will be, so replace them + var regex = new Regex(@"[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}"); + output = regex.Replace(output, "OUTER-ID", 1); + output = regex.Replace(output, "INNER-ID", 1); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\DeserializeCollectionRequest.json")] + public void Deserializes_collections_properly() + { + using (var inputStream = File.OpenRead("DeserializeCollectionRequest.json")) + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + + // Act + var posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; + + // Assert + posts.Count.Should().Be(2); + posts[0].Id.Should().Be(p.Id); + posts[0].Title.Should().Be(p.Title); + posts[0].Author.Id.Should().Be(a.Id); + posts[1].Id.Should().Be(p2.Id); + posts[1].Title.Should().Be(p2.Title); + posts[1].Author.Id.Should().Be(a.Id); + } + } + + [TestMethod] + [DeploymentItem(@"Data\DeserializeAttributeRequest.json")] + public async Task Deserializes_attributes_properly() + { + using (var inputStream = File.OpenRead("DeserializeAttributeRequest.json")) + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + + // Act + var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), inputStream, null, null); + + // Assert + deserialized.Count.Should().Be(2); + deserialized[0].ShouldBeEquivalentTo(s1); + deserialized[1].ShouldBeEquivalentTo(s2); + } + } + + [TestMethod] + [DeploymentItem(@"Data\DeserializeRawJsonTest.json")] + public async Task DeserializeRawJsonTest() + { + using (var inputStream = File.OpenRead("DeserializeRawJsonTest.json")) + { + // Arrange + var formatter = new JsonApiFormatter(new PluralizationService()); + + // Act + var comments = ((IEnumerable)await formatter.ReadFromStreamAsync(typeof (Comment), inputStream, null, null)).ToArray(); + + // Assert + Assert.AreEqual(2, comments.Count()); + Assert.AreEqual(null, comments[0].CustomData); + Assert.AreEqual("{\"foo\":\"bar\"}", comments[1].CustomData); + } + } + + // Issue #1 + [TestMethod(), Timeout(1000)] + public void DeserializeExtraPropertyTest() + { + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + MemoryStream stream = new MemoryStream(); + + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":[]}}}")); + + // Act + Author a; + a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + + // Assert + Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! + } + + // Issue #1 + [TestMethod(), Timeout(1000)] + public void DeserializeExtraRelationshipTest() + { + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + MemoryStream stream = new MemoryStream(); + + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":[],""bogus"":[""PANIC!""]}}}")); + + // Act + Author a; + a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + + // Assert + Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! + } + + [TestMethod] + [DeploymentItem(@"Data\NonStandardIdTest.json")] + public void SerializeNonStandardIdTest() + { + var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + var stream = new MemoryStream(); + var payload = new List { + new NonStandardIdThing { Uuid = new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"), Data = "Swap" } + }; + + // Act + formatter.WriteToStreamAsync(typeof(List), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + var expectedJson = File.ReadAllText("NonStandardIdTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + output.Should().Be(minifiedExpectedJson); + } + + #region Non-standard Id attribute tests + + [TestMethod] + [DeploymentItem(@"Data\NonStandardIdTest.json")] + public void DeserializeNonStandardIdTest() + { + var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + var stream = new FileStream("NonStandardIdTest.json",FileMode.Open); + + // Act + IList things; + things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + stream.Close(); + + // Assert + things.Count.Should().Be(1); + things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); + } + + [TestMethod] + [DeploymentItem(@"Data\NonStandardIdTest.json")] + public void DeserializeNonStandardIdWithIdOnly() + { + var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + string json = File.ReadAllText("NonStandardIdTest.json"); + json = Regex.Replace(json, @"""uuid"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,",""); // remove the uuid attribute + var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); + + // Act + IList things; + things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + + // Assert + json.Should().NotContain("uuid", "The \"uuid\" attribute was supposed to be removed, test methodology problem!"); + things.Count.Should().Be(1); + things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); + } + + [TestMethod] + [DeploymentItem(@"Data\NonStandardIdTest.json")] + public void DeserializeNonStandardIdWithoutId() + { + var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + string json = File.ReadAllText("NonStandardIdTest.json"); + json = Regex.Replace(json, @"""id"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,", ""); // remove the uuid attribute + var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); + + // Act + IList things; + things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; + + // Assert + json.Should().NotContain("\"id\"", "The \"id\" attribute was supposed to be removed, test methodology problem!"); + things.Count.Should().Be(1); + things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); + + } + + #endregion + + } +} From 962be72444a60f665d8a95c8128cd148287a8fca Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 11:21:05 -0500 Subject: [PATCH 012/186] use fixture file for metadata manager test --- JSONAPI.Tests/Core/MetadataManagerTests.cs | 38 +++++++++---------- ...adataManagerPropertyWasPresentRequest.json | 8 ++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 3 ++ 3 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs index e4a0d259..733f5706 100644 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ b/JSONAPI.Tests/Core/MetadataManagerTests.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using JSONAPI.Json; using System.IO; using JSONAPI.Tests.Models; @@ -11,29 +10,28 @@ namespace JSONAPI.Tests.Core public class MetadataManagerTests { [TestMethod] + [DeploymentItem(@"Data\MetadataManagerPropertyWasPresentRequest.json")] public void PropertyWasPresentTest() { - // Arrange + using (var inputStream = File.OpenRead("MetadataManagerPropertyWasPresentRequest.json")) + { + // Arrange + JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); + var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""posts"":{""id"":42,""links"":{""author"":""18""}}}")); + // Act + bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); + bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); + bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); + bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); - Post p; - p = (Post)formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Act - bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); - bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); - bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); - bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); - - // Assert - Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); - Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); - Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); - Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); + // Assert + Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); + Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); + Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); + Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); + } } } } diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json new file mode 100644 index 00000000..12c678ed --- /dev/null +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -0,0 +1,8 @@ +{ + "posts": { + "id": "42", + "links": { + "author": "18" + } + } +} \ No newline at end of file diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index f2a56b01..4f377db4 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -104,6 +104,9 @@ + + Always + Always From f81047ae1aa7180acf24143ad3afa5c5972a86f0 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 11:37:27 -0500 Subject: [PATCH 013/186] comply with spec regarding updating relationships --- .../Acceptance/AcceptanceTestsBase.cs | 107 ++----- .../Fixtures/Posts/Requests/PostRequest.json | 5 +- .../PutWithArrayRelationshipValueRequest.json | 10 + ...son => PutWithAttributeUpdateRequest.json} | 0 .../PutWithMissingToManyIdsRequest.json | 12 + .../PutWithMissingToManyTypeRequest.json | 12 + .../PutWithMissingToOneIdRequest.json | 12 + .../PutWithMissingToOneTypeRequest.json | 12 + ...PutWithStringRelationshipValueRequest.json | 10 + .../Requests/PutWithToManyUpdateRequest.json | 13 + .../Requests/PutWithToOneUpdateRequest.json | 13 + ...PutWithArrayRelationshipValueResponse.json | 12 + ...on => PutWithAttributeUpdateResponse.json} | 0 .../PutWithMissingToManyIdsResponse.json | 12 + .../PutWithMissingToManyTypeResponse.json | 12 + .../PutWithMissingToOneIdResponse.json | 12 + .../PutWithMissingToOneTypeResponse.json | 12 + ...utWithStringRelationshipValueResponse.json | 12 + .../PutWithToManyUpdateResponse.json | 15 + .../Responses/PutWithToOneUpdateResponse.json | 15 + .../Acceptance/PostsTests.cs | 276 +++++++++++++++++- .../Acceptance/SortingTests.cs | 44 ++- .../Acceptance/UserGroupsTests.cs | 9 +- .../EntityConverterTests.cs | 35 --- .../JSONAPI.EntityFramework.Tests.csproj | 20 +- ...adataManagerPropertyWasPresentRequest.json | 5 +- JSONAPI/Json/JsonApiFormatter.cs | 139 ++++++++- 27 files changed, 685 insertions(+), 151 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutRequest.json => PutWithAttributeUpdateRequest.json} (100%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutResponse.json => PutWithAttributeUpdateResponse.json} (100%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index 5f67a887..f42b124a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -1,6 +1,5 @@ using System; using System.Data.Common; -using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -18,6 +17,8 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance public abstract class AcceptanceTestsBase { private static readonly Regex GuidRegex = new Regex(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase); + //private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace"":[\s]*""[\w\:\\\.\s\,\-]*"""); + private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace""[\s]*:[\s]*"".*?"""); private static readonly Uri BaseUri = new Uri("http://localhost"); protected static DbConnection GetEffortConnection() @@ -25,33 +26,21 @@ protected static DbConnection GetEffortConnection() return TestHelpers.GetEffortConnection(@"Acceptance\Data"); } - protected async Task ExpectGetToSucceed(DbConnection effortConnection, string requestPath, - string expectedResponseTextResourcePath) + protected async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath) { - var response = await GetEndpointResponse(effortConnection, requestPath); - - response.StatusCode.Should().Be(HttpStatusCode.OK); var responseContent = await response.Content.ReadAsStringAsync(); - var expected = + var expectedResponse = JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - - protected async Task ExpectGetToFail(DbConnection effortConnection, string requestPath, - string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest) - { - var expectedResponse = JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - var response = await GetEndpointResponse(effortConnection, requestPath); - response.StatusCode.Should().Be(expectedStatusCode); - - var responseContent = await response.Content.ReadAsStringAsync(); var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); + redactedResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); redactedResponse.Should().Be(expectedResponse); } - protected async Task GetEndpointResponse(DbConnection effortConnection, string requestPath) + #region GET + + protected async Task SubmitGet(DbConnection effortConnection, string requestPath) { using (var server = TestServer.Create(app => { @@ -65,26 +54,10 @@ protected async Task GetEndpointResponse(DbConnection effor } } - protected async Task TestGetWithFilter(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); + #endregion + #region POST - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestGetById(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) + protected async Task SubmitPost(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) { using (var server = TestServer.Create(app => { @@ -93,28 +66,7 @@ protected async Task TestGetById(DbConnection effortConnection, string requestPa })) { var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestPost(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var requestContent = - JsonHelpers.MinifyJson( - TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath)); + var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); var response = await server .CreateRequest(uri.ToString()) .And(request => @@ -122,16 +74,14 @@ protected async Task TestPost(DbConnection effortConnection, string requestPath, request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); }) .PostAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); + return response; } } - protected async Task TestPut(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath, string expectedResponseTextResourcePath) + #endregion + #region PUT + + protected async Task SubmitPut(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) { using (var server = TestServer.Create(app => { @@ -140,26 +90,21 @@ protected async Task TestPut(DbConnection effortConnection, string requestPath, })) { var uri = new Uri(BaseUri, requestPath); - var requestContent = - JsonHelpers.MinifyJson( - TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath)); + var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); var response = await server .CreateRequest(uri.ToString()) .And(request => { - request.Content = new StringContent(requestContent, Encoding.UTF8, - "application/vnd.api+json"); + request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); }).SendAsync("PUT"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); + return response; } } - protected async Task TestDelete(DbConnection effortConnection, string requestPath) + #endregion + #region DELETE + + protected async Task SubmitDelete(DbConnection effortConnection, string requestPath) { using (var server = TestServer.Create(app => { @@ -171,8 +116,10 @@ protected async Task TestDelete(DbConnection effortConnection, string requestPat var response = await server .CreateRequest(uri.ToString()) .SendAsync("DELETE"); - response.StatusCode.Should().Be(HttpStatusCode.NoContent); + return response; } } + + #endregion } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json index a97e1a23..ce9eeff8 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json @@ -6,7 +6,10 @@ "content": "Added post content", "created": "2015-03-11T04:31:00+00:00", "links": { - "author": "401" + "author": { + "type": "users", + "id": "401" + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json new file mode 100644 index 00000000..c049e472 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json @@ -0,0 +1,10 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": ["301"] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json new file mode 100644 index 00000000..8ddf5314 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json @@ -0,0 +1,12 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": { + "type": "tags" + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json new file mode 100644 index 00000000..b14c3281 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json @@ -0,0 +1,12 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": { + "ids": [ "301" ] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json new file mode 100644 index 00000000..6683cb1c --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json @@ -0,0 +1,12 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "author": { + "type": "users" + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json new file mode 100644 index 00000000..52d534f5 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json @@ -0,0 +1,12 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "author": { + "id": "403" + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json new file mode 100644 index 00000000..3f2ab79f --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json @@ -0,0 +1,10 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "author": "301" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json new file mode 100644 index 00000000..37ebbbe5 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json @@ -0,0 +1,13 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": { + "type": "tags", + "ids": ["301"] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json new file mode 100644 index 00000000..5bd71f57 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json @@ -0,0 +1,13 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "author": { + "type": "users", + "id": "403" + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json new file mode 100644 index 00000000..62bf2d23 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "The value of the relationship must be an object.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json new file mode 100644 index 00000000..a42e78e8 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Nothing was specified for the `ids` property.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json new file mode 100644 index 00000000..18b4f910 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Nothing was specified for the `type` property.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json new file mode 100644 index 00000000..5e021e73 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Nothing was specified for the `id` property.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json new file mode 100644 index 00000000..18b4f910 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Nothing was specified for the `type` property.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json new file mode 100644 index 00000000..62bf2d23 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "The value of the relationship must be an object.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json new file mode 100644 index 00000000..04c39b9a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json @@ -0,0 +1,15 @@ +{ + "posts": [ + { + "id": "202", + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00", + "links": { + "author": "401", + "comments": [ "104" ], + "tags": [ "301" ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json new file mode 100644 index 00000000..4baeb786 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json @@ -0,0 +1,15 @@ +{ + "posts": [ + { + "id": "202", + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00", + "links": { + "author": "403", + "comments": [ "104" ], + "tags": [ "302", "303" ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index ecfe7813..e3a36966 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -1,5 +1,7 @@ using System; +using System.Data.Entity; using System.Linq; +using System.Net; using System.Threading.Tasks; using FluentAssertions; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; @@ -20,7 +22,10 @@ public async Task GetAll() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json"); + var response = await SubmitGet(effortConnection, "posts"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json"); } } @@ -34,7 +39,10 @@ public async Task GetWithFilter() { using (var effortConnection = GetEffortConnection()) { - await TestGetWithFilter(effortConnection, "posts?title=Post 4", @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json"); + var response = await SubmitGet(effortConnection, "posts?title=Post 4"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json"); } } @@ -48,7 +56,10 @@ public async Task GetById() { using (var effortConnection = GetEffortConnection()) { - await TestGetById(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json"); + var response = await SubmitGet(effortConnection, "posts/202"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json"); } } @@ -62,7 +73,10 @@ public async Task Post() { using (var effortConnection = GetEffortConnection()) { - await TestPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Requests\PostRequest.json", @"Acceptance\Fixtures\Posts\Responses\PostResponse.json"); + var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Requests\PostRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PostResponse.json"); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -84,15 +98,18 @@ public async Task Post() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Put() + public async Task PutWithAttributeUpdate() { using (var effortConnection = GetEffortConnection()) { - await TestPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutRequest.json", @"Acceptance\Fixtures\Posts\Responses\PutResponse.json"); + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithAttributeUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithAttributeUpdateResponse.json"); using (var dbContext = new TestDbContext(effortConnection, false)) { - var allPosts = dbContext.Posts.ToArray(); + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); allPosts.Length.Should().Be(4); var actualPost = allPosts.First(t => t.Id == "202"); actualPost.Id.Should().Be("202"); @@ -100,6 +117,247 @@ public async Task Put() actualPost.Content.Should().Be("Post 2 content"); actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithToManyUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyUpdateResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("301"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithToOneUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToOneUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToOneUpdateResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("403"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithMissingToOneId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneIdRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneIdResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithMissingToOneType() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneTypeRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneTypeResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithMissingToManyIds() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyIdsRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyIdsResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithMissingToManyType() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyTypeRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyTypeResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithArrayRelationshipValue() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithArrayRelationshipValueRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithArrayRelationshipValueResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithStringRelationshipValue() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithStringRelationshipValueRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithStringRelationshipValueResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); } } } @@ -114,7 +372,9 @@ public async Task Delete() { using (var effortConnection = GetEffortConnection()) { - await TestDelete(effortConnection, "posts/203"); + var response = await SubmitDelete(effortConnection, "posts/203"); + + response.StatusCode.Should().Be(HttpStatusCode.NoContent); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs index 18fdcff5..2b9adba3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.EntityFramework.Tests.Acceptance @@ -16,7 +18,10 @@ public async Task GetSortedAscending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json"); } } @@ -30,7 +35,10 @@ public async Task GetSortedDesending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=-firstName"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json"); } } @@ -44,7 +52,10 @@ public async Task GetSortedByMultipleAscending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,%2BfirstName"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json"); } } @@ -58,7 +69,10 @@ public async Task GetSortedByMultipleDescending() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=-lastName,-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=-lastName,-firstName"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json"); } } @@ -72,7 +86,10 @@ public async Task GetSortedByMixedDirection() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "users?sort=%2BlastName,-firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,-firstName"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json"); } } @@ -86,7 +103,10 @@ public async Task GetSortedByUnknownColumn() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=%2Bfoobar", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=%2Bfoobar"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json"); } } @@ -100,7 +120,10 @@ public async Task GetSortedBySameColumnTwice() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=%2BfirstName,%2BfirstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName,%2BfirstName"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json"); } } @@ -114,7 +137,10 @@ public async Task GetSortedByColumnMissingDirection() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToFail(effortConnection, "users?sort=firstName", @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json"); + var response = await SubmitGet(effortConnection, "users?sort=firstName"); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json"); } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs index 764aa59a..996a27ea 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.EntityFramework.Tests.Acceptance @@ -12,7 +14,10 @@ public async Task Get() { using (var effortConnection = GetEffortConnection()) { - await ExpectGetToSucceed(effortConnection, "user-groups", @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json"); + var response = await SubmitGet(effortConnection, "user-groups"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json"); } } } diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index 0e5f1a22..88c83713 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -119,41 +119,6 @@ public void SerializeTest() } - [TestMethod] - [DeploymentItem(@"Data\Post.json")] - public async Task DeserializePostIntegrationTest() - { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); - - // Serialize a post and change the JSON... Not "unit" at all, I know, but a good integration test I think... - formatter.WriteToStreamAsync(typeof(Post), p, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - string serializedPost = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - // Change the post title (a scalar value) - serializedPost = serializedPost.Replace("Linkbait!", "Not at all linkbait!"); - // Remove a comment (Note that order is undefined/not deterministic!) - serializedPost = Regex.Replace(serializedPost, String.Format(@"(""comments""\s*:\s*\[[^]]*)(,""{0}""|""{0}"",)", c3.Id), @"$1"); - - // Reread the serialized JSON... - stream.Dispose(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(serializedPost)); - - // Act - Post pUpdated; - pUpdated = (Post)await formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null); - pUpdated = await materializer.MaterializeUpdateAsync(pUpdated); - - // Assert - Assert.AreEqual(a, pUpdated.Author); - Assert.AreEqual("Not at all linkbait!", pUpdated.Title); - Assert.AreEqual(2, pUpdated.Comments.Count()); - Assert.IsFalse(pUpdated.Comments.Contains(c3)); - //Debug.WriteLine(sw.ToString()); - } - [TestMethod] public async Task UnderpostingTest() { diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 1e8d203e..1bcbff61 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -139,6 +139,23 @@ + + + + + + + + + + + + + + + + + Designer @@ -147,8 +164,7 @@ - - + Always diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json index 12c678ed..32d13c98 100644 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -2,7 +2,10 @@ "posts": { "id": "42", "links": { - "author": "18" + "author": { + "id": "18", + "type": "authors" + } } } } \ No newline at end of file diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 655ff5b4..a9dc606c 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -8,12 +8,14 @@ using System.Dynamic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Web.Http; using JSONAPI.Extensions; namespace JSONAPI.Json @@ -481,6 +483,15 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J #region Deserialization + private class BadRequestException : Exception + { + public BadRequestException(string message) + : base(message) + { + + } + } + public override Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) { return Task.FromResult(ReadFromStream(type, readStream, content, formatterLogger)); ; @@ -502,17 +513,19 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, { var effectiveEncoding = SelectCharacterEncoding(contentHeaders); - JsonReader reader = this.CreateJsonReader(typeof(IDictionary), readStream, effectiveEncoding); + JsonReader reader = this.CreateJsonReader(typeof (IDictionary), readStream, + effectiveEncoding); JsonSerializer serializer = this.CreateJsonSerializer(); reader.Read(); - if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("Document root is not an object!"); + if (reader.TokenType != JsonToken.StartObject) + throw new JsonSerializationException("Document root is not an object!"); while (reader.Read()) { if (reader.TokenType == JsonToken.PropertyName) { - string value = (string)reader.Value; + string value = (string) reader.Value; reader.Read(); // burn the PropertyName token switch (value) { @@ -530,15 +543,18 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, // Could be a single resource or multiple, according to spec! if (reader.TokenType == JsonToken.StartArray) { - Type listType = (typeof(List<>)).MakeGenericType(singleType); - retval = (IList)Activator.CreateInstance(listType); + Type listType = (typeof (List<>)).MakeGenericType(singleType); + retval = (IList) Activator.CreateInstance(listType); reader.Read(); // Burn off StartArray token while (reader.TokenType == JsonToken.StartObject) { - ((IList)retval).Add(Deserialize(singleType, readStream, reader, serializer)); + ((IList) retval).Add(Deserialize(singleType, readStream, reader, serializer)); } // burn EndArray token... - if (reader.TokenType != JsonToken.EndArray) throw new JsonReaderException(String.Format("Expected JsonToken.EndArray but got {0}", reader.TokenType)); + if (reader.TokenType != JsonToken.EndArray) + throw new JsonReaderException( + String.Format("Expected JsonToken.EndArray but got {0}", + reader.TokenType)); reader.Read(); } else @@ -569,7 +585,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, { if (!type.IsAssignableFrom(retval.GetType()) && _modelManager.IsSerializedAsMany(type)) { - IList list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(singleType)); + IList list = (IList) Activator.CreateInstance(typeof (List<>).MakeGenericType(singleType)); list.Add(retval); return list; } @@ -580,6 +596,28 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, } } + catch (BadRequestException ex) + { + // We have to perform our own serialization of the error response here. + var response = new HttpResponseMessage(HttpStatusCode.BadRequest); + + var writeStream = new MemoryStream(); + response.Content = new StreamContent(writeStream); + + var effectiveEncoding = SelectCharacterEncoding(contentHeaders); + + JsonWriter writer = CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); + JsonSerializer serializer = CreateJsonSerializer(); + + var httpError = new HttpError(ex, true); // TODO: allow consumer to choose whether to include error detail + _errorSerializer.SerializeError(httpError, writeStream, writer, serializer); + + writer.Flush(); + writeStream.Flush(); + writeStream.Seek(0, SeekOrigin.Begin); + + throw new HttpResponseException(response); + } catch (Exception e) { if (formatterLogger == null) @@ -760,13 +798,47 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); if (prop != null && !prop.PropertyType.CanWriteAsJsonApiAttribute()) { + if (reader.TokenType != JsonToken.StartObject) + throw new BadRequestException("The value of the relationship must be an object."); + //FIXME: We're really assuming they're ICollections...but testing for that doesn't work for some reason. Break prone! if (prop.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)) && prop.PropertyType.IsGenericType) { // Is a hasMany - //TODO: At present only supports an array of string IDs! - JArray ids = JArray.Load(reader); + JArray ids = null; + string resourceType = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + break; + + // Not sure what else could even go here, but if it's not a property name, throw an error. + if (reader.TokenType != JsonToken.PropertyName) + throw new BadRequestException("Unexpected token: " + reader.TokenType); + + var propName = (string) reader.Value; + reader.Read(); + + if (propName == "ids") + { + ids = JArray.Load(reader); + } + else if (propName == "type") + { + resourceType = (string)reader.Value; + } + } + + // According to the spec, the ids must be specified + if (ids == null) + throw new BadRequestException("Nothing was specified for the `ids` property."); + + // We aren't doing anything with this value for now, but it needs to be present in the request payload. + // We will need to reference it to properly support polymorphism. + if (resourceType == null) + throw new BadRequestException("Nothing was specified for the `type` property."); Type relType; if (prop.PropertyType.IsGenericType) @@ -824,18 +896,59 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade foreach (JToken token in ids) { //((ICollection)prop.GetValue(obj, null)).Add(Activator.CreateInstance(relType)); - object dummyobj = Activator.CreateInstance(relType); add.Invoke(hmrel, new object[] { this.GetById(relType, token.ToObject()) }); } + + prop.SetValue(obj, hmrel); } else { // Is a belongsTo - //TODO: At present only supports a string ID! + string id = null; + string resourceType = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + break; + + // Not sure what else could even go here, but if it's not a property name, throw an error. + if (reader.TokenType != JsonToken.PropertyName) + throw new BadRequestException("Unexpected token: " + reader.TokenType); + + var propName = (string)reader.Value; + reader.Read(); + + if (propName == "id") + { + var idValue = reader.Value; + + // The id must be a string. + if (!(idValue is string)) + throw new BadRequestException("The value of the `id` property must be a string."); + + id = (string)idValue; + } + else if (propName == "type") + { + // TODO: we don't do anything with this value yet, but we will need to in order to + // support polymorphic endpoints + resourceType = (string)reader.Value; + } + } + + // The id must be specified. + if (id == null) + throw new BadRequestException("Nothing was specified for the `id` property."); + + // The type must be specified. + if (resourceType == null) + throw new BadRequestException("Nothing was specified for the `type` property."); + Type relType = prop.PropertyType; - prop.SetValue(obj, GetById(relType, (string)reader.Value)); + prop.SetValue(obj, GetById(relType, id)); } // Tell the MetadataManager that we deserialized this property From 11af2d245e93f1971d113f56ec816bae26709397 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Feb 2015 15:28:37 -0500 Subject: [PATCH 014/186] support updating to-one relationship with null --- .../Models/Post.cs | 1 - .../PutWithNullToOneUpdateRequest.json | 10 +++ ...PutWithArrayRelationshipValueResponse.json | 2 +- .../PutWithNullToOneUpdateResponse.json | 15 ++++ ...utWithStringRelationshipValueResponse.json | 2 +- .../Acceptance/PostsTests.cs | 30 +++++++ .../JSONAPI.EntityFramework.Tests.csproj | 2 + .../EntityFrameworkMaterializer.cs | 12 ++- JSONAPI/Json/JsonApiFormatter.cs | 83 +++++++++++-------- 9 files changed, 115 insertions(+), 42 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs index dbcb4363..04ba1ad9 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs @@ -17,7 +17,6 @@ public class Post public DateTimeOffset Created { get; set; } - [Required] [JsonIgnore] public string AuthorId { get; set; } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json new file mode 100644 index 00000000..97215718 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json @@ -0,0 +1,10 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "author": null + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json index 62bf2d23..f7fd4197 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "The value of the relationship must be an object.", + "detail": "The value of a to-many relationship must be an object.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json new file mode 100644 index 00000000..a42619b9 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json @@ -0,0 +1,15 @@ +{ + "posts": [ + { + "id": "202", + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00", + "links": { + "author": null, + "comments": [ "104" ], + "tags": [ "302", "303" ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json index 62bf2d23..ce669b8d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "The value of the relationship must be an object.", + "detail": "The value of a to-one relationship must be an object or null.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index e3a36966..10d69bf3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -182,6 +182,36 @@ public async Task PutWithToOneUpdate() } } + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithNullToOneUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithNullToOneUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().BeNull(); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + [TestMethod] [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 1bcbff61..8205cabe 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -156,6 +156,8 @@ + + Designer diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index cca5b1f4..4ef98036 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -348,10 +348,16 @@ private async Task Merge (Type type, object ephemeral, object material) if (materialKey != ephemeralKey) { - object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null); + if (ephemeralKey == null) + { + prop.SetValue(material, null, null); + } + else + { + object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); + prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null); + } } - // else, } else { diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index a9dc606c..e26016cb 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -798,14 +798,14 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); if (prop != null && !prop.PropertyType.CanWriteAsJsonApiAttribute()) { - if (reader.TokenType != JsonToken.StartObject) - throw new BadRequestException("The value of the relationship must be an object."); - //FIXME: We're really assuming they're ICollections...but testing for that doesn't work for some reason. Break prone! if (prop.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)) && prop.PropertyType.IsGenericType) { // Is a hasMany + if (reader.TokenType != JsonToken.StartObject) + throw new BadRequestException("The value of a to-many relationship must be an object."); + JArray ids = null; string resourceType = null; @@ -905,50 +905,61 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade { // Is a belongsTo - string id = null; - string resourceType = null; - - while (reader.Read()) + if (reader.TokenType == JsonToken.StartObject) { - if (reader.TokenType == JsonToken.EndObject) - break; + string id = null; + string resourceType = null; - // Not sure what else could even go here, but if it's not a property name, throw an error. - if (reader.TokenType != JsonToken.PropertyName) - throw new BadRequestException("Unexpected token: " + reader.TokenType); + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + break; - var propName = (string)reader.Value; - reader.Read(); + // Not sure what else could even go here, but if it's not a property name, throw an error. + if (reader.TokenType != JsonToken.PropertyName) + throw new BadRequestException("Unexpected token: " + reader.TokenType); - if (propName == "id") - { - var idValue = reader.Value; + var propName = (string)reader.Value; + reader.Read(); - // The id must be a string. - if (!(idValue is string)) - throw new BadRequestException("The value of the `id` property must be a string."); + if (propName == "id") + { + var idValue = reader.Value; - id = (string)idValue; - } - else if (propName == "type") - { - // TODO: we don't do anything with this value yet, but we will need to in order to - // support polymorphic endpoints - resourceType = (string)reader.Value; + // The id must be a string. + if (!(idValue is string)) + throw new BadRequestException("The value of the `id` property must be a string."); + + id = (string)idValue; + } + else if (propName == "type") + { + // TODO: we don't do anything with this value yet, but we will need to in order to + // support polymorphic endpoints + resourceType = (string)reader.Value; + } } - } - // The id must be specified. - if (id == null) - throw new BadRequestException("Nothing was specified for the `id` property."); + // The id must be specified. + if (id == null) + throw new BadRequestException("Nothing was specified for the `id` property."); - // The type must be specified. - if (resourceType == null) - throw new BadRequestException("Nothing was specified for the `type` property."); + // The type must be specified. + if (resourceType == null) + throw new BadRequestException("Nothing was specified for the `type` property."); - Type relType = prop.PropertyType; + Type relType = prop.PropertyType; - prop.SetValue(obj, GetById(relType, id)); + prop.SetValue(obj, GetById(relType, id)); + } + else if (reader.TokenType == JsonToken.Null) + { + prop.SetValue(obj, null); + } + else + { + throw new BadRequestException("The value of a to-one relationship must be an object or null."); + } } // Tell the MetadataManager that we deserialized this property From 9d64c7f0dba20c26334d6d4280a3980fba0361dd Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 21 Feb 2015 21:51:43 -0500 Subject: [PATCH 015/186] support data member in to-many link relationship objects --- .../PutWithToManyEmptyDataUpdateRequest.json | 12 +++ ...ithToManyHomogeneousDataUpdateRequest.json | 21 +++++ .../PutWithMissingToManyIdsResponse.json | 2 +- .../PutWithMissingToManyTypeResponse.json | 2 +- .../PutWithToManyEmptyDataUpdateResponse.json | 15 ++++ ...thToManyHomogeneousDataUpdateResponse.json | 15 ++++ .../Acceptance/PostsTests.cs | 60 +++++++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 4 + JSONAPI/Json/JsonApiFormatter.cs | 77 ++++++++++++++++--- 9 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json new file mode 100644 index 00000000..6ceb7544 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json @@ -0,0 +1,12 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": { + "data": [] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json new file mode 100644 index 00000000..b72a0dfd --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json @@ -0,0 +1,21 @@ +{ + "posts": [ + { + "id": "202", + "links": { + "tags": { + "data": [ + { + "id": "301", + "type": "tags" + }, + { + "id": "303", + "type": "tags" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json index a42e78e8..c247c761 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Nothing was specified for the `ids` property.", + "detail": "If `data` is not specified, then `ids` must be specified.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json index 18b4f910..5e05ae2c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Nothing was specified for the `type` property.", + "detail": "If `data` is not specified, then `type` must be specified.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json new file mode 100644 index 00000000..6ed335a1 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json @@ -0,0 +1,15 @@ +{ + "posts": [ + { + "id": "202", + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00", + "links": { + "author": "401", + "comments": [ "104" ], + "tags": [ ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json new file mode 100644 index 00000000..260fcda1 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json @@ -0,0 +1,15 @@ +{ + "posts": [ + { + "id": "202", + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00", + "links": { + "author": "401", + "comments": [ "104" ], + "tags": [ "303", "301" ] + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index 10d69bf3..f61b7dfc 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -152,6 +152,66 @@ public async Task PutWithToManyUpdate() } } + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithToManyHomogeneousDataUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyHomogeneousDataUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyHomogeneousDataUpdateResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("301", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PutWithToManyEmptyDataUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyEmptyDataUpdateRequest.json"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyEmptyDataUpdateResponse.json"); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Should().BeEmpty(); + } + } + } + [TestMethod] [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 8205cabe..332bacb0 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -158,6 +158,10 @@ + + + + Designer diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index e26016cb..f3402b97 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -808,6 +808,7 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade JArray ids = null; string resourceType = null; + JArray relatedObjects = null; while (reader.Read()) { @@ -823,22 +824,32 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade if (propName == "ids") { + if (reader.TokenType != JsonToken.StartArray) + throw new BadRequestException("The value of `ids` must be an array."); + ids = JArray.Load(reader); } else if (propName == "type") { + if (reader.TokenType != JsonToken.String) + throw new BadRequestException("Unexpected value for `type`: " + reader.TokenType); + resourceType = (string)reader.Value; } + else if (propName == "data") + { + if (reader.TokenType != JsonToken.StartArray) + throw new BadRequestException("Unexpected value for `data`: " + reader.TokenType); + + relatedObjects = JArray.Load(reader); + } + else + { + throw new BadRequestException("Unexpected property name: " + propName); + } } - // According to the spec, the ids must be specified - if (ids == null) - throw new BadRequestException("Nothing was specified for the `ids` property."); - - // We aren't doing anything with this value for now, but it needs to be present in the request payload. - // We will need to reference it to properly support polymorphism. - if (resourceType == null) - throw new BadRequestException("Nothing was specified for the `type` property."); + var relatedStubs = new List(); Type relType; if (prop.PropertyType.IsGenericType) @@ -851,6 +862,51 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade relType = prop.PropertyType.GetElementType(); } + // According to the spec, either the type and ids or data must be specified + if (relatedObjects != null) + { + if (ids != null) + throw new BadRequestException("If `data` is specified, then `ids` may not be."); + + if (resourceType != null) + throw new BadRequestException("If `data` is specified, then `type` may not be."); + + foreach (var relatedObject in relatedObjects) + { + if (!(relatedObject is JObject)) + throw new BadRequestException("Each element in the `data` array must be an object."); + + var relatedObjectType = relatedObject["type"] as JValue; + if (relatedObjectType == null || relatedObjectType.Type != JTokenType.String) + throw new BadRequestException("Each element in the `data` array must have a string value for the key `type`."); + + var relatedObjectId = relatedObject["id"] as JValue; + if (relatedObjectId == null || relatedObjectId.Type != JTokenType.String) + throw new BadRequestException("Each element in the `data` array must have a string value for the key `id`."); + + var relatedObjectIdValue = relatedObjectId.Value(); + if (string.IsNullOrWhiteSpace(relatedObjectIdValue)) + throw new BadRequestException("The value for `id` must be specified."); + + var stub = GetById(relType, relatedObjectIdValue); + relatedStubs.Add(stub); + } + } + else if (ids == null) + { + throw new BadRequestException("If `data` is not specified, then `ids` must be specified."); + } + else if (resourceType == null) + { + // We aren't doing anything with this value for now, but it needs to be present in the request payload. + // We will need to reference it to properly support polymorphism. + throw new BadRequestException("If `data` is not specified, then `type` must be specified."); + } + else + { + relatedStubs.AddRange(ids.Select(token => GetById(relType, token.ToObject()))); + } + IEnumerable hmrel = (IEnumerable)prop.GetValue(obj, null); if (hmrel == null) { @@ -893,10 +949,9 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade Type hmtype = hmrel.GetType(); MethodInfo add = hmtype.GetMethod("Add"); - foreach (JToken token in ids) + foreach (var stub in relatedStubs) { - //((ICollection)prop.GetValue(obj, null)).Add(Activator.CreateInstance(relType)); - add.Invoke(hmrel, new object[] { this.GetById(relType, token.ToObject()) }); + add.Invoke(hmrel, new [] { stub }); } prop.SetValue(obj, hmrel); From b75ea9be1aede2eac506101db70a48c150737721 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 21 Feb 2015 22:02:20 -0500 Subject: [PATCH 016/186] fix existing tests --- .../Data/DeserializeCollectionRequest.json | 14 ++++++++++---- JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json index 717ec39b..c29c7ea6 100644 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -3,15 +3,21 @@ { "id": "1", "title": "Linkbait!", - "links": { - "author": "1" + "links": { + "author": { + "type": "authors", + "id": "1" + } } }, { "id": "2", "title": "Rant #1023", - "links": { - "author": "1" + "links": { + "author": { + "type": "authors", + "id": "1" + } } } ] diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index 8c561428..12ad5ed2 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -445,7 +445,7 @@ public void DeserializeExtraPropertyTest() JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":[]}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""type"": ""posts"",""ids"": []}}}")); // Act Author a; @@ -462,7 +462,7 @@ public void DeserializeExtraRelationshipTest() JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":[],""bogus"":[""PANIC!""]}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""type"": ""posts"",""ids"": []},""bogus"":[""PANIC!""]}}}")); // Act Author a; From 97ec5a533776ce79589601862ba16bd52663fc18 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 21 Feb 2015 22:21:56 -0500 Subject: [PATCH 017/186] cleanup formatter code a little --- JSONAPI/Json/JsonApiFormatter.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index f3402b97..57f2a654 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -515,7 +515,6 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, var effectiveEncoding = SelectCharacterEncoding(contentHeaders); JsonReader reader = this.CreateJsonReader(typeof (IDictionary), readStream, effectiveEncoding); - JsonSerializer serializer = this.CreateJsonSerializer(); reader.Read(); if (reader.TokenType != JsonToken.StartObject) @@ -548,7 +547,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, reader.Read(); // Burn off StartArray token while (reader.TokenType == JsonToken.StartObject) { - ((IList) retval).Add(Deserialize(singleType, readStream, reader, serializer)); + ((IList) retval).Add(Deserialize(singleType, reader)); } // burn EndArray token... if (reader.TokenType != JsonToken.EndArray) @@ -562,7 +561,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, // Because we choose what to deserialize based on the ApiController method signature // (not choose the method signature based on what we're deserializing), the `type` // parameter will always be `IList` even if a single model is sent! - retval = Deserialize(singleType, readStream, reader, serializer); + retval = Deserialize(singleType, reader); } } else @@ -652,7 +651,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, return GetDefaultValueForType(type); } - public object Deserialize(Type objectType, Stream readStream, JsonReader reader, JsonSerializer serializer) + private object Deserialize(Type objectType, JsonReader reader) { object retval = Activator.CreateInstance(objectType); @@ -671,7 +670,7 @@ public object Deserialize(Type objectType, Stream readStream, JsonReader reader, { reader.Read(); // burn the PropertyName token //TODO: linked resources (Done??) - DeserializeLinkedResources(retval, readStream, reader, serializer); + DeserializeLinkedResources(retval, reader); } else if (prop != null) { @@ -782,7 +781,7 @@ select prop return retval; } - private void DeserializeLinkedResources(object obj, Stream readStream, JsonReader reader, JsonSerializer serializer) + private void DeserializeLinkedResources(object obj, JsonReader reader) { //reader.Read(); if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("'links' property is not an object!"); From 1582663b36687a753d6639f3611203b42c89ab4d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 21 Feb 2015 22:29:50 -0500 Subject: [PATCH 018/186] allow using any type as an ID property --- JSONAPI/Json/JsonApiFormatter.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 57f2a654..69687629 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -1062,14 +1062,7 @@ protected string GetValueForIdProperty(PropertyInfo idprop, object obj) { if (idprop != null) { - if (idprop.PropertyType == typeof(string)) - return (string)idprop.GetValue(obj, null); - if (idprop.PropertyType == typeof(Guid)) - return idprop.GetValue(obj).ToString(); - if (idprop.PropertyType == typeof(int)) - return ((int)idprop.GetValue(obj, null)).ToString(); - if (idprop.PropertyType == typeof(byte)) - return ((byte)idprop.GetValue(obj, null)).ToString(); + return idprop.GetValue(obj).ToString(); } return "NOIDCOMPUTABLE!"; } From 343ff012c4e4e689139f99a1416ad02eb5107c62 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 21 Feb 2015 22:35:01 -0500 Subject: [PATCH 019/186] use model manager method to determine if relationship is to-many --- JSONAPI/Json/JsonApiFormatter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 69687629..173f1b71 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -797,8 +797,7 @@ private void DeserializeLinkedResources(object obj, JsonReader reader) PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); if (prop != null && !prop.PropertyType.CanWriteAsJsonApiAttribute()) { - //FIXME: We're really assuming they're ICollections...but testing for that doesn't work for some reason. Break prone! - if (prop.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)) && prop.PropertyType.IsGenericType) + if (_modelManager.IsSerializedAsMany(prop.PropertyType)) { // Is a hasMany From 61817f4704ed1b54df029562137088fff06b3958 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 23 Feb 2015 18:08:35 -0500 Subject: [PATCH 020/186] ensure correct content-type is sent --- .../Acceptance/AcceptanceTestsBase.cs | 5 +++- JSONAPI/Json/JsonApiFormatter.cs | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index f42b124a..cafd9ca0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -1,5 +1,6 @@ using System; using System.Data.Common; +using System.Linq; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -26,7 +27,7 @@ protected static DbConnection GetEffortConnection() return TestHelpers.GetEffortConnection(@"Acceptance\Data"); } - protected async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath) + protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath) { var responseContent = await response.Content.ReadAsStringAsync(); @@ -36,6 +37,8 @@ protected async Task AssertResponseContent(HttpResponseMessage response, string redactedResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); redactedResponse.Should().Be(expectedResponse); + response.Content.Headers.ContentType.MediaType.Should().Be("application/vnd.api+json"); + response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); } #region GET diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 173f1b71..06b05261 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -43,7 +43,7 @@ internal JsonApiFormatter(IModelManager modelManager, IErrorSerializer errorSeri { _modelManager = modelManager; _errorSerializer = errorSerializer; - SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json")); + SupportedMediaTypes.Insert(0, new MediaTypeHeaderValue("application/vnd.api+json")); ValidateRawJsonStrings = true; } @@ -600,20 +600,26 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, // We have to perform our own serialization of the error response here. var response = new HttpResponseMessage(HttpStatusCode.BadRequest); - var writeStream = new MemoryStream(); - response.Content = new StreamContent(writeStream); - - var effectiveEncoding = SelectCharacterEncoding(contentHeaders); + using (var writeStream = new MemoryStream()) + { + var effectiveEncoding = SelectCharacterEncoding(contentHeaders); + JsonWriter writer = CreateJsonWriter(typeof (object), writeStream, effectiveEncoding); + JsonSerializer serializer = CreateJsonSerializer(); - JsonWriter writer = CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); - JsonSerializer serializer = CreateJsonSerializer(); + var httpError = new HttpError(ex, true); + // TODO: allow consumer to choose whether to include error detail + _errorSerializer.SerializeError(httpError, writeStream, writer, serializer); - var httpError = new HttpError(ex, true); // TODO: allow consumer to choose whether to include error detail - _errorSerializer.SerializeError(httpError, writeStream, writer, serializer); + writer.Flush(); + writeStream.Flush(); + writeStream.Seek(0, SeekOrigin.Begin); - writer.Flush(); - writeStream.Flush(); - writeStream.Seek(0, SeekOrigin.Begin); + using (var stringReader = new StreamReader(writeStream)) + { + var stringContent = stringReader.ReadToEnd(); // TODO: use async version + response.Content = new StringContent(stringContent, Encoding.UTF8, "application/vnd.api+json"); + } + } throw new HttpResponseException(response); } From 7a7414719cf61cebf1d458b507fa5908119f26e1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 23 Feb 2015 20:24:38 -0500 Subject: [PATCH 021/186] get key names without using metadata workspace --- .../EntityFrameworkMaterializerTests.cs | 21 ++++++---- .../EntityFrameworkMaterializer.cs | 38 ++++++++++++------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs index 01f9e437..52a88af4 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs @@ -4,19 +4,26 @@ using JSONAPI.EntityFramework.Tests.Models; using FluentAssertions; using System.Collections.Generic; +using System.Data.Entity; namespace JSONAPI.EntityFramework.Tests { [TestClass] public class EntityFrameworkMaterializerTests { + private class TestDbContext : DbContext + { + public DbSet Backlinks { get; set; } + public DbSet Posts { get; set; } + } + private class NotAnEntity { public string Id { get; set; } public string Temporary { get; set; } } - private TestEntities context; + private TestDbContext context; private Backlink b1, b2; [TestInitialize] @@ -26,7 +33,7 @@ public void SetupEntities() var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance; //- - context = new TestEntities(); + context = new TestDbContext(); //JSONAPI.EntityFramework.Json.ContractResolver.ObjectContext = context; @@ -72,17 +79,17 @@ public void GetKeyNamesNonStandardIdTest() } [TestMethod] - [ExpectedException(typeof(System.ArgumentException))] public void GetKeyNamesNotAnEntityTest() { // Arrange var materializer = new EntityFrameworkMaterializer(context); // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(NotAnEntity)); - - // Assert - Assert.Fail("A System.ArgumentException should be thrown, this assertion should be unreachable!"); + Action action = () => + { + materializer.GetKeyNames(typeof (NotAnEntity)); + }; + action.ShouldThrow().Which.Message.Should().Be("The Type NotAnEntity was not found in the DbContext with Type TestDbContext"); } } } diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index cca5b1f4..b8b846ef 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -186,26 +186,36 @@ private Type GetSingleType(Type type) protected internal virtual IEnumerable GetKeyNames(Type type) { - ObjectContext objectContext = ((IObjectContextAdapter)this.context).ObjectContext; - System.Data.Entity.Core.Metadata.Edm.EdmType meta; - try { - meta = objectContext.MetadataWorkspace - .GetType(type.Name, type.Namespace, System.Data.Entity.Core.Metadata.Edm.DataSpace.CSpace); + var openMethod = typeof (EntityFrameworkMaterializer).GetMethod("GetKeyNamesFromGeneric", BindingFlags.NonPublic | BindingFlags.Static); + var method = openMethod.MakeGenericMethod(type); + try + { + return (IEnumerable)method.Invoke(null, new object[] { this.context }); } - catch (System.ArgumentException e) + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + // ReSharper disable once UnusedMember.Local + private static IEnumerable GetKeyNamesFromGeneric(DbContext dbContext) where T : class + { + var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; + ObjectSet objectSet; + try + { + objectSet = objectContext.CreateObjectSet(); + + } + catch (InvalidOperationException e) { throw new ArgumentException( - String.Format("The Type {0} was not found in the DbContext with Type {1}", type.Name, this.context.GetType().Name), + String.Format("The Type {0} was not found in the DbContext with Type {1}", typeof(T).Name, dbContext.GetType().Name), e ); } - var members = (IEnumerable)meta - .MetadataProperties - .Where(mp => mp.Name == "KeyMembers") - .First() - .Value; - IEnumerable retval = members.Select(m => m.Name); - return retval; + return objectSet.EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); } /// From c6946def94eda066c5a1d85963981eaa3a815f0e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 24 Feb 2015 10:53:09 -0500 Subject: [PATCH 022/186] clean up EF Materializer a little bit --- .../EntityFrameworkMaterializer.cs | 42 +++++++++++-------- .../EntityFrameworkMaterializer_Util.cs | 4 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 99287854..72afca42 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Data.Entity; using JSONAPI.Core; @@ -13,18 +12,23 @@ namespace JSONAPI.EntityFramework { + /// + /// IMaterializer implementation for use with Entity Framework + /// public partial class EntityFrameworkMaterializer : IMaterializer { - private DbContext context; - - public DbContext DbContext - { - get { return this.context; } - } + /// + /// The DbContext instance used to perform materializer operations + /// + public DbContext DbContext { get; private set; } - public EntityFrameworkMaterializer(DbContext context) : base() + /// + /// Creates a new EntityFrameworkMaterializer. + /// + /// The DbContext instance used to perform materializer operations + public EntityFrameworkMaterializer(DbContext context) { - this.context = context; + DbContext = context; } #region IMaterializer contract methods @@ -58,7 +62,7 @@ public virtual Task GetByIdAsync(Type type, params Object[] idValues) i++; } } - return this.context.Set(type).FindAsync(idv2); + return DbContext.Set(type).FindAsync(idv2); } public async Task GetByIdAsync(params Object[] idValues) @@ -90,13 +94,13 @@ public virtual async Task MaterializeAsync(Type type, object ephemeral) object retval = null; if (!anyNull) { - retval = await context.Set(type).FindAsync(idValues.ToArray()); + retval = await DbContext.Set(type).FindAsync(idValues.ToArray()); } if (retval == null) { // Didn't find it...create a new one! retval = Activator.CreateInstance(type); - context.Set(type).Add(retval); + DbContext.Set(type).Add(retval); if (!anyNull) { // For a new object, if a key is specified, we want to merge the key, at least. @@ -190,7 +194,7 @@ protected internal virtual IEnumerable GetKeyNames(Type type) var method = openMethod.MakeGenericMethod(type); try { - return (IEnumerable)method.Invoke(null, new object[] { this.context }); + return (IEnumerable)method.Invoke(null, new object[] { DbContext }); } catch (TargetInvocationException ex) { @@ -234,9 +238,14 @@ private IEnumerable GetKeyProperties(Type type) return retval; } + /// + /// Gets the name of the entity set property on the db context corresponding to the given type. + /// + /// Type type to get the entity set name for. + /// The name of the entity set property protected string GetEntitySetName(Type type) { - ObjectContext objectContext = ((IObjectContextAdapter)this.context).ObjectContext; + ObjectContext objectContext = ((IObjectContextAdapter)DbContext).ObjectContext; try { var container = objectContext.MetadataWorkspace @@ -304,7 +313,6 @@ private async Task Merge (Type type, object ephemeral, object material) if (IsMany(prop.PropertyType)) { Type elementType = GetSingleType(prop.PropertyType); - IEnumerable keyNames = GetKeyNames(elementType); var materialMany = (IEnumerable)prop.GetValue(material, null); var ephemeralMany = (IEnumerable)prop.GetValue(ephemeral, null); @@ -334,7 +342,7 @@ private async Task Merge (Type type, object ephemeral, object material) { object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); object obj = await GetByIdAsync(elementType, idParams); - mmadd.Invoke(materialMany, new object[] { obj }); + mmadd.Invoke(materialMany, new [] { obj }); } // Remove from hasMany if (mmremove != null) @@ -342,7 +350,7 @@ private async Task Merge (Type type, object ephemeral, object material) { object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); object obj = await GetByIdAsync(elementType, idParams); - mmremove.Invoke(materialMany, new object[] { obj }); + mmremove.Invoke(materialMany, new [] { obj }); } } else if(IsModel(prop.PropertyType)) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs index db35bfc8..a40440e5 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs @@ -18,7 +18,7 @@ public partial class EntityFrameworkMaterializer public T GetDetachedOriginal(T entity, bool fixupRelationships = false) where T : class { - DbPropertyValues originalValues = this.context.Entry(entity).OriginalValues; + DbPropertyValues originalValues = DbContext.Entry(entity).OriginalValues; T orig = (T)originalValues.ToObject(); if (fixupRelationships) { @@ -29,7 +29,7 @@ public T GetDetachedOriginal(T entity, bool fixupRelationships = false) public IEnumerable GetAssociationChanges(T1 parent, string propertyName, EntityState findState) { - ObjectContext ocontext = ((IObjectContextAdapter)this.context).ObjectContext; + ObjectContext ocontext = ((IObjectContextAdapter)DbContext).ObjectContext; MetadataWorkspace metadataWorkspace = ocontext.MetadataWorkspace; // Find the AssociationType that matches the property traits given as input From 5eaeb68e16db3e8690bf07cef1f9c4bb1799b0e4 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 24 Feb 2015 12:11:19 -0500 Subject: [PATCH 023/186] inject IMetadataManager onto EntityFrameworkMaterializer --- .../Controllers/CommentsController.cs | 2 +- .../Controllers/PostsController.cs | 2 +- .../Controllers/TagsController.cs | 2 +- .../Controllers/UserGroupsController.cs | 2 +- .../Controllers/UsersController.cs | 2 +- .../EntityConverterTests.cs | 2 +- .../EntityFrameworkMaterializerTests.cs | 11 +++++++--- .../EntityFrameworkMaterializer.cs | 7 +++++-- JSONAPI.EntityFramework/Http/ApiController.cs | 4 +++- JSONAPI/Core/IMetadataManager.cs | 20 +++++++++++++++++++ JSONAPI/Core/MetadataManager.cs | 11 ++-------- JSONAPI/JSONAPI.csproj | 1 + 12 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 JSONAPI/Core/IMetadataManager.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs index 73fe5d25..3aa108ef 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs @@ -15,7 +15,7 @@ public CommentsController(TestDbContext dbContext) protected override IMaterializer MaterializerFactory() { - return new EntityFrameworkMaterializer(DbContext); + return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs index 0d368070..728266ad 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs @@ -15,7 +15,7 @@ public PostsController(TestDbContext dbContext) protected override IMaterializer MaterializerFactory() { - return new EntityFrameworkMaterializer(DbContext); + return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs index b20ceb00..e17978d8 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs @@ -15,7 +15,7 @@ public TagsController(TestDbContext dbContext) protected override IMaterializer MaterializerFactory() { - return new EntityFrameworkMaterializer(DbContext); + return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs index 4fafe7c2..d4a31b72 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs @@ -15,7 +15,7 @@ public UserGroupsController(TestDbContext dbContext) protected override IMaterializer MaterializerFactory() { - return new EntityFrameworkMaterializer(DbContext); + return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs index b074f272..8bc13b8e 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs @@ -15,7 +15,7 @@ public UsersController(TestDbContext dbContext) protected override IMaterializer MaterializerFactory() { - return new EntityFrameworkMaterializer(DbContext); + return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index 88c83713..e9cdf706 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -126,7 +126,7 @@ public async Task UnderpostingTest() JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); MemoryStream stream = new MemoryStream(); - EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); + EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context, MetadataManager.Instance); string underpost = @"{""posts"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}"; stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(underpost)); diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs index 52a88af4..bd7122f3 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs @@ -5,6 +5,8 @@ using FluentAssertions; using System.Collections.Generic; using System.Data.Entity; +using JSONAPI.Core; +using Moq; namespace JSONAPI.EntityFramework.Tests { @@ -54,7 +56,8 @@ public void SetupEntities() public void GetKeyNamesStandardIdTest() { // Arrange - var materializer = new EntityFrameworkMaterializer(context); + var mockMetadataManager = new Mock(MockBehavior.Strict); + var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); // Act IEnumerable keyNames = materializer.GetKeyNames(typeof(Post)); @@ -68,7 +71,8 @@ public void GetKeyNamesStandardIdTest() public void GetKeyNamesNonStandardIdTest() { // Arrange - var materializer = new EntityFrameworkMaterializer(context); + var mockMetadataManager = new Mock(MockBehavior.Strict); + var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); // Act IEnumerable keyNames = materializer.GetKeyNames(typeof(Backlink)); @@ -82,7 +86,8 @@ public void GetKeyNamesNonStandardIdTest() public void GetKeyNamesNotAnEntityTest() { // Arrange - var materializer = new EntityFrameworkMaterializer(context); + var mockMetadataManager = new Mock(MockBehavior.Strict); + var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); // Act Action action = () => diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 72afca42..d7efc309 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -17,6 +17,8 @@ namespace JSONAPI.EntityFramework /// public partial class EntityFrameworkMaterializer : IMaterializer { + private readonly IMetadataManager _metadataManager; + /// /// The DbContext instance used to perform materializer operations /// @@ -26,8 +28,9 @@ public partial class EntityFrameworkMaterializer : IMaterializer /// Creates a new EntityFrameworkMaterializer. /// /// The DbContext instance used to perform materializer operations - public EntityFrameworkMaterializer(DbContext context) + public EntityFrameworkMaterializer(DbContext context, IMetadataManager metadataManager) { + _metadataManager = metadataManager; DbContext = context; } @@ -308,7 +311,7 @@ private async Task Merge (Type type, object ephemeral, object material) foreach (PropertyInfo prop in props) { // Comply with the spec, if a key was not set, it should not be updated! - if (!MetadataManager.Instance.PropertyWasPresent(ephemeral, prop)) continue; + if (!_metadataManager.PropertyWasPresent(ephemeral, prop)) continue; if (IsMany(prop.PropertyType)) { diff --git a/JSONAPI.EntityFramework/Http/ApiController.cs b/JSONAPI.EntityFramework/Http/ApiController.cs index c8d140c1..491ffef8 100644 --- a/JSONAPI.EntityFramework/Http/ApiController.cs +++ b/JSONAPI.EntityFramework/Http/ApiController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using JSONAPI.Core; namespace JSONAPI.EntityFramework.Http { @@ -18,7 +19,8 @@ protected override JSONAPI.Core.IMaterializer MaterializerFactory() if (_materializer == null) { DbContext context = (DbContext)Activator.CreateInstance(typeof(TC)); - _materializer = new JSONAPI.EntityFramework.EntityFrameworkMaterializer(context); + var metadataManager = MetadataManager.Instance; + _materializer = new JSONAPI.EntityFramework.EntityFrameworkMaterializer(context, metadataManager); } return _materializer; } diff --git a/JSONAPI/Core/IMetadataManager.cs b/JSONAPI/Core/IMetadataManager.cs new file mode 100644 index 00000000..4b471405 --- /dev/null +++ b/JSONAPI/Core/IMetadataManager.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// Manages request-specific metadata + /// + public interface IMetadataManager + { + /// + /// Find whether or not a given property was + /// posted in the original JSON--i.e. to determine whether an update operation should be + /// performed, and/or if a default value should be used. + /// + /// The object deserialized by JsonApiFormatter + /// The property to check + /// Whether or not the property was found in the original JSON and set by the deserializer + bool PropertyWasPresent(object deserialized, PropertyInfo prop); + } +} diff --git a/JSONAPI/Core/MetadataManager.cs b/JSONAPI/Core/MetadataManager.cs index d1541d52..f27136c7 100644 --- a/JSONAPI/Core/MetadataManager.cs +++ b/JSONAPI/Core/MetadataManager.cs @@ -8,7 +8,7 @@ namespace JSONAPI.Core { - public sealed class MetadataManager + public sealed class MetadataManager : IMetadataManager { #region Singleton pattern @@ -64,14 +64,7 @@ internal Dictionary DeserializationMetadata(object deserialized) } } - /// - /// Find whether or not a given property was - /// posted in the original JSON--i.e. to determine whether an update operation should be - /// performed, and/or if a default value should be used. - /// - /// The object deserialized by JsonApiFormatter - /// The property to check - /// Whether or not the property was found in the original JSON and set by the deserializer + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool PropertyWasPresent(object deserialized, PropertyInfo prop) { diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 351fa116..5350137e 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -73,6 +73,7 @@ + From 6988ba64e66a4bfd345756b5d041cbe800397ee6 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 25 Feb 2015 12:01:57 +0100 Subject: [PATCH 024/186] Fixes EntityFrameworkMaterializer bugs when materializing Decimal, etc. --- .../EntityFrameworkMaterializer.cs | 11 ++--------- JSONAPI/Extensions/TypeExtensions.cs | 4 ++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index b8b846ef..c9810fb8 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Collections; using System.Data.Entity.Core; +using JSONAPI.Extensions; namespace JSONAPI.EntityFramework { @@ -163,15 +164,7 @@ private bool IsMany(Type type) public bool IsModel(Type objectType) { - if (objectType.IsPrimitive - || typeof(System.Guid).IsAssignableFrom(objectType) - || typeof(System.DateTime).IsAssignableFrom(objectType) - || typeof(System.DateTimeOffset).IsAssignableFrom(objectType) - || typeof(System.Guid?).IsAssignableFrom(objectType) - || typeof(System.DateTime?).IsAssignableFrom(objectType) - || typeof(System.DateTimeOffset?).IsAssignableFrom(objectType) - || typeof(String).IsAssignableFrom(objectType) - ) + if (objectType.CanWriteAsJsonApiAttribute()) return false; else return true; } diff --git a/JSONAPI/Extensions/TypeExtensions.cs b/JSONAPI/Extensions/TypeExtensions.cs index d116163a..5646c68e 100644 --- a/JSONAPI/Extensions/TypeExtensions.cs +++ b/JSONAPI/Extensions/TypeExtensions.cs @@ -2,9 +2,9 @@ namespace JSONAPI.Extensions { - internal static class TypeExtensions + public static class TypeExtensions { - internal static bool CanWriteAsJsonApiAttribute(this Type objectType) + public static bool CanWriteAsJsonApiAttribute(this Type objectType) { if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof (Nullable<>)) objectType = objectType.GetGenericArguments()[0]; From 85d3a04af7d205ac7ab1ce55b2c71c16389172ae Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 13:49:36 -0500 Subject: [PATCH 025/186] refactor acceptance tests This simplifies the boilerplate for acceptance tests by requiring a single statement to assert both the expected response content and the status code. --- .../Acceptance/AcceptanceTestsBase.cs | 5 +- .../Acceptance/PostsTests.cs | 48 +++++++------------ .../Acceptance/SortingTests.cs | 24 ++++------ .../Acceptance/UserGroupsTests.cs | 3 +- 4 files changed, 29 insertions(+), 51 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index cafd9ca0..f1c5e065 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -1,6 +1,7 @@ using System; using System.Data.Common; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -27,7 +28,7 @@ protected static DbConnection GetEffortConnection() return TestHelpers.GetEffortConnection(@"Acceptance\Data"); } - protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath) + protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); @@ -39,6 +40,8 @@ protected static async Task AssertResponseContent(HttpResponseMessage response, redactedResponse.Should().Be(expectedResponse); response.Content.Headers.ContentType.MediaType.Should().Be("application/vnd.api+json"); response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); + + response.StatusCode.Should().Be(expectedStatusCode); } #region GET diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index f61b7dfc..f9e5ed79 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -24,8 +24,7 @@ public async Task GetAll() { var response = await SubmitGet(effortConnection, "posts"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json", HttpStatusCode.OK); } } @@ -41,8 +40,7 @@ public async Task GetWithFilter() { var response = await SubmitGet(effortConnection, "posts?title=Post 4"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json", HttpStatusCode.OK); } } @@ -58,8 +56,7 @@ public async Task GetById() { var response = await SubmitGet(effortConnection, "posts/202"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json", HttpStatusCode.OK); } } @@ -75,8 +72,7 @@ public async Task Post() { var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Requests\PostRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PostResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PostResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -104,8 +100,7 @@ public async Task PutWithAttributeUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithAttributeUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithAttributeUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithAttributeUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -134,8 +129,7 @@ public async Task PutWithToManyUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -164,8 +158,7 @@ public async Task PutWithToManyHomogeneousDataUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyHomogeneousDataUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyHomogeneousDataUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -194,8 +187,7 @@ public async Task PutWithToManyEmptyDataUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyEmptyDataUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyEmptyDataUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyEmptyDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -224,8 +216,7 @@ public async Task PutWithToOneUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToOneUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToOneUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -254,8 +245,7 @@ public async Task PutWithNullToOneUpdate() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithNullToOneUpdateRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -284,8 +274,7 @@ public async Task PutWithMissingToOneId() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneIdRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneIdResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneIdResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -314,8 +303,7 @@ public async Task PutWithMissingToOneType() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneTypeRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneTypeResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneTypeResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -344,8 +332,7 @@ public async Task PutWithMissingToManyIds() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyIdsRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyIdsResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyIdsResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -374,8 +361,7 @@ public async Task PutWithMissingToManyType() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyTypeRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyTypeResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyTypeResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -404,8 +390,7 @@ public async Task PutWithArrayRelationshipValue() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithArrayRelationshipValueRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithArrayRelationshipValueResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -434,8 +419,7 @@ public async Task PutWithStringRelationshipValue() { var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithStringRelationshipValueRequest.json"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithStringRelationshipValueResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs index 2b9adba3..9bc23161 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs @@ -20,8 +20,7 @@ public async Task GetSortedAscending() { var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json", HttpStatusCode.OK); } } @@ -37,8 +36,7 @@ public async Task GetSortedDesending() { var response = await SubmitGet(effortConnection, "users?sort=-firstName"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json", HttpStatusCode.OK); } } @@ -54,8 +52,7 @@ public async Task GetSortedByMultipleAscending() { var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,%2BfirstName"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json", HttpStatusCode.OK); } } @@ -71,8 +68,7 @@ public async Task GetSortedByMultipleDescending() { var response = await SubmitGet(effortConnection, "users?sort=-lastName,-firstName"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json", HttpStatusCode.OK); } } @@ -88,8 +84,7 @@ public async Task GetSortedByMixedDirection() { var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,-firstName"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json", HttpStatusCode.OK); } } @@ -105,8 +100,7 @@ public async Task GetSortedByUnknownColumn() { var response = await SubmitGet(effortConnection, "users?sort=%2Bfoobar"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest); } } @@ -122,8 +116,7 @@ public async Task GetSortedBySameColumnTwice() { var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName,%2BfirstName"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest); } } @@ -139,8 +132,7 @@ public async Task GetSortedByColumnMissingDirection() { var response = await SubmitGet(effortConnection, "users?sort=firstName"); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json", HttpStatusCode.BadRequest); } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs index 996a27ea..4e93bf78 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs @@ -16,8 +16,7 @@ public async Task Get() { var response = await SubmitGet(effortConnection, "user-groups"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await AssertResponseContent(response, @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json"); + await AssertResponseContent(response, @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json", HttpStatusCode.OK); } } } From 42392cd8ba0d314b9f2d502c53019768d26f63df Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 13:27:37 -0500 Subject: [PATCH 026/186] require registration of resource types --- .../Startup.cs | 5 ++ .../EntityConverterTests.cs | 10 ++- .../EnableSortingAttributeTests.cs | 1 + JSONAPI.Tests/Core/MetadataManagerTests.cs | 4 +- JSONAPI.Tests/Core/ModelManagerTests.cs | 75 ++++++++++++++-- .../Json/JsonApiMediaFormatterTests.cs | 85 ++++++++++++------- JSONAPI.Tests/Json/LinkTemplateTests.cs | 8 +- JSONAPI.TodoMVC.API/Startup.cs | 2 + .../ActionFilters/EnableSortingAttribute.cs | 2 +- JSONAPI/Core/IModelManager.cs | 15 +++- JSONAPI/Core/ModelManager.cs | 84 ++++++++++++++---- JSONAPI/Json/JsonApiFormatter.cs | 6 +- 12 files changed, 229 insertions(+), 68 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index 513796ab..8487230c 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -66,6 +66,11 @@ private static HttpConfiguration GetWebApiConfiguration() var pluralizationService = new PluralizationService(); var modelManager = new ModelManager(pluralizationService); + modelManager.RegisterResourceType(typeof(Comment)); + modelManager.RegisterResourceType(typeof(Post)); + modelManager.RegisterResourceType(typeof(Tag)); + modelManager.RegisterResourceType(typeof(User)); + modelManager.RegisterResourceType(typeof(UserGroup)); var formatter = new JsonApiFormatter(modelManager); config.Formatters.Clear(); diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index e9cdf706..3ce95e56 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -107,7 +107,10 @@ public void SetupEntities() public void SerializeTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + var modelManager = new ModelManager(new Core.PluralizationService()); + modelManager.RegisterResourceType(typeof(Post)); + + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act @@ -123,7 +126,10 @@ public void SerializeTest() public async Task UnderpostingTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + var modelManager = new ModelManager(new Core.PluralizationService()); + modelManager.RegisterResourceType(typeof(Post)); + + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context, MetadataManager.Instance); diff --git a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs index e85060c8..03a86adf 100644 --- a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs @@ -75,6 +75,7 @@ private HttpResponseMessage GetActionFilterResponse(string uri) { { "Dummy", "Dummies" } })); + modelManager.RegisterResourceType(typeof(Dummy)); var filter = new EnableSortingAttribute(modelManager); diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs index 733f5706..fbbe17a3 100644 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ b/JSONAPI.Tests/Core/MetadataManagerTests.cs @@ -16,7 +16,9 @@ public void PropertyWasPresentTest() using (var inputStream = File.OpenRead("MetadataManagerPropertyWasPresentRequest.json")) { // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Post)); + JsonApiFormatter formatter = new JsonApiFormatter(modelManager); var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 22cfe4d0..832f5d46 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Collections.Generic; using System.Collections; +using FluentAssertions; namespace JSONAPI.Tests.Core { @@ -64,18 +65,22 @@ public void FindsIdFromAttribute() } [TestMethod] - public void GetJsonKeyForTypeTest() + public void GetResourceTypeName_returns_correct_value_for_registered_types() { // Arrange var pluralizationService = new PluralizationService(); var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + mm.RegisterResourceType(typeof(Author)); + mm.RegisterResourceType(typeof(Comment)); + mm.RegisterResourceType(typeof(UserGroup)); // Act - var postKey = mm.GetJsonKeyForType(typeof(Post)); - var authorKey = mm.GetJsonKeyForType(typeof(Author)); - var commentKey = mm.GetJsonKeyForType(typeof(Comment)); - var manyCommentKey = mm.GetJsonKeyForType(typeof(Comment[])); - var userGroupsKey = mm.GetJsonKeyForType(typeof(UserGroup)); + var postKey = mm.GetResourceTypeNameForType(typeof(Post)); + var authorKey = mm.GetResourceTypeNameForType(typeof(Author)); + var commentKey = mm.GetResourceTypeNameForType(typeof(Comment)); + var manyCommentKey = mm.GetResourceTypeNameForType(typeof(Comment[])); + var userGroupsKey = mm.GetResourceTypeNameForType(typeof(UserGroup)); // Assert Assert.AreEqual("posts", postKey); @@ -85,6 +90,64 @@ public void GetJsonKeyForTypeTest() Assert.AreEqual("user-groups", userGroupsKey); } + [TestMethod] + public void GetResourceTypeNameForType_fails_when_getting_unregistered_type() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + Action action = () => + { + mm.GetResourceTypeNameForType(typeof(Post)); + }; + + // Assert + action.ShouldThrow().WithMessage("The type `JSONAPI.Tests.Models.Post` was not registered."); + } + + [TestMethod] + public void GetTypeByResourceTypeName_returns_correct_value_for_registered_names() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + mm.RegisterResourceType(typeof(Author)); + mm.RegisterResourceType(typeof(Comment)); + mm.RegisterResourceType(typeof(UserGroup)); + + // Act + var postType = mm.GetTypeByResourceTypeName("posts"); + var authorType = mm.GetTypeByResourceTypeName("authors"); + var commentType = mm.GetTypeByResourceTypeName("comments"); + var userGroupType = mm.GetTypeByResourceTypeName("user-groups"); + + // Assert + postType.Should().Be(typeof (Post)); + authorType.Should().Be(typeof (Author)); + commentType.Should().Be(typeof (Comment)); + userGroupType.Should().Be(typeof (UserGroup)); + } + + [TestMethod] + public void GetTypeByResourceTypeName_fails_when_getting_unregistered_name() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + Action action = () => + { + mm.GetTypeByResourceTypeName("posts"); + }; + + // Assert + action.ShouldThrow().WithMessage("The resource type name `posts` was not registered."); + } + [TestMethod] public void GetJsonKeyForPropertyTest() { diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index 12ad5ed2..0060688a 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -205,16 +205,14 @@ public void SetupModels() public void SerializerIntegrationTest() { // Arrange - //PayloadConverter pc = new PayloadConverter(); - //ModelConverter mc = new ModelConverter(); - //ContractResolver.PluralizationService = new PluralizationService(); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Author)); + modelManager.RegisterResourceType(typeof(Comment)); + modelManager.RegisterResourceType(typeof(Post)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act - //Payload payload = new Payload(a.Posts); - //js.Serialize(jw, payload); formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }.ToList(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); // Assert @@ -230,16 +228,14 @@ public void SerializerIntegrationTest() public void SerializeArrayIntegrationTest() { // Arrange - //PayloadConverter pc = new PayloadConverter(); - //ModelConverter mc = new ModelConverter(); - //ContractResolver.PluralizationService = new PluralizationService(); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Author)); + modelManager.RegisterResourceType(typeof(Comment)); + modelManager.RegisterResourceType(typeof(Post)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act - //Payload payload = new Payload(a.Posts); - //js.Serialize(jw, payload); formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); // Assert @@ -255,7 +251,9 @@ public void SerializeArrayIntegrationTest() public void Serializes_attributes_properly() { // Arrang - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Sample)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act @@ -273,7 +271,9 @@ public void Serializes_attributes_properly() public void Serializes_byte_ids_properly() { // Arrang - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Tag)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act @@ -291,7 +291,9 @@ public void Serializes_byte_ids_properly() public void Reformats_raw_json_string_with_unquoted_keys() { // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act @@ -310,7 +312,9 @@ public void Reformats_raw_json_string_with_unquoted_keys() public void Does_not_serialize_malformed_raw_json_string() { // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); // Act @@ -329,7 +333,8 @@ public void Does_not_serialize_malformed_raw_json_string() public void Should_serialize_error() { // Arrange - var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer()); + var modelManager = new ModelManager(new PluralizationService()); + var formatter = new JsonApiFormatter(modelManager, new MockErrorSerializer()); var stream = new MemoryStream(); // Act @@ -348,7 +353,8 @@ public void Should_serialize_error() public void SerializeErrorIntegrationTest() { // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); var mockInnerException = new Mock(MockBehavior.Strict); @@ -384,7 +390,9 @@ public void Deserializes_collections_properly() using (var inputStream = File.OpenRead("DeserializeCollectionRequest.json")) { // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Post)); + var formatter = new JsonApiFormatter(modelManager); // Act var posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; @@ -407,7 +415,9 @@ public async Task Deserializes_attributes_properly() using (var inputStream = File.OpenRead("DeserializeAttributeRequest.json")) { // Arrange - JsonApiFormatter formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Sample)); + var formatter = new JsonApiFormatter(modelManager); // Act var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), inputStream, null, null); @@ -426,7 +436,9 @@ public async Task DeserializeRawJsonTest() using (var inputStream = File.OpenRead("DeserializeRawJsonTest.json")) { // Arrange - var formatter = new JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); // Act var comments = ((IEnumerable)await formatter.ReadFromStreamAsync(typeof (Comment), inputStream, null, null)).ToArray(); @@ -442,7 +454,10 @@ public async Task DeserializeRawJsonTest() [TestMethod(), Timeout(1000)] public void DeserializeExtraPropertyTest() { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Author)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""type"": ""posts"",""ids"": []}}}")); @@ -459,7 +474,10 @@ public void DeserializeExtraPropertyTest() [TestMethod(), Timeout(1000)] public void DeserializeExtraRelationshipTest() { - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Author)); + var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""type"": ""posts"",""ids"": []},""bogus"":[""PANIC!""]}}}")); @@ -476,7 +494,10 @@ public void DeserializeExtraRelationshipTest() [DeploymentItem(@"Data\NonStandardIdTest.json")] public void SerializeNonStandardIdTest() { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(NonStandardIdThing)); + var formatter = new JsonApiFormatter(modelManager); var stream = new MemoryStream(); var payload = new List { new NonStandardIdThing { Uuid = new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"), Data = "Swap" } @@ -498,7 +519,9 @@ public void SerializeNonStandardIdTest() [DeploymentItem(@"Data\NonStandardIdTest.json")] public void DeserializeNonStandardIdTest() { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(NonStandardIdThing)); + var formatter = new JsonApiFormatter(modelManager); var stream = new FileStream("NonStandardIdTest.json",FileMode.Open); // Act @@ -515,7 +538,9 @@ public void DeserializeNonStandardIdTest() [DeploymentItem(@"Data\NonStandardIdTest.json")] public void DeserializeNonStandardIdWithIdOnly() { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(NonStandardIdThing)); + var formatter = new JsonApiFormatter(modelManager); string json = File.ReadAllText("NonStandardIdTest.json"); json = Regex.Replace(json, @"""uuid"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,",""); // remove the uuid attribute var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); @@ -534,7 +559,9 @@ public void DeserializeNonStandardIdWithIdOnly() [DeploymentItem(@"Data\NonStandardIdTest.json")] public void DeserializeNonStandardIdWithoutId() { - var formatter = new JSONAPI.Json.JsonApiFormatter(new PluralizationService()); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(NonStandardIdThing)); + var formatter = new JsonApiFormatter(modelManager); string json = File.ReadAllText("NonStandardIdTest.json"); json = Regex.Replace(json, @"""id"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,", ""); // remove the uuid attribute var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); diff --git a/JSONAPI.Tests/Json/LinkTemplateTests.cs b/JSONAPI.Tests/Json/LinkTemplateTests.cs index e5eb0d83..dd230037 100644 --- a/JSONAPI.Tests/Json/LinkTemplateTests.cs +++ b/JSONAPI.Tests/Json/LinkTemplateTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using JSONAPI.Core; namespace JSONAPI.Tests.Json { @@ -49,10 +50,9 @@ public void SetupModels() [DeploymentItem(@"Data\LinkTemplateTest.json")] public void GetResourceWithLinkTemplateRelationship() { - var formatter = new JsonApiFormatter - ( - new JSONAPI.Core.PluralizationService() - ); + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Post)); + var formatter = new JsonApiFormatter(modelManager); var stream = new MemoryStream(); formatter.WriteToStreamAsync(typeof(Post), ThePost, stream, null, null); diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index cf1a44b4..74562992 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -5,6 +5,7 @@ using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.Http; using JSONAPI.Json; +using JSONAPI.TodoMVC.API.Models; using Owin; namespace JSONAPI.TodoMVC.API @@ -24,6 +25,7 @@ private static HttpConfiguration GetWebApiConfiguration() var config = new HttpConfiguration(); var modelManager = new ModelManager(pluralizationService); + modelManager.RegisterResourceType(typeof(Todo)); var formatter = new JsonApiFormatter(modelManager); config.Formatters.Clear(); diff --git a/JSONAPI/ActionFilters/EnableSortingAttribute.cs b/JSONAPI/ActionFilters/EnableSortingAttribute.cs index d467a38d..90f4608f 100644 --- a/JSONAPI/ActionFilters/EnableSortingAttribute.cs +++ b/JSONAPI/ActionFilters/EnableSortingAttribute.cs @@ -90,7 +90,7 @@ private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sort if (property == null) throw new SortingException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", - propertyName, _modelManager.GetJsonKeyForType(typeof (T)))); + propertyName, _modelManager.GetResourceTypeNameForType(typeof (T)))); if (usedProperties.ContainsKey(property)) throw new SortingException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); diff --git a/JSONAPI/Core/IModelManager.cs b/JSONAPI/Core/IModelManager.cs index 8f5a0305..a7ffe940 100644 --- a/JSONAPI/Core/IModelManager.cs +++ b/JSONAPI/Core/IModelManager.cs @@ -25,13 +25,20 @@ public interface IModelManager PropertyInfo GetIdProperty(Type type); /// - /// Returns the key that will be used to represent a collection of objects of a - /// given type, for example in the top-level of a JSON API document or within - /// the "linked" objects section of a payload. + /// Returns the name that will be used to represent this type in json-api documents. + /// The `type` property of resource objects of this type will have this value. /// /// The serializable Type /// The string denoting the given type in JSON documents. - string GetJsonKeyForType(Type type); + string GetResourceTypeNameForType(Type type); + + /// + /// Gets the registered Type corresponding to a json-api resource type name. Inverse + /// of + /// + /// + /// The type that has been registered for this resource type name. + Type GetTypeByResourceTypeName(string resourceTypeName); /// /// Returns the key that will be used to represent the given property in serialized diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index 2990545c..3cea2f7d 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -38,11 +38,16 @@ protected Lazy>> _propertyMaps () => new Dictionary>() ); - protected Lazy> _jsonKeysForType + protected Lazy> _resourceTypeNamesByType = new Lazy>( () => new Dictionary() ); + protected Lazy> _typesByResourceTypeName + = new Lazy>( + () => new Dictionary() + ); + protected Lazy> _isSerializedAsMany = new Lazy>( () => new Dictionary() @@ -126,35 +131,78 @@ public PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey) #endregion - public string GetJsonKeyForType(Type type) + public string GetResourceTypeNameForType(Type type) { - string key = null; + if (IsSerializedAsMany(type)) + type = GetElementType(type); - var keyCache = _jsonKeysForType.Value; + string resourceTypeName; + if (_resourceTypeNamesByType.Value.TryGetValue(type, out resourceTypeName)) return resourceTypeName; - lock (keyCache) - { - if (IsSerializedAsMany(type)) - type = GetElementType(type); + throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName)); + } - if (keyCache.TryGetValue(type, out key)) return key; + public Type GetTypeByResourceTypeName(string resourceTypeName) + { + Type type; + if (_typesByResourceTypeName.Value.TryGetValue(resourceTypeName, out type)) return type; - var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList(); + throw new InvalidOperationException(String.Format("The resource type name `{0}` was not registered.", resourceTypeName)); + } + + /// + /// Registers a type with this ModelManager. + /// + /// The type to register. + public void RegisterResourceType(Type type) + { + var resourceTypeName = CalculateResourceTypeNameForType(type); + RegisterResourceType(type, resourceTypeName); + } - string title = type.Name; - if (attrs.Any()) + /// + /// Registeres a type with this ModelManager, using a default resource type name. + /// + /// The type to register. + /// The resource type name to use + public void RegisterResourceType(Type type, string resourceTypeName) + { + lock (_resourceTypeNamesByType.Value) + { + lock (_typesByResourceTypeName.Value) { - var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") - .Select(arg => arg.TypedValue.Value.ToString()).ToList(); - if (titles.Any()) title = titles.First(); + if (_resourceTypeNamesByType.Value.ContainsKey(type)) + throw new InvalidOperationException(String.Format("The type `{0}` has already been registered.", + type.FullName)); + + if (_typesByResourceTypeName.Value.ContainsKey(resourceTypeName)) + throw new InvalidOperationException( + String.Format("The resource type name `{0}` has already been registered.", resourceTypeName)); + + _resourceTypeNamesByType.Value[type] = resourceTypeName; + _typesByResourceTypeName.Value[resourceTypeName] = type; } + } + } - key = FormatPropertyName(PluralizationService.Pluralize(title)).Dasherize(); + /// + /// Determines the resource type name for a given type. + /// + /// The type to calculate the resouce type name for + /// The type's resource type name + protected virtual string CalculateResourceTypeNameForType(Type type) + { + var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(Newtonsoft.Json.JsonObjectAttribute)).ToList(); - keyCache.Add(type, key); + string title = type.Name; + if (attrs.Any()) + { + var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") + .Select(arg => arg.TypedValue.Value.ToString()).ToList(); + if (titles.Any()) title = titles.First(); } - return key; + return FormatPropertyName(PluralizationService.Pluralize(title)).Dasherize(); } public string GetJsonKeyForProperty(PropertyInfo propInfo) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 06b05261..2ffe5126 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -123,7 +123,7 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w //writer.Formatting = Formatting.Indented; - var root = _modelManager.GetJsonKeyForType(type); + var root = _modelManager.GetResourceTypeNameForType(type); writer.WriteStartObject(); writer.WritePropertyName(root); @@ -469,7 +469,7 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J foreach (KeyValuePair> apair in writers) { apair.Value.Key.WriteEnd(); // close off the array - writer.WritePropertyName(_modelManager.GetJsonKeyForType(apair.Key)); + writer.WritePropertyName(_modelManager.GetResourceTypeNameForType(apair.Key)); writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter } @@ -501,7 +501,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, { object retval = null; Type singleType = GetSingleType(type); - var pripropname = _modelManager.GetJsonKeyForType(type); + var pripropname = _modelManager.GetResourceTypeNameForType(type); var contentHeaders = content == null ? null : content.Headers; // If content length is 0 then return default value for this type From d4e868f9e8e12b9c55bb5e0e03c60eea71c5e87d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 14:06:49 -0500 Subject: [PATCH 027/186] search base types for unregistered types When trying to determine the resource type name for a type "A" that is not registered but is derived from another type "B" that is, then the model manager should return the resource type name for type "B". This is particularly useful when trying to serialize Entity Framework proxies. --- JSONAPI.Tests/Core/ModelManagerTests.cs | 20 ++++++++++++++++++++ JSONAPI/Core/ModelManager.cs | 11 +++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 832f5d46..098b1e19 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -25,6 +25,11 @@ private class CustomIdModel public string Data { get; set; } } + private class DerivedPost : Post + { + + } + [TestMethod] public void FindsIdNamedId() { @@ -90,6 +95,21 @@ public void GetResourceTypeName_returns_correct_value_for_registered_types() Assert.AreEqual("user-groups", userGroupsKey); } + [TestMethod] + public void GetResourceTypeNameForType_gets_name_for_closest_registered_base_type_for_unregistered_type() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + + // Act + var resourceTypeName = mm.GetResourceTypeNameForType(typeof(DerivedPost)); + + // Assert + resourceTypeName.Should().Be("posts"); + } + [TestMethod] public void GetResourceTypeNameForType_fails_when_getting_unregistered_type() { diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index 3cea2f7d..60beebd7 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -136,8 +136,15 @@ public string GetResourceTypeNameForType(Type type) if (IsSerializedAsMany(type)) type = GetElementType(type); - string resourceTypeName; - if (_resourceTypeNamesByType.Value.TryGetValue(type, out resourceTypeName)) return resourceTypeName; + var currentType = type; + while (currentType != null && currentType != typeof(Object)) + { + string resourceTypeName; + if (_resourceTypeNamesByType.Value.TryGetValue(currentType, out resourceTypeName)) return resourceTypeName; + + // This particular type wasn't registered, but maybe the base type was + currentType = currentType.BaseType; + } throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName)); } From 008b961f50583f43cff28d0c037d5f10b1787f79 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 14:19:44 -0500 Subject: [PATCH 028/186] include `type` key in resource objects --- .../Fixtures/Posts/Responses/GetAllResponse.json | 4 ++++ .../Fixtures/Posts/Responses/GetByIdResponse.json | 1 + .../Posts/Responses/GetWithFilterResponse.json | 1 + .../Fixtures/Posts/Responses/PostResponse.json | 1 + .../Responses/PutWithAttributeUpdateResponse.json | 1 + .../Responses/PutWithNullToOneUpdateResponse.json | 1 + .../PutWithToManyEmptyDataUpdateResponse.json | 1 + ...PutWithToManyHomogeneousDataUpdateResponse.json | 1 + .../Responses/PutWithToManyUpdateResponse.json | 1 + .../Responses/PutWithToOneUpdateResponse.json | 1 + .../Responses/GetSortedAscendingResponse.json | 10 ++++++++++ .../GetSortedByMixedDirectionResponse.json | 10 ++++++++++ .../GetSortedByMultipleAscendingResponse.json | 10 ++++++++++ .../GetSortedByMultipleDescendingResponse.json | 10 ++++++++++ .../Responses/GetSortedDescendingResponse.json | 10 ++++++++++ .../UserGroups/Responses/GetAllResponse.json | 1 + .../EntityConverterTests.cs | 1 + JSONAPI.Tests/Data/AttributeSerializationTest.json | 4 +++- JSONAPI.Tests/Data/ByteIdSerializationTest.json | 3 +++ JSONAPI.Tests/Data/LinkTemplateTest.json | 1 + JSONAPI.Tests/Data/MalformedRawJsonString.json | 5 +++-- JSONAPI.Tests/Data/NonStandardIdTest.json | 1 + .../ReformatsRawJsonStringWithUnquotedKeys.json | 5 +++-- JSONAPI.Tests/Data/SerializerIntegrationTest.json | 9 +++++++++ JSONAPI.TodoMVC.API/Startup.cs | 1 + JSONAPI/Json/JsonApiFormatter.cs | 14 +++++++------- 26 files changed, 96 insertions(+), 12 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json index c4f471e0..92da787e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "201", "title": "Post 1", "content": "Post 1 content", @@ -12,6 +13,7 @@ } }, { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", @@ -23,6 +25,7 @@ } }, { + "type": "posts", "id": "203", "title": "Post 3", "content": "Post 3 content", @@ -34,6 +37,7 @@ } }, { + "type": "posts", "id": "204", "title": "Post 4", "content": "Post 4 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json index 0141da6b..582f9d05 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json index a83c61f6..a5acc62f 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "204", "title": "Post 4", "content": "Post 4 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json index 7322f663..37ff5bc0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "205", "title": "Added post", "content": "Added post content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json index 03e7d034..566a212c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "New post title", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json index a42619b9..6214bc02 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json index 6ed335a1..692aa418 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json index 260fcda1..2a0c6d54 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json index 04c39b9a..5f6608bf 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json index 4baeb786..2108f210 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "202", "title": "Post 2", "content": "Post 2 content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json index 0d5100ab..9d435e07 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json @@ -1,6 +1,7 @@ { "users": [ { + "type": "users", "id": "401", "firstName": "Alice", "lastName": "Smith", @@ -11,6 +12,7 @@ } }, { + "type": "users", "id": "402", "firstName": "Bob", "lastName": "Jones", @@ -21,6 +23,7 @@ } }, { + "type": "users", "id": "403", "firstName": "Charlie", "lastName": "Michaels", @@ -31,6 +34,7 @@ } }, { + "type": "users", "id": "409", "firstName": "Charlie", "lastName": "Burns", @@ -41,6 +45,7 @@ } }, { + "type": "users", "id": "406", "firstName": "Ed", "lastName": "Burns", @@ -51,6 +56,7 @@ } }, { + "type": "users", "id": "405", "firstName": "Michelle", "lastName": "Johnson", @@ -61,6 +67,7 @@ } }, { + "type": "users", "id": "408", "firstName": "Pat", "lastName": "Morgan", @@ -71,6 +78,7 @@ } }, { + "type": "users", "id": "404", "firstName": "Richard", "lastName": "Smith", @@ -81,6 +89,7 @@ } }, { + "type": "users", "id": "410", "firstName": "Sally", "lastName": "Burns", @@ -91,6 +100,7 @@ } }, { + "type": "users", "id": "407", "firstName": "Thomas", "lastName": "Potter", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json index aa304398..ed921b2a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json @@ -1,6 +1,7 @@ { "users": [ { + "type": "users", "id": "410", "firstName": "Sally", "lastName": "Burns", @@ -11,6 +12,7 @@ } }, { + "type": "users", "id": "406", "firstName": "Ed", "lastName": "Burns", @@ -21,6 +23,7 @@ } }, { + "type": "users", "id": "409", "firstName": "Charlie", "lastName": "Burns", @@ -31,6 +34,7 @@ } }, { + "type": "users", "id": "405", "firstName": "Michelle", "lastName": "Johnson", @@ -41,6 +45,7 @@ } }, { + "type": "users", "id": "402", "firstName": "Bob", "lastName": "Jones", @@ -51,6 +56,7 @@ } }, { + "type": "users", "id": "403", "firstName": "Charlie", "lastName": "Michaels", @@ -61,6 +67,7 @@ } }, { + "type": "users", "id": "408", "firstName": "Pat", "lastName": "Morgan", @@ -71,6 +78,7 @@ } }, { + "type": "users", "id": "407", "firstName": "Thomas", "lastName": "Potter", @@ -81,6 +89,7 @@ } }, { + "type": "users", "id": "404", "firstName": "Richard", "lastName": "Smith", @@ -91,6 +100,7 @@ } }, { + "type": "users", "id": "401", "firstName": "Alice", "lastName": "Smith", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json index 5d9d2b8a..d761b494 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json @@ -1,6 +1,7 @@ { "users": [ { + "type": "users", "id": "409", "firstName": "Charlie", "lastName": "Burns", @@ -11,6 +12,7 @@ } }, { + "type": "users", "id": "406", "firstName": "Ed", "lastName": "Burns", @@ -21,6 +23,7 @@ } }, { + "type": "users", "id": "410", "firstName": "Sally", "lastName": "Burns", @@ -31,6 +34,7 @@ } }, { + "type": "users", "id": "405", "firstName": "Michelle", "lastName": "Johnson", @@ -41,6 +45,7 @@ } }, { + "type": "users", "id": "402", "firstName": "Bob", "lastName": "Jones", @@ -51,6 +56,7 @@ } }, { + "type": "users", "id": "403", "firstName": "Charlie", "lastName": "Michaels", @@ -61,6 +67,7 @@ } }, { + "type": "users", "id": "408", "firstName": "Pat", "lastName": "Morgan", @@ -71,6 +78,7 @@ } }, { + "type": "users", "id": "407", "firstName": "Thomas", "lastName": "Potter", @@ -81,6 +89,7 @@ } }, { + "type": "users", "id": "401", "firstName": "Alice", "lastName": "Smith", @@ -91,6 +100,7 @@ } }, { + "type": "users", "id": "404", "firstName": "Richard", "lastName": "Smith", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json index 98be56b2..9ab92960 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json @@ -1,6 +1,7 @@ { "users": [ { + "type": "users", "id": "404", "firstName": "Richard", "lastName": "Smith", @@ -11,6 +12,7 @@ } }, { + "type": "users", "id": "401", "firstName": "Alice", "lastName": "Smith", @@ -21,6 +23,7 @@ } }, { + "type": "users", "id": "407", "firstName": "Thomas", "lastName": "Potter", @@ -31,6 +34,7 @@ } }, { + "type": "users", "id": "408", "firstName": "Pat", "lastName": "Morgan", @@ -41,6 +45,7 @@ } }, { + "type": "users", "id": "403", "firstName": "Charlie", "lastName": "Michaels", @@ -51,6 +56,7 @@ } }, { + "type": "users", "id": "402", "firstName": "Bob", "lastName": "Jones", @@ -61,6 +67,7 @@ } }, { + "type": "users", "id": "405", "firstName": "Michelle", "lastName": "Johnson", @@ -71,6 +78,7 @@ } }, { + "type": "users", "id": "410", "firstName": "Sally", "lastName": "Burns", @@ -81,6 +89,7 @@ } }, { + "type": "users", "id": "406", "firstName": "Ed", "lastName": "Burns", @@ -91,6 +100,7 @@ } }, { + "type": "users", "id": "409", "firstName": "Charlie", "lastName": "Burns", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json index c401ec97..17f4400c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json @@ -1,6 +1,7 @@ { "users": [ { + "type": "users", "id": "407", "firstName": "Thomas", "lastName": "Potter", @@ -11,6 +12,7 @@ } }, { + "type": "users", "id": "410", "firstName": "Sally", "lastName": "Burns", @@ -21,6 +23,7 @@ } }, { + "type": "users", "id": "404", "firstName": "Richard", "lastName": "Smith", @@ -31,6 +34,7 @@ } }, { + "type": "users", "id": "408", "firstName": "Pat", "lastName": "Morgan", @@ -41,6 +45,7 @@ } }, { + "type": "users", "id": "405", "firstName": "Michelle", "lastName": "Johnson", @@ -51,6 +56,7 @@ } }, { + "type": "users", "id": "406", "firstName": "Ed", "lastName": "Burns", @@ -61,6 +67,7 @@ } }, { + "type": "users", "id": "403", "firstName": "Charlie", "lastName": "Michaels", @@ -71,6 +78,7 @@ } }, { + "type": "users", "id": "409", "firstName": "Charlie", "lastName": "Burns", @@ -81,6 +89,7 @@ } }, { + "type": "users", "id": "402", "firstName": "Bob", "lastName": "Jones", @@ -91,6 +100,7 @@ } }, { + "type": "users", "id": "401", "firstName": "Alice", "lastName": "Smith", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json index 122b7db6..c72b123b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json @@ -1,6 +1,7 @@ { "user-groups": [ { + "type": "user-groups", "id": "501", "name": "Admin users", "links": { diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index 3ce95e56..359bb1b3 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -108,6 +108,7 @@ public void SerializeTest() { // Arrange var modelManager = new ModelManager(new Core.PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); modelManager.RegisterResourceType(typeof(Post)); JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(modelManager); diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json index 1a4fa932..786b00c1 100644 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ b/JSONAPI.Tests/Data/AttributeSerializationTest.json @@ -1,9 +1,10 @@ { "samples": [ { + "type": "samples", "id": "1", "booleanField": false, - "nullableBooleanField": false, + "nullableBooleanField": false, "sByteField": 0, "nullableSByteField": null, "byteField": 0, @@ -36,6 +37,7 @@ "enumField": 0, "nullableEnumField": null }, { + "type": "samples", "id": "2", "booleanField": true, "nullableBooleanField": true, diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json index f4063a5b..3031f6ff 100644 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ b/JSONAPI.Tests/Data/ByteIdSerializationTest.json @@ -1,12 +1,15 @@ { "tags": [ { + "type": "tags", "id": "1", "text": "Ember" }, { + "type": "tags", "id": "2", "text": "React" }, { + "type": "tags", "id": "3", "text": "Angular" } diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json index 6d351b09..a4284384 100644 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ b/JSONAPI.Tests/Data/LinkTemplateTest.json @@ -1,5 +1,6 @@ { "posts": { + "type": "posts", "id": "2", "title": "How to fry an egg", "links": { diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json index 54c335f8..0173d448 100644 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ b/JSONAPI.Tests/Data/MalformedRawJsonString.json @@ -1,11 +1,12 @@ { "comments": [ { + "type": "comments", "id": "5", "body": null, "customData": { }, - "links": { - "post": null + "links": { + "post": null } } ] diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json index c84f2fcc..a1ed9336 100644 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ b/JSONAPI.Tests/Data/NonStandardIdTest.json @@ -1,6 +1,7 @@ { "non-standard-id-things": [ { + "type": "non-standard-id-things", "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", "uuid": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", "data": "Swap" diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json index 49894bcf..fcc4eeae 100644 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json @@ -1,10 +1,11 @@ { "comments": [ { + "type": "comments", "id": "5", "body": null, - "customData": { - "unquotedKey": 5 + "customData": { + "unquotedKey": 5 }, "links": { "post": null diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json index 3daee7ff..294c0b85 100644 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ b/JSONAPI.Tests/Data/SerializerIntegrationTest.json @@ -1,6 +1,7 @@ { "posts": [ { + "type": "posts", "id": "1", "title": "Linkbait!", "links": { @@ -9,6 +10,7 @@ } }, { + "type": "posts", "id": "2", "title": "Rant #1023", "links": { @@ -17,6 +19,7 @@ } }, { + "type": "posts", "id": "3", "title": "Polemic in E-flat minor #824", "links": { @@ -25,6 +28,7 @@ } }, { + "type": "posts", "id": "4", "title": "This post has no author.", "links": { @@ -36,12 +40,14 @@ "linked": { "comments": [ { + "type": "comments", "id": "2", "body": "Nuh uh!", "customData": null, "links": { "post": "1" } }, { + "type": "comments", "id": "3", "body": "Yeah huh!", "customData": null, @@ -50,6 +56,7 @@ } }, { + "type": "comments", "id": "4", "body": "Third Reich.", "customData": { @@ -60,6 +67,7 @@ } }, { + "type": "comments", "id": "5", "body": "I laughed, I cried!", "customData": null, @@ -70,6 +78,7 @@ ], "authors": [ { + "type": "authors", "id": "1", "name": "Jason Hater", "links": { diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index 74562992..72d785b5 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -7,6 +7,7 @@ using JSONAPI.Json; using JSONAPI.TodoMVC.API.Models; using Owin; +using PluralizationService = JSONAPI.Core.PluralizationService; namespace JSONAPI.TodoMVC.API { diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 2ffe5126..2c70a458 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -167,16 +167,16 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js { writer.WriteStartObject(); - // The spec no longer requires that the ID key be "id": - // "An ID SHOULD be represented by an 'id' key..." :-/ - // But Ember Data does. So, we'll add "id" to the document - // always, and also serialize the property under its given - // name, for now at least. - //TODO: Partly because of this, we should probably disallow updates to Id properties where practical. + var resourceType = value.GetType(); + + // Write the type + writer.WritePropertyName("type"); + var jsonTypeKey = _modelManager.GetResourceTypeNameForType(resourceType); + writer.WriteValue(jsonTypeKey); // Do the Id now... writer.WritePropertyName("id"); - var idProp = _modelManager.GetIdProperty(value.GetType()); + var idProp = _modelManager.GetIdProperty(resourceType); writer.WriteValue(GetValueForIdProperty(idProp, value)); // Leverage the cached map to avoid another costly call to System.Type.GetProperties() From ff593daddca9f97ee11ee3eeaedd52ea2e4d16c9 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 15:24:53 -0500 Subject: [PATCH 029/186] use `data` key for primary data instead of keying by resource type name. --- .../Fixtures/Posts/Requests/PostRequest.json | 3 +- .../PutWithArrayRelationshipValueRequest.json | 3 +- .../PutWithAttributeUpdateRequest.json | 3 +- .../PutWithMissingToManyIdsRequest.json | 3 +- .../PutWithMissingToManyTypeRequest.json | 3 +- .../PutWithMissingToOneIdRequest.json | 3 +- .../PutWithMissingToOneTypeRequest.json | 3 +- .../PutWithNullToOneUpdateRequest.json | 3 +- ...PutWithStringRelationshipValueRequest.json | 3 +- .../PutWithToManyEmptyDataUpdateRequest.json | 3 +- ...ithToManyHomogeneousDataUpdateRequest.json | 3 +- .../Requests/PutWithToManyUpdateRequest.json | 3 +- .../Requests/PutWithToOneUpdateRequest.json | 3 +- .../Posts/Responses/GetAllResponse.json | 2 +- .../Posts/Responses/GetByIdResponse.json | 2 +- .../Responses/GetWithFilterResponse.json | 2 +- .../Posts/Responses/PostResponse.json | 2 +- .../PutWithAttributeUpdateResponse.json | 2 +- .../PutWithNullToOneUpdateResponse.json | 2 +- .../PutWithToManyEmptyDataUpdateResponse.json | 2 +- ...thToManyHomogeneousDataUpdateResponse.json | 2 +- .../PutWithToManyUpdateResponse.json | 2 +- .../Responses/PutWithToOneUpdateResponse.json | 2 +- .../Responses/GetSortedAscendingResponse.json | 2 +- .../GetSortedByMixedDirectionResponse.json | 2 +- .../GetSortedByMultipleAscendingResponse.json | 2 +- ...GetSortedByMultipleDescendingResponse.json | 2 +- .../GetSortedDescendingResponse.json | 2 +- .../UserGroups/Responses/GetAllResponse.json | 2 +- .../EntityConverterTests.cs | 2 +- .../Data/AttributeSerializationTest.json | 2 +- .../Data/ByteIdSerializationTest.json | 2 +- .../Data/DeserializeAttributeRequest.json | 2 +- .../Data/DeserializeCollectionRequest.json | 2 +- .../Data/DeserializeRawJsonTest.json | 2 +- JSONAPI.Tests/Data/LinkTemplateTest.json | 2 +- .../Data/MalformedRawJsonString.json | 2 +- ...adataManagerPropertyWasPresentRequest.json | 3 +- JSONAPI.Tests/Data/NonStandardIdTest.json | 2 +- ...eformatsRawJsonStringWithUnquotedKeys.json | 2 +- .../Data/SerializerIntegrationTest.json | 2 +- .../Json/JsonApiMediaFormatterTests.cs | 4 +- JSONAPI/Json/JsonApiFormatter.cs | 80 +++++++++++-------- 43 files changed, 103 insertions(+), 77 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json index ce9eeff8..9cfdb44e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "205", "title": "Added post", "content": "Added post content", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json index c049e472..04b74242 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": ["301"] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json index 41f00896..d6c319c2 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "title": "New post title" } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json index 8ddf5314..51f807c3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json index b14c3281..8649da07 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json index 6683cb1c..d08c7f90 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "author": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json index 52d534f5..69a9860b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "author": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json index 97215718..ffeb70e2 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "author": null diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json index 3f2ab79f..06a48cf9 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "author": "301" diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json index 6ceb7544..f87eb1c1 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json index b72a0dfd..d4a56c0a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json index 37ebbbe5..9dea2bc2 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "tags": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json index 5bd71f57..675c67a3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json @@ -1,6 +1,7 @@ { - "posts": [ + "data": [ { + "type": "posts", "id": "202", "links": { "author": { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json index 92da787e..5592bfbc 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "201", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json index 582f9d05..faa72760 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json index a5acc62f..95d81f18 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "204", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json index 37ff5bc0..9da869ef 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "205", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json index 566a212c..0ee29b97 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json index 6214bc02..079784a1 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json index 692aa418..62ca3775 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json index 2a0c6d54..36f44989 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json index 5f6608bf..f92f7c33 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json index 2108f210..b843a36d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "202", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json index 9d435e07..905ad31e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json @@ -1,5 +1,5 @@ { - "users": [ + "data": [ { "type": "users", "id": "401", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json index ed921b2a..e2f8072e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json @@ -1,5 +1,5 @@ { - "users": [ + "data": [ { "type": "users", "id": "410", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json index d761b494..387256fa 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json @@ -1,5 +1,5 @@ { - "users": [ + "data": [ { "type": "users", "id": "409", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json index 9ab92960..e0f988b3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json @@ -1,5 +1,5 @@ { - "users": [ + "data": [ { "type": "users", "id": "404", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json index 17f4400c..6a80b703 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json @@ -1,5 +1,5 @@ { - "users": [ + "data": [ { "type": "users", "id": "407", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json index c72b123b..e707faa8 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json @@ -1,5 +1,5 @@ { - "user-groups": [ + "data": [ { "type": "user-groups", "id": "501", diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs index 359bb1b3..7e5299c2 100644 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs @@ -135,7 +135,7 @@ public async Task UnderpostingTest() EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context, MetadataManager.Instance); - string underpost = @"{""posts"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}"; + string underpost = @"{""data"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}"; stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(underpost)); int previousCommentsCount = p.Comments.Count; diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json index 786b00c1..e00c19a0 100644 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ b/JSONAPI.Tests/Data/AttributeSerializationTest.json @@ -1,5 +1,5 @@ { - "samples": [ + "data": [ { "type": "samples", "id": "1", diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json index 3031f6ff..ed041273 100644 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ b/JSONAPI.Tests/Data/ByteIdSerializationTest.json @@ -1,5 +1,5 @@ { - "tags": [ + "data": [ { "type": "tags", "id": "1", diff --git a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json index 1a4fa932..25942259 100644 --- a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json +++ b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json @@ -1,5 +1,5 @@ { - "samples": [ + "data": [ { "id": "1", "booleanField": false, diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json index c29c7ea6..1d3bb1bb 100644 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "id": "1", "title": "Linkbait!", diff --git a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json b/JSONAPI.Tests/Data/DeserializeRawJsonTest.json index 684bb5ff..d24d3fd2 100644 --- a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json +++ b/JSONAPI.Tests/Data/DeserializeRawJsonTest.json @@ -1,5 +1,5 @@ { - "comments": [ + "data": [ { "id": "2", "customData": null diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json index a4284384..d5607aca 100644 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ b/JSONAPI.Tests/Data/LinkTemplateTest.json @@ -1,5 +1,5 @@ { - "posts": { + "data": { "type": "posts", "id": "2", "title": "How to fry an egg", diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json index 0173d448..fba94c29 100644 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ b/JSONAPI.Tests/Data/MalformedRawJsonString.json @@ -1,5 +1,5 @@ { - "comments": [ + "data": [ { "type": "comments", "id": "5", diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json index 32d13c98..f4294e50 100644 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -1,5 +1,6 @@ { - "posts": { + "data": { + "type": "posts", "id": "42", "links": { "author": { diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json index a1ed9336..62a81283 100644 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ b/JSONAPI.Tests/Data/NonStandardIdTest.json @@ -1,5 +1,5 @@ { - "non-standard-id-things": [ + "data": [ { "type": "non-standard-id-things", "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json index fcc4eeae..6fb8b6dc 100644 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json @@ -1,5 +1,5 @@ { - "comments": [ + "data": [ { "type": "comments", "id": "5", diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json index 294c0b85..9d68949b 100644 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ b/JSONAPI.Tests/Data/SerializerIntegrationTest.json @@ -1,5 +1,5 @@ { - "posts": [ + "data": [ { "type": "posts", "id": "1", diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index 0060688a..c8c84c47 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -460,7 +460,7 @@ public void DeserializeExtraPropertyTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""type"": ""posts"",""ids"": []}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""type"": ""posts"",""ids"": []}}}")); // Act Author a; @@ -480,7 +480,7 @@ public void DeserializeExtraRelationshipTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""authors"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""type"": ""posts"",""ids"": []},""bogus"":[""PANIC!""]}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""type"": ""posts"",""ids"": []},""bogus"":[""PANIC!""]}}}")); // Act Author a; diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 2c70a458..8130bea8 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -88,6 +88,8 @@ public override bool CanWriteType(Type type) return true; } + private const string PrimaryDataKeyName = "data"; + #region Serialization public override Task WriteToStreamAsync(System.Type type, object value, Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext) @@ -123,10 +125,8 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w //writer.Formatting = Formatting.Indented; - var root = _modelManager.GetResourceTypeNameForType(type); - writer.WriteStartObject(); - writer.WritePropertyName(root); + writer.WritePropertyName(PrimaryDataKeyName); if (_modelManager.IsSerializedAsMany(value.GetType())) this.SerializeMany(value, writeStream, writer, serializer, aggregator); else @@ -501,7 +501,6 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, { object retval = null; Type singleType = GetSingleType(type); - var pripropname = _modelManager.GetResourceTypeNameForType(type); var contentHeaders = content == null ? null : content.Headers; // If content length is 0 then return default value for this type @@ -520,6 +519,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("Document root is not an object!"); + bool foundPrimaryData = false; while (reader.Read()) { if (reader.TokenType == JsonToken.PropertyName) @@ -536,36 +536,10 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, // ignore this, is it even meaningful in a PUT/POST body? reader.Skip(); break; - default: - if (value == pripropname) - { - // Could be a single resource or multiple, according to spec! - if (reader.TokenType == JsonToken.StartArray) - { - Type listType = (typeof (List<>)).MakeGenericType(singleType); - retval = (IList) Activator.CreateInstance(listType); - reader.Read(); // Burn off StartArray token - while (reader.TokenType == JsonToken.StartObject) - { - ((IList) retval).Add(Deserialize(singleType, reader)); - } - // burn EndArray token... - if (reader.TokenType != JsonToken.EndArray) - throw new JsonReaderException( - String.Format("Expected JsonToken.EndArray but got {0}", - reader.TokenType)); - reader.Read(); - } - else - { - // Because we choose what to deserialize based on the ApiController method signature - // (not choose the method signature based on what we're deserializing), the `type` - // parameter will always be `IList` even if a single model is sent! - retval = Deserialize(singleType, reader); - } - } - else - reader.Skip(); + case PrimaryDataKeyName: + // Could be a single resource or multiple, according to spec! + foundPrimaryData = true; + retval = DeserializePrimaryData(singleType, reader); break; } } @@ -573,6 +547,10 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, reader.Skip(); } + if (!foundPrimaryData) + throw new BadRequestException(String.Format("Expected primary data located at the `{0}` key", PrimaryDataKeyName)); + + /* WARNING: May transform a single object into a list of objects!!! * This is a necessary workaround to support POST and PUT of multiple * resoruces as per the spec, because WebAPI does NOT allow you to overload @@ -657,6 +635,40 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, return GetDefaultValueForType(type); } + private object DeserializePrimaryData(Type singleType, JsonReader reader) + { + object retval; + if (reader.TokenType == JsonToken.StartArray) + { + Type listType = (typeof(List<>)).MakeGenericType(singleType); + retval = (IList)Activator.CreateInstance(listType); + reader.Read(); // Burn off StartArray token + while (reader.TokenType == JsonToken.StartObject) + { + ((IList)retval).Add(Deserialize(singleType, reader)); + } + // burn EndArray token... + if (reader.TokenType != JsonToken.EndArray) + throw new JsonReaderException( + String.Format("Expected JsonToken.EndArray but got {0}", + reader.TokenType)); + reader.Read(); + } + else if (reader.TokenType == JsonToken.StartObject) + { + // Because we choose what to deserialize based on the ApiController method signature + // (not choose the method signature based on what we're deserializing), the `type` + // parameter will always be `IList` even if a single model is sent! + retval = Deserialize(singleType, reader); + } + else + { + throw new BadRequestException(String.Format("Unexpected value for the `{0}` key", PrimaryDataKeyName)); + } + + return retval; + } + private object Deserialize(Type objectType, JsonReader reader) { object retval = Activator.CreateInstance(objectType); From 9f76960f74e4707b2cd873a7413acfba51554108 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 25 Feb 2015 16:18:24 -0500 Subject: [PATCH 030/186] add test for getting heterogeneous collections --- .../Controllers/SearchController.cs | 26 ++++++++++++++++++ ...PI.EntityFramework.Tests.TestWebApp.csproj | 1 + .../Responses/GetSearchResultsResponse.json | 26 ++++++++++++++++++ .../Acceptance/HeterogeneousTests.cs | 27 +++++++++++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 2 ++ 5 files changed, 82 insertions(+) create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs new file mode 100644 index 00000000..c58afd28 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class SearchController : ApiController + { + private readonly TestDbContext _dbContext; + + public SearchController(TestDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Get(string s) + { + IEnumerable posts = await _dbContext.Posts.Where(p => p.Title.Contains(s)).ToArrayAsync(); + IEnumerable comments = await _dbContext.Comments.Where(p => p.Text.Contains(s)).ToArrayAsync(); + return posts.Concat(comments); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index e7b67ffb..49f3828a 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -115,6 +115,7 @@ + diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json new file mode 100644 index 00000000..df2dba0c --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json @@ -0,0 +1,26 @@ +{ + "data": [ + { + "type": "posts", + "id": "201", + "title": "Post 1", + "content": "Post 1 content", + "created": "2015-01-31T14:00:00+00:00", + "links": { + "author": "401", + "comments": [ "101", "102", "103" ], + "tags": [ "301", "302" ] + } + }, + { + "type": "comments", + "id": "101", + "text": "Comment 1", + "created": "2015-01-31T14:30:00+00:00", + "links": { + "author": "403", + "post": "201" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs new file mode 100644 index 00000000..400bd900 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class HeterogeneousTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "search?s=1"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Heterogeneous\Responses\GetSearchResultsResponse.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 332bacb0..b2489cad 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -110,6 +110,7 @@ + @@ -162,6 +163,7 @@ + Designer From be374234a3d66f497944adb6f0de10dd389d6d16 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 6 Mar 2015 11:33:21 -0600 Subject: [PATCH 031/186] ensure tests work in resharper --- JSONAPI.EntityFramework.Tests/App.Config | 6 - .../EntityConverterTests.cs | 154 ------------------ .../EntityFrameworkMaterializerTests.cs | 50 +++--- .../JSONAPI.EntityFramework.Tests.csproj | 2 - .../Models/TestEntities.cs | 26 --- JSONAPI.EntityFramework/App.config | 3 - 6 files changed, 26 insertions(+), 215 deletions(-) delete mode 100644 JSONAPI.EntityFramework.Tests/EntityConverterTests.cs delete mode 100644 JSONAPI.EntityFramework.Tests/Models/TestEntities.cs diff --git a/JSONAPI.EntityFramework.Tests/App.Config b/JSONAPI.EntityFramework.Tests/App.Config index 7548116e..2a6fe587 100644 --- a/JSONAPI.EntityFramework.Tests/App.Config +++ b/JSONAPI.EntityFramework.Tests/App.Config @@ -4,9 +4,6 @@
- - - @@ -52,8 +49,5 @@ - - - \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs deleted file mode 100644 index 7e5299c2..00000000 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Text; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using JSONAPI.Core; -using JSONAPI.Json; -using JSONAPI.EntityFramework; -using JSONAPI.EntityFramework.Tests.Models; -using System.IO; -using System.Diagnostics; -using System.Reflection; -using System.Data; -using System.Text.RegularExpressions; - -namespace JSONAPI.EntityFramework.Tests -{ - [TestClass] - public class EntityConverterTests - { - private TestEntities context; - private Author a, a2; - private Post p, p2, p3; - private Comment c, c2, c3, c4; - - [TestInitialize] - public void SetupEntities() - { - //- See http://stackoverflow.com/a/19130718/489116 - var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance; - //- - - context = new TestEntities(); - //JSONAPI.EntityFramework.Json.ContractResolver.ObjectContext = context; - - - // Clear it out! - foreach (Comment o in context.Comments) context.Comments.Remove(o); - foreach (Post o in context.Posts) context.Posts.Remove(o); - foreach (Author o in context.Authors) context.Authors.Remove(o); - context.SaveChanges(); - - - a = new Author - { - Id = Guid.NewGuid().ToString(), - Name = "Jason Hater" - }; - context.Authors.Add(a); - - p = new Post() - { - Title = "Linkbait!", - Author = a - }; - p2 = new Post - { - Title = "Rant #1023", - Author = a - }; - p3 = new Post - { - Title = "Polemic in E-flat minor #824", - Author = a - }; - - a.Posts.Add(p); - a.Posts.Add(p2); - a.Posts.Add(p3); - - p.Comments.Add( - c = new Comment() - { - Body = "Nuh uh!", - Post = p - } - ); - p.Comments.Add( - c2 = new Comment() - { - Body = "Yeah huh!", - Post = p - } - ); - p.Comments.Add( - c3 = new Comment() - { - Body = "Third Reich.", - Post = p - } - ); - - p2.Comments.Add( - c4 = new Comment - { - Body = "I laughed, I cried!", - Post = p2 - } - ); - - context.SaveChanges(); - } - - [TestMethod] - public void SerializeTest() - { - // Arrange - var modelManager = new ModelManager(new Core.PluralizationService()); - modelManager.RegisterResourceType(typeof(Comment)); - modelManager.RegisterResourceType(typeof(Post)); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(modelManager); - MemoryStream stream = new MemoryStream(); - - // Act - formatter.WriteToStreamAsync(typeof(Post), p.Comments.First(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - - } - - [TestMethod] - public async Task UnderpostingTest() - { - // Arrange - var modelManager = new ModelManager(new Core.PluralizationService()); - modelManager.RegisterResourceType(typeof(Post)); - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(modelManager); - MemoryStream stream = new MemoryStream(); - - EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context, MetadataManager.Instance); - - string underpost = @"{""data"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}"; - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(underpost)); - - int previousCommentsCount = p.Comments.Count; - - // Act - Post pUpdated; - pUpdated = (Post)await formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null); - pUpdated = await materializer.MaterializeUpdateAsync(pUpdated); - - // Assert - Assert.AreEqual(previousCommentsCount, pUpdated.Comments.Count, "Comments were wiped out!"); - Assert.AreEqual("Not at all linkbait!", pUpdated.Title, "Title was not updated."); - } - - } -} diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs index bd7122f3..5c81c3ce 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs @@ -1,5 +1,7 @@ using System; +using System.Data.Common; using System.Linq; +using Effort; using Microsoft.VisualStudio.TestTools.UnitTesting; using JSONAPI.EntityFramework.Tests.Models; using FluentAssertions; @@ -17,6 +19,11 @@ private class TestDbContext : DbContext { public DbSet Backlinks { get; set; } public DbSet Posts { get; set; } + + public TestDbContext(DbConnection conn) : base(conn, true) + { + + } } private class NotAnEntity @@ -25,31 +32,29 @@ private class NotAnEntity public string Temporary { get; set; } } - private TestDbContext context; - private Backlink b1, b2; + private DbConnection _conn; + private TestDbContext _context; [TestInitialize] public void SetupEntities() { - //- See http://stackoverflow.com/a/19130718/489116 - var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance; - //- - - context = new TestDbContext(); - //JSONAPI.EntityFramework.Json.ContractResolver.ObjectContext = context; - - - // Clear it out! - foreach (Backlink o in context.Backlinks) context.Backlinks.Remove(o); - context.SaveChanges(); + _conn = DbConnectionFactory.CreateTransient(); + _context = new TestDbContext(_conn); - b1 = new Backlink + var b1 = new Backlink { Url = "http://www.google.com/", Snippet = "1 Results" }; + _context.Backlinks.Add(b1); - context.SaveChanges(); + _context.SaveChanges(); + } + + [TestCleanup] + private void CleanupTest() + { + _context.Dispose(); } [TestMethod] @@ -57,10 +62,10 @@ public void GetKeyNamesStandardIdTest() { // Arrange var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); + var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Post)); + IEnumerable keyNames = materializer.GetKeyNames(typeof(Post)).ToArray(); // Assert keyNames.Count().Should().Be(1); @@ -72,10 +77,10 @@ public void GetKeyNamesNonStandardIdTest() { // Arrange var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); + var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Backlink)); + IEnumerable keyNames = materializer.GetKeyNames(typeof(Backlink)).ToArray(); // Assert keyNames.Count().Should().Be(1); @@ -87,13 +92,10 @@ public void GetKeyNamesNotAnEntityTest() { // Arrange var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(context, mockMetadataManager.Object); + var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); // Act - Action action = () => - { - materializer.GetKeyNames(typeof (NotAnEntity)); - }; + Action action = () => materializer.GetKeyNames(typeof (NotAnEntity)); action.ShouldThrow().Which.Message.Should().Be("The Type NotAnEntity was not found in the DbContext with Type TestDbContext"); } } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index b2489cad..88eb1ce1 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -116,7 +116,6 @@ - @@ -125,7 +124,6 @@ - diff --git a/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs b/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs deleted file mode 100644 index 67b61d3b..00000000 --- a/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace JSONAPI.EntityFramework.Tests.Models -{ - using System; - using System.Data.Entity; - using System.Data.Entity.Infrastructure; - - public partial class TestEntities : DbContext - { - private class TestEntitiesInitializer : DropCreateDatabaseIfModelChanges { } - - public TestEntities() - : base("name=TestEntities") - { - } - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - Database.SetInitializer(new TestEntitiesInitializer()); - } - - public virtual DbSet Authors { get; set; } - public virtual DbSet Posts { get; set; } - public virtual DbSet Comments { get; set; } - public virtual DbSet Backlinks { get; set; } - } -} diff --git a/JSONAPI.EntityFramework/App.config b/JSONAPI.EntityFramework/App.config index 70ad9000..22a39869 100644 --- a/JSONAPI.EntityFramework/App.config +++ b/JSONAPI.EntityFramework/App.config @@ -10,9 +10,6 @@ - - - From e85c38e40e491dfa6e80d6a1106fd7867ac847f8 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 6 Mar 2015 16:06:04 -0600 Subject: [PATCH 032/186] refactor model manager --- .../Responses/GetSearchResultsResponse.json | 4 +- .../EnableFilteringAttributeTests.cs | 7 +- JSONAPI.Tests/Core/MetadataManagerTests.cs | 1 + JSONAPI.Tests/Core/ModelManagerTests.cs | 23 +- JSONAPI.Tests/Data/NonStandardIdTest.json | 1 - .../Json/JsonApiMediaFormatterTests.cs | 26 +- JSONAPI.Tests/Json/LinkTemplateTests.cs | 1 + .../ActionFilters/EnableFilteringAttribute.cs | 626 +++++++++--------- .../ActionFilters/EnableSortingAttribute.cs | 4 +- JSONAPI/Core/IModelManager.cs | 5 +- JSONAPI/Core/ModelManager.cs | 249 ++++--- JSONAPI/Core/ModelProperty.cs | 68 ++ JSONAPI/JSONAPI.csproj | 1 + JSONAPI/Json/JsonApiFormatter.cs | 47 +- 14 files changed, 589 insertions(+), 474 deletions(-) create mode 100644 JSONAPI/Core/ModelProperty.cs diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json index df2dba0c..c4c35cdc 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json @@ -18,8 +18,8 @@ "text": "Comment 1", "created": "2015-01-31T14:30:00+00:00", "links": { - "author": "403", - "post": "201" + "post": "201", + "author": "403" } } ] diff --git a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs index 6a2d6fc1..61223884 100644 --- a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Formatting; +using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using FluentAssertions; @@ -563,6 +564,8 @@ private HttpActionExecutedContext CreateActionExecutedContext(IModelManager mode private T[] GetArray(string uri) { var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Dummy)); + modelManager.RegisterResourceType(typeof(RelatedItemWithId)); var filter = new EnableFilteringAttribute(modelManager); @@ -1130,8 +1133,8 @@ public void Filters_by_missing_nullable_double_property() [TestMethod] public void Does_not_filter_unknown_type() { - var returnedArray = GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); - returnedArray.Length.Should().Be(_fixtures.Count); + Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); + action.ShouldThrow().Which.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } #endregion diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs index fbbe17a3..2a231144 100644 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ b/JSONAPI.Tests/Core/MetadataManagerTests.cs @@ -18,6 +18,7 @@ public void PropertyWasPresentTest() // Arrange var modelManager = new ModelManager(new PluralizationService()); modelManager.RegisterResourceType(typeof(Post)); + modelManager.RegisterResourceType(typeof(Author)); JsonApiFormatter formatter = new JsonApiFormatter(modelManager); var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 098b1e19..155139b5 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -35,6 +35,7 @@ public void FindsIdNamedId() { // Arrange var mm = new ModelManager(new PluralizationService()); + mm.RegisterResourceType(typeof(Author)); // Act PropertyInfo idprop = mm.GetIdProperty(typeof(Author)); @@ -44,17 +45,18 @@ public void FindsIdNamedId() } [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public void DoesntFindMissingId() + public void Cant_register_model_with_missing_id() { // Arrange var mm = new ModelManager(new PluralizationService()); // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(InvalidModel)); + Action action = () => mm.RegisterResourceType(typeof(InvalidModel)); // Assert - Assert.Fail("An InvalidOperationException should be thrown and we shouldn't get here!"); + action.ShouldThrow() + .Which.Message.Should() + .Be("Unable to determine Id property for type JSONAPI.Tests.Core.ModelManagerTests+InvalidModel"); } [TestMethod] @@ -62,9 +64,11 @@ public void FindsIdFromAttribute() { // Arrange var mm = new ModelManager(new PluralizationService()); + mm.RegisterResourceType(typeof(CustomIdModel)); // Act PropertyInfo idprop = mm.GetIdProperty(typeof(CustomIdModel)); + // Assert Assert.AreSame(typeof(CustomIdModel).GetProperty("Uuid"), idprop); } @@ -194,6 +198,7 @@ public void GetPropertyForJsonKeyTest() var pluralizationService = new PluralizationService(); var mm = new ModelManager(pluralizationService); Type authorType = typeof(Author); + mm.RegisterResourceType(authorType); // Act var idProp = mm.GetPropertyForJsonKey(authorType, "id"); @@ -201,10 +206,14 @@ public void GetPropertyForJsonKeyTest() var postsProp = mm.GetPropertyForJsonKey(authorType, "posts"); // Assert - Assert.AreSame(authorType.GetProperty("Id"), idProp); - Assert.AreSame(authorType.GetProperty("Name"), nameProp); - Assert.AreSame(authorType.GetProperty("Posts"), postsProp); + idProp.Property.Should().BeSameAs(authorType.GetProperty("Id")); + idProp.Should().BeOfType(); + + nameProp.Property.Should().BeSameAs(authorType.GetProperty("Name")); + nameProp.Should().BeOfType(); + postsProp.Property.Should().BeSameAs(authorType.GetProperty("Posts")); + postsProp.Should().BeOfType(); } [TestMethod] diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json index 62a81283..ec4b2a94 100644 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ b/JSONAPI.Tests/Data/NonStandardIdTest.json @@ -3,7 +3,6 @@ { "type": "non-standard-id-things", "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", - "uuid": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", "data": "Swap" } ] diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index c8c84c47..abb0720f 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -392,6 +392,8 @@ public void Deserializes_collections_properly() // Arrange var modelManager = new ModelManager(new PluralizationService()); modelManager.RegisterResourceType(typeof(Post)); + modelManager.RegisterResourceType(typeof(Author)); + modelManager.RegisterResourceType(typeof(Comment)); var formatter = new JsonApiFormatter(modelManager); // Act @@ -536,7 +538,7 @@ public void DeserializeNonStandardIdTest() [TestMethod] [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdWithIdOnly() + public void DeserializeNonStandardId() { var modelManager = new ModelManager(new PluralizationService()); modelManager.RegisterResourceType(typeof(NonStandardIdThing)); @@ -554,28 +556,6 @@ public void DeserializeNonStandardIdWithIdOnly() things.Count.Should().Be(1); things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); } - - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdWithoutId() - { - var modelManager = new ModelManager(new PluralizationService()); - modelManager.RegisterResourceType(typeof(NonStandardIdThing)); - var formatter = new JsonApiFormatter(modelManager); - string json = File.ReadAllText("NonStandardIdTest.json"); - json = Regex.Replace(json, @"""id"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,", ""); // remove the uuid attribute - var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); - - // Act - IList things; - things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Assert - json.Should().NotContain("\"id\"", "The \"id\" attribute was supposed to be removed, test methodology problem!"); - things.Count.Should().Be(1); - things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - - } #endregion diff --git a/JSONAPI.Tests/Json/LinkTemplateTests.cs b/JSONAPI.Tests/Json/LinkTemplateTests.cs index dd230037..2daeea62 100644 --- a/JSONAPI.Tests/Json/LinkTemplateTests.cs +++ b/JSONAPI.Tests/Json/LinkTemplateTests.cs @@ -52,6 +52,7 @@ public void GetResourceWithLinkTemplateRelationship() { var modelManager = new ModelManager(new PluralizationService()); modelManager.RegisterResourceType(typeof(Post)); + modelManager.RegisterResourceType(typeof(User)); var formatter = new JsonApiFormatter(modelManager); var stream = new MemoryStream(); diff --git a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs b/JSONAPI/ActionFilters/EnableFilteringAttribute.cs index d468ca12..94427193 100644 --- a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs +++ b/JSONAPI/ActionFilters/EnableFilteringAttribute.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Linq.Expressions; +using System.Net; using System.Net.Http; using System.Reflection; +using System.Web.Http; using System.Web.Http.Filters; using JSONAPI.Core; @@ -72,333 +74,339 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (String.IsNullOrWhiteSpace(queryPair.Key)) continue; - var prop = _modelManager.GetPropertyForJsonKey(type, queryPair.Key); - - if (prop != null) + ModelProperty modelProperty; + try + { + modelProperty = _modelManager.GetPropertyForJsonKey(type, queryPair.Key); + } + catch (InvalidOperationException) { - var propertyType = prop.PropertyType; + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + + var queryValue = queryPair.Value; + if (string.IsNullOrWhiteSpace(queryValue)) + queryValue = null; - var queryValue = queryPair.Value; - if (string.IsNullOrWhiteSpace(queryValue)) - queryValue = null; + Expression expr = null; - Expression expr = null; - if (propertyType == typeof (String)) - { - if (String.IsNullOrWhiteSpace(queryValue)) - { - Expression propertyExpr = Expression.Property(param, prop); - expr = Expression.Equal(propertyExpr, Expression.Constant(null)); - } - else - { - Expression propertyExpr = Expression.Property(param, prop); - expr = Expression.Equal(propertyExpr, Expression.Constant(queryValue)); - } - } - else if (propertyType == typeof(Boolean)) - { - bool value; - expr = bool.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof (Boolean?)) - { - bool tmp; - var value = bool.TryParse(queryValue, out tmp) ? tmp : (bool?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(SByte)) - { - SByte value; - expr = SByte.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof (SByte?)) - { - SByte tmp; - var value = SByte.TryParse(queryValue, out tmp) ? tmp : (SByte?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Byte)) - { - Byte value; - expr = Byte.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Byte?)) - { - Byte tmp; - var value = Byte.TryParse(queryValue, out tmp) ? tmp : (Byte?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int16)) - { - Int16 value; - expr = Int16.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int16?)) - { - Int16 tmp; - var value = Int16.TryParse(queryValue, out tmp) ? tmp : (Int16?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt16)) - { - UInt16 value; - expr = UInt16.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt16?)) - { - UInt16 tmp; - var value = UInt16.TryParse(queryValue, out tmp) ? tmp : (UInt16?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int32)) - { - Int32 value; - expr = Int32.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int32?)) - { - Int32 tmp; - var value = Int32.TryParse(queryValue, out tmp) ? tmp : (Int32?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt32)) - { - UInt32 value; - expr = UInt32.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt32?)) - { - UInt32 tmp; - var value = UInt32.TryParse(queryValue, out tmp) ? tmp : (UInt32?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int64)) - { - Int64 value; - expr = Int64.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int64?)) - { - Int64 tmp; - var value = Int64.TryParse(queryValue, out tmp) ? tmp : (Int64?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt64)) - { - UInt64 value; - expr = UInt64.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt64?)) - { - UInt64 tmp; - var value = UInt64.TryParse(queryValue, out tmp) ? tmp : (UInt64?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Single)) - { - Single value; - expr = Single.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Single?)) - { - Single tmp; - var value = Single.TryParse(queryValue, out tmp) ? tmp : (Single?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Double)) - { - Double value; - expr = Double.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Double?)) - { - Double tmp; - var value = Double.TryParse(queryValue, out tmp) ? tmp : (Double?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Decimal)) - { - Decimal value; - expr = Decimal.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Decimal?)) - { - Decimal tmp; - var value = Decimal.TryParse(queryValue, out tmp) ? tmp : (Decimal?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(DateTime)) - { - DateTime value; - expr = DateTime.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(DateTime?)) - { - DateTime tmp; - var value = DateTime.TryParse(queryValue, out tmp) ? tmp : (DateTime?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(DateTimeOffset)) - { - DateTimeOffset value; - expr = DateTimeOffset.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof (DateTimeOffset?)) - { - DateTimeOffset tmp; - var value = DateTimeOffset.TryParse(queryValue, out tmp) ? tmp : (DateTimeOffset?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType.IsEnum) - { - int value; - expr = (int.TryParse(queryValue, out value) && Enum.IsDefined(propertyType, value)) - ? GetEnumPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (Nullable<>) && - propertyType.GenericTypeArguments[0].IsEnum) - { - int tmp; - var value = int.TryParse(queryValue, out tmp) ? tmp : (int?)null; - expr = GetEnumPropertyExpression(value, prop, param); - } - else - { - // See if it is a relationship property - if (_modelManager.IsSerializedAsMany(propertyType)) - { - var elementType = _modelManager.GetElementType(propertyType); - PropertyInfo relatedIdProperty; - try - { - relatedIdProperty = _modelManager.GetIdProperty(elementType); - } - catch (InvalidOperationException) - { - relatedIdProperty = null; - } + // See if it is a field property + var fieldModelProperty = modelProperty as FieldModelProperty; + if (fieldModelProperty != null) + expr = GetPredicateBodyForField(fieldModelProperty, queryValue, param); - if (relatedIdProperty != null) - { - var propertyExpr = Expression.Property(param, prop); + // See if it is a relationship property + var relationshipModelProperty = modelProperty as RelationshipModelProperty; + if (relationshipModelProperty != null) + expr = GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); - if (string.IsNullOrWhiteSpace(queryValue)) - { - var leftExpr = Expression.Equal(propertyExpr, Expression.Constant(null)); + workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); + } - var asQueryableCallExpr = Expression.Call( - typeof(Queryable), - "AsQueryable", - new[] { elementType }, - propertyExpr); - var anyCallExpr = Expression.Call( - typeof(Queryable), - "Any", - new[] { elementType }, - asQueryableCallExpr); - var rightExpr = Expression.Not(anyCallExpr); + return workingExpr ?? Expression.Constant(true); // No filters, so return everything + } + + private Expression GetPredicateBodyForField(FieldModelProperty modelProperty, string queryValue, ParameterExpression param) + { + var prop = modelProperty.Property; + var propertyType = prop.PropertyType; + + Expression expr; + if (propertyType == typeof(String)) + { + if (String.IsNullOrWhiteSpace(queryValue)) + { + Expression propertyExpr = Expression.Property(param, prop); + expr = Expression.Equal(propertyExpr, Expression.Constant(null)); + } + else + { + Expression propertyExpr = Expression.Property(param, prop); + expr = Expression.Equal(propertyExpr, Expression.Constant(queryValue)); + } + } + else if (propertyType == typeof(Boolean)) + { + bool value; + expr = bool.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Boolean?)) + { + bool tmp; + var value = bool.TryParse(queryValue, out tmp) ? tmp : (bool?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(SByte)) + { + SByte value; + expr = SByte.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(SByte?)) + { + SByte tmp; + var value = SByte.TryParse(queryValue, out tmp) ? tmp : (SByte?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Byte)) + { + Byte value; + expr = Byte.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Byte?)) + { + Byte tmp; + var value = Byte.TryParse(queryValue, out tmp) ? tmp : (Byte?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Int16)) + { + Int16 value; + expr = Int16.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Int16?)) + { + Int16 tmp; + var value = Int16.TryParse(queryValue, out tmp) ? tmp : (Int16?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(UInt16)) + { + UInt16 value; + expr = UInt16.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(UInt16?)) + { + UInt16 tmp; + var value = UInt16.TryParse(queryValue, out tmp) ? tmp : (UInt16?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Int32)) + { + Int32 value; + expr = Int32.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Int32?)) + { + Int32 tmp; + var value = Int32.TryParse(queryValue, out tmp) ? tmp : (Int32?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(UInt32)) + { + UInt32 value; + expr = UInt32.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(UInt32?)) + { + UInt32 tmp; + var value = UInt32.TryParse(queryValue, out tmp) ? tmp : (UInt32?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Int64)) + { + Int64 value; + expr = Int64.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Int64?)) + { + Int64 tmp; + var value = Int64.TryParse(queryValue, out tmp) ? tmp : (Int64?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(UInt64)) + { + UInt64 value; + expr = UInt64.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(UInt64?)) + { + UInt64 tmp; + var value = UInt64.TryParse(queryValue, out tmp) ? tmp : (UInt64?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Single)) + { + Single value; + expr = Single.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Single?)) + { + Single tmp; + var value = Single.TryParse(queryValue, out tmp) ? tmp : (Single?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Double)) + { + Double value; + expr = Double.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Double?)) + { + Double tmp; + var value = Double.TryParse(queryValue, out tmp) ? tmp : (Double?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Decimal)) + { + Decimal value; + expr = Decimal.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Decimal?)) + { + Decimal tmp; + var value = Decimal.TryParse(queryValue, out tmp) ? tmp : (Decimal?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(DateTime)) + { + DateTime value; + expr = DateTime.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(DateTime?)) + { + DateTime tmp; + var value = DateTime.TryParse(queryValue, out tmp) ? tmp : (DateTime?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(DateTimeOffset)) + { + DateTimeOffset value; + expr = DateTimeOffset.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(DateTimeOffset?)) + { + DateTimeOffset tmp; + var value = DateTimeOffset.TryParse(queryValue, out tmp) ? tmp : (DateTimeOffset?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType.IsEnum) + { + int value; + expr = (int.TryParse(queryValue, out value) && Enum.IsDefined(propertyType, value)) + ? GetEnumPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (Nullable<>) && + propertyType.GenericTypeArguments[0].IsEnum) + { + int tmp; + var value = int.TryParse(queryValue, out tmp) ? tmp : (int?) null; + expr = GetEnumPropertyExpression(value, prop, param); + } + else + { + expr = Expression.Constant(true); + } - expr = Expression.OrElse(leftExpr, rightExpr); - } - else - { - var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null)); + return expr; + } - var idValue = queryValue.Trim(); - var idExpr = Expression.Constant(idValue); - var anyParam = Expression.Parameter(elementType); - var relatedIdPropertyExpr = Expression.Property(anyParam, relatedIdProperty); - var relatedIdPropertyEqualsIdExpr = Expression.Equal(relatedIdPropertyExpr, idExpr); - var anyPredicateExpr = Expression.Lambda(relatedIdPropertyEqualsIdExpr, anyParam); - var asQueryableCallExpr = Expression.Call( - typeof(Queryable), - "AsQueryable", - new[] { elementType }, - propertyExpr); - var rightExpr = Expression.Call( - typeof(Queryable), - "Any", - new[] { elementType }, - asQueryableCallExpr, - anyPredicateExpr); + private Expression GetPredicateBodyForRelationship(RelationshipModelProperty modelProperty, string queryValue, ParameterExpression param) + { + var relatedType = modelProperty.RelatedType; + PropertyInfo relatedIdProperty; + try + { + relatedIdProperty = _modelManager.GetIdProperty(relatedType); + } + catch (InvalidOperationException) + { + throw new HttpResponseException(HttpStatusCode.BadRequest); + } - expr = Expression.AndAlso(leftExpr, rightExpr); - } - } - } - else - { - PropertyInfo relatedIdProperty; - try - { - relatedIdProperty = _modelManager.GetIdProperty(propertyType); - } - catch (InvalidOperationException) - { - relatedIdProperty = null; - } + var prop = modelProperty.Property; - if (relatedIdProperty != null) - { - var propertyExpr = Expression.Property(param, prop); + if (modelProperty.IsToMany) + { + var propertyExpr = Expression.Property(param, prop); - if (string.IsNullOrWhiteSpace(queryValue)) - { - expr = Expression.Equal(propertyExpr, Expression.Constant(null)); - } - else - { - var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null)); + if (string.IsNullOrWhiteSpace(queryValue)) + { + var leftExpr = Expression.Equal(propertyExpr, Expression.Constant(null)); - var idValue = queryValue.Trim(); - var idExpr = Expression.Constant(idValue); - var relatedIdPropertyExpr = Expression.Property(propertyExpr, relatedIdProperty); - var rightExpr = Expression.Equal(relatedIdPropertyExpr, idExpr); + var asQueryableCallExpr = Expression.Call( + typeof(Queryable), + "AsQueryable", + new[] { relatedType }, + propertyExpr); + var anyCallExpr = Expression.Call( + typeof(Queryable), + "Any", + new[] { relatedType }, + asQueryableCallExpr); + var rightExpr = Expression.Not(anyCallExpr); - expr = Expression.AndAlso(leftExpr, rightExpr); - } - } - } - } + return Expression.OrElse(leftExpr, rightExpr); + } + else + { + var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null)); - if (expr == null) - expr = Expression.Constant(true); + var idValue = queryValue.Trim(); + var idExpr = Expression.Constant(idValue); + var anyParam = Expression.Parameter(relatedType); + var relatedIdPropertyExpr = Expression.Property(anyParam, relatedIdProperty); + var relatedIdPropertyEqualsIdExpr = Expression.Equal(relatedIdPropertyExpr, idExpr); + var anyPredicateExpr = Expression.Lambda(relatedIdPropertyEqualsIdExpr, anyParam); + var asQueryableCallExpr = Expression.Call( + typeof(Queryable), + "AsQueryable", + new[] { relatedType }, + propertyExpr); + var rightExpr = Expression.Call( + typeof(Queryable), + "Any", + new[] { relatedType }, + asQueryableCallExpr, + anyPredicateExpr); - workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); + return Expression.AndAlso(leftExpr, rightExpr); } } + else + { + var propertyExpr = Expression.Property(param, prop); - return workingExpr ?? Expression.Constant(true); // No filters, so return everything + if (string.IsNullOrWhiteSpace(queryValue)) + return Expression.Equal(propertyExpr, Expression.Constant(null)); + + var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null)); + + var idValue = queryValue.Trim(); + var idExpr = Expression.Constant(idValue); + var relatedIdPropertyExpr = Expression.Property(propertyExpr, relatedIdProperty); + var rightExpr = Expression.Equal(relatedIdPropertyExpr, idExpr); + + return Expression.AndAlso(leftExpr, rightExpr); + } } private static Expression GetPropertyExpression(T value, PropertyInfo property, diff --git a/JSONAPI/ActionFilters/EnableSortingAttribute.cs b/JSONAPI/ActionFilters/EnableSortingAttribute.cs index 90f4608f..5ebb967a 100644 --- a/JSONAPI/ActionFilters/EnableSortingAttribute.cs +++ b/JSONAPI/ActionFilters/EnableSortingAttribute.cs @@ -69,7 +69,7 @@ private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sort { var selectors = new List>>>(); - var usedProperties = new Dictionary(); + var usedProperties = new Dictionary(); var sortExpressions = sortParam.Split(','); foreach (var sortExpression in sortExpressions) @@ -98,7 +98,7 @@ private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sort usedProperties[property] = null; var paramExpr = Expression.Parameter(typeof (T)); - var propertyExpr = Expression.Property(paramExpr, property); + var propertyExpr = Expression.Property(paramExpr, property.Property); var selector = Expression.Lambda>(propertyExpr, paramExpr); selectors.Add(Tuple.Create(ascending, selector)); diff --git a/JSONAPI/Core/IModelManager.cs b/JSONAPI/Core/IModelManager.cs index a7ffe940..0cb83f26 100644 --- a/JSONAPI/Core/IModelManager.cs +++ b/JSONAPI/Core/IModelManager.cs @@ -54,7 +54,7 @@ public interface IModelManager /// The Type to find the property on /// The JSON key representing a property /// - PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey); + ModelProperty GetPropertyForJsonKey(Type type, string jsonKey); /// /// Analogue to System.Type.GetProperties(), but made available so that any caching done @@ -62,8 +62,7 @@ public interface IModelManager /// /// The type to get properties from /// All properties recognized by the IModelManager. - //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? - PropertyInfo[] GetProperties(Type type); + ModelProperty[] GetProperties(Type type); /// /// Determines whether or not the given type will be treated as a "Many" relationship. diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index 60beebd7..ad65fec7 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -1,12 +1,11 @@ -using JSONAPI.Attributes; -using JSONAPI.Json; +using System.Collections.ObjectModel; +using JSONAPI.Attributes; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using JSONAPI.Extensions; +using Newtonsoft.Json; namespace JSONAPI.Core { @@ -15,6 +14,8 @@ public class ModelManager : IModelManager public ModelManager(IPluralizationService pluralizationService) { _pluralizationService = pluralizationService; + RegistrationsByName = new Dictionary(); + RegistrationsByType = new Dictionary(); } protected IPluralizationService _pluralizationService = null; @@ -28,25 +29,36 @@ public IPluralizationService PluralizationService #region Cache storage - protected Lazy> _idProperties - = new Lazy>( - () => new Dictionary() - ); - - protected Lazy>> _propertyMaps - = new Lazy>>( - () => new Dictionary>() - ); - - protected Lazy> _resourceTypeNamesByType - = new Lazy>( - () => new Dictionary() - ); + /// + /// Represents a type's registration with a model manager + /// + protected sealed class TypeRegistration + { + internal TypeRegistration() { } + + /// + /// The type that has been registered + /// + public Type Type { get; internal set; } + + /// + /// The serialized format of the type's name + /// + public string ResourceTypeName { get; internal set; } + + /// + /// The property to be used as this type's ID. + /// + public PropertyInfo IdProperty { get; internal set; } + + /// + /// A resource's properties, keyed by name. + /// + public IReadOnlyDictionary Properties { get; internal set; } + } - protected Lazy> _typesByResourceTypeName - = new Lazy>( - () => new Dictionary() - ); + protected readonly IDictionary RegistrationsByName; + protected readonly IDictionary RegistrationsByType; protected Lazy> _isSerializedAsMany = new Lazy>( @@ -60,101 +72,62 @@ protected Lazy> _getElementType #endregion - #region Id property determination - public PropertyInfo GetIdProperty(Type type) { - PropertyInfo idprop = null; - - var idPropCache = _idProperties.Value; - - lock (idPropCache) - { - if (idPropCache.TryGetValue(type, out idprop)) return idprop; - - // First, look for UseAsIdAttribute - idprop = type.GetProperties() - .Where(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute))) - .FirstOrDefault(); - if (idprop == null) - { - idprop = type.GetProperty("Id"); - } - - if (idprop == null) - throw new InvalidOperationException(String.Format("Unable to determine Id property for type {0}", type)); - - idPropCache.Add(type, idprop); - } - - return idprop; + return GetRegistrationByType(type).IdProperty; } - #endregion - - #region Property Maps - - protected IDictionary GetPropertyMap(Type type) + public ModelProperty[] GetProperties(Type type) { - Dictionary propMap = null; - - var propMapCache = _propertyMaps.Value; - - lock (propMapCache) - { - if (propMapCache.TryGetValue(type, out propMap)) return propMap; - - propMap = new Dictionary(); - PropertyInfo[] props = type.GetProperties(); - foreach (PropertyInfo prop in props) - { - propMap[GetJsonKeyForProperty(prop)] = prop; - } - - propMapCache.Add(type, propMap); - } - - return propMap; + var typeRegistration = GetRegistrationByType(type); + return typeRegistration.Properties.Values.ToArray(); } - public PropertyInfo[] GetProperties(Type type) + public ModelProperty GetPropertyForJsonKey(Type type, string jsonKey) { - return GetPropertyMap(type).Values.ToArray(); + var typeRegistration = GetRegistrationByType(type); + ModelProperty property; + return (typeRegistration.Properties.TryGetValue(jsonKey, out property)) ? property : null; } - public PropertyInfo GetPropertyForJsonKey(Type type, string jsonKey) + public string GetResourceTypeNameForType(Type type) { - PropertyInfo propInfo; - if (GetPropertyMap(type).TryGetValue(jsonKey, out propInfo)) return propInfo; - else return null; // Or, throw an exception here?? + return GetRegistrationByType(type).ResourceTypeName; } - #endregion - - public string GetResourceTypeNameForType(Type type) + public Type GetTypeByResourceTypeName(string resourceTypeName) { - if (IsSerializedAsMany(type)) - type = GetElementType(type); - - var currentType = type; - while (currentType != null && currentType != typeof(Object)) + lock (RegistrationsByName) { - string resourceTypeName; - if (_resourceTypeNamesByType.Value.TryGetValue(currentType, out resourceTypeName)) return resourceTypeName; + TypeRegistration typeRegistration; + if (RegistrationsByName.TryGetValue(resourceTypeName, out typeRegistration)) + return typeRegistration.Type; - // This particular type wasn't registered, but maybe the base type was - currentType = currentType.BaseType; + throw new InvalidOperationException(String.Format("The resource type name `{0}` was not registered.", + resourceTypeName)); } - - throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName)); } - public Type GetTypeByResourceTypeName(string resourceTypeName) + private TypeRegistration GetRegistrationByType(Type type) { - Type type; - if (_typesByResourceTypeName.Value.TryGetValue(resourceTypeName, out type)) return type; + lock (RegistrationsByType) + { + if (IsSerializedAsMany(type)) + type = GetElementType(type); + + var currentType = type; + while (currentType != null && currentType != typeof(Object)) + { + TypeRegistration registration; + if (RegistrationsByType.TryGetValue(currentType, out registration)) + return registration; - throw new InvalidOperationException(String.Format("The resource type name `{0}` was not registered.", resourceTypeName)); + // This particular type wasn't registered, but maybe the base type was. + currentType = currentType.BaseType; + } + } + + throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName)); } /// @@ -174,24 +147,83 @@ public void RegisterResourceType(Type type) /// The resource type name to use public void RegisterResourceType(Type type, string resourceTypeName) { - lock (_resourceTypeNamesByType.Value) + lock (RegistrationsByType) { - lock (_typesByResourceTypeName.Value) + lock (RegistrationsByName) { - if (_resourceTypeNamesByType.Value.ContainsKey(type)) + if (RegistrationsByType.ContainsKey(type)) throw new InvalidOperationException(String.Format("The type `{0}` has already been registered.", type.FullName)); - if (_typesByResourceTypeName.Value.ContainsKey(resourceTypeName)) + if (RegistrationsByName.ContainsKey(resourceTypeName)) throw new InvalidOperationException( String.Format("The resource type name `{0}` has already been registered.", resourceTypeName)); - _resourceTypeNamesByType.Value[type] = resourceTypeName; - _typesByResourceTypeName.Value[resourceTypeName] = type; + var registration = new TypeRegistration + { + Type = type, + ResourceTypeName = resourceTypeName + }; + + var propertyMap = new Dictionary(); + + var idProperty = CalculateIdProperty(type); + if (idProperty == null) + throw new InvalidOperationException(String.Format( + "Unable to determine Id property for type {0}", type)); + + registration.IdProperty = idProperty; + + var props = type.GetProperties(); + foreach (var prop in props) + { + var jsonKey = prop == registration.IdProperty + ? "id" + : GetJsonKeyForProperty(prop); + var property = CreateModelProperty(prop, jsonKey); + propertyMap[jsonKey] = property; + } + + registration.Properties = new ReadOnlyDictionary(propertyMap); + + + RegistrationsByType.Add(type, registration); + RegistrationsByName.Add(resourceTypeName, registration); } } } + /// + /// Creates a cacheable model property representation from a PropertyInfo + /// + /// The property + /// The key that this model property will be serialized as + /// A model property represenation + protected virtual ModelProperty CreateModelProperty(PropertyInfo prop, string jsonKey) + { + var type = prop.PropertyType; + var ignoreByDefault = + prop.CustomAttributes.Any(c => c.AttributeType == typeof(JsonIgnoreAttribute)); + + if (prop.PropertyType.CanWriteAsJsonApiAttribute()) + return new FieldModelProperty(prop, jsonKey, ignoreByDefault); + + var isToMany = + type.IsArray || + (type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType); + + Type relatedType; + if (isToMany) + { + relatedType = type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType(); + } + else + { + relatedType = type; + } + return new RelationshipModelProperty(prop, jsonKey, ignoreByDefault, relatedType, isToMany); + } + /// /// Determines the resource type name for a given type. /// @@ -265,5 +297,18 @@ public Type GetElementType(Type manyType) return etype; } + /// + /// Calculates the ID property for a given resource type. + /// + /// The type to use to calculate the ID for + /// The ID property to use for this type + protected virtual PropertyInfo CalculateIdProperty(Type type) + { + return + type + .GetProperties() + .FirstOrDefault(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute))) + ?? type.GetProperty("Id"); + } } } diff --git a/JSONAPI/Core/ModelProperty.cs b/JSONAPI/Core/ModelProperty.cs new file mode 100644 index 00000000..c8523f1e --- /dev/null +++ b/JSONAPI/Core/ModelProperty.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// Stores a model's property and its usage. + /// + public abstract class ModelProperty + { + internal ModelProperty(PropertyInfo property, string jsonKey, bool ignoreByDefault) + { + IgnoreByDefault = ignoreByDefault; + JsonKey = jsonKey; + Property = property; + } + + /// + /// The PropertyInfo backing this ModelProperty + /// + public PropertyInfo Property { get; private set; } + + /// + /// The key that will be used to represent this property in JSON API documents + /// + public string JsonKey { get; private set; } + + /// + /// Whether this property should be ignored by default for serialization. + /// + public bool IgnoreByDefault { get; private set; } + } + + /// + /// A ModelProperty representing a flat field on a resource object + /// + public sealed class FieldModelProperty : ModelProperty + { + internal FieldModelProperty(PropertyInfo property, string jsonKey, bool ignoreByDefault) + : base(property, jsonKey, ignoreByDefault) + { + } + } + + /// + /// A ModelProperty representing a relationship to another resource + /// + public class RelationshipModelProperty : ModelProperty + { + internal RelationshipModelProperty(PropertyInfo property, string jsonKey, bool ignoreByDefault, Type relatedType, bool isToMany) + : base(property, jsonKey, ignoreByDefault) + { + RelatedType = relatedType; + IsToMany = isToMany; + } + + /// + /// The type of resource found on the other side of this relationship + /// + public Type RelatedType { get; private set; } + + /// + /// Whether the property represents a to-many (true) or to-one (false) relationship + /// + public bool IsToMany { get; private set; } + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 5350137e..4feb31fb 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -79,6 +79,7 @@ + diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 8130bea8..752d44bf 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -180,24 +180,23 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js writer.WriteValue(GetValueForIdProperty(idProp, value)); // Leverage the cached map to avoid another costly call to System.Type.GetProperties() - PropertyInfo[] props = _modelManager.GetProperties(value.GetType()); + var props = _modelManager.GetProperties(value.GetType()); // Do non-model properties first, everything else goes in "links" //TODO: Unless embedded??? - IList modelProps = new List(); + var relationshipModelProperties = new List(); - foreach (PropertyInfo prop in props) + foreach (var modelProperty in props) { - string propKey = _modelManager.GetJsonKeyForProperty(prop); - if (propKey == "id") continue; // Don't write the "id" property twice, see above! + var prop = modelProperty.Property; + if (prop == idProp) continue; // Don't write the "id" property twice, see above! - if (prop.PropertyType.CanWriteAsJsonApiAttribute()) + if (modelProperty is FieldModelProperty) { - if (prop.GetCustomAttributes().Any(attr => attr is JsonIgnoreAttribute)) - continue; + if (modelProperty.IgnoreByDefault) continue; // TODO: allow overriding this // numbers, strings, dates... - writer.WritePropertyName(propKey); + writer.WritePropertyName(modelProperty.JsonKey); var propertyValue = prop.GetValue(value, null); @@ -239,25 +238,26 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js serializer.Serialize(writer, propertyValue); } } - else + else if (modelProperty is RelationshipModelProperty) { - modelProps.Add(prop); - continue; + relationshipModelProperties.Add((RelationshipModelProperty)modelProperty); } } // Now do other stuff - if (modelProps.Count() > 0) + if (relationshipModelProperties.Count() > 0) { writer.WritePropertyName("links"); writer.WriteStartObject(); } - foreach (PropertyInfo prop in modelProps) + foreach (var relationshipModelProperty in relationshipModelProperties) { bool skip = false, iip = false; string lt = null; SerializeAsOptions sa = SerializeAsOptions.Ids; + var prop = relationshipModelProperty.Property; + object[] attrs = prop.GetCustomAttributes(true); foreach (object attr in attrs) @@ -382,7 +382,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js } } - if (modelProps.Count() > 0) + if (relationshipModelProperties.Count() > 0) { writer.WriteEndObject(); } @@ -680,9 +680,7 @@ private object Deserialize(Type objectType, JsonReader reader) if (reader.TokenType == JsonToken.PropertyName) { string value = (string)reader.Value; - PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); - // If the model object has a non-standard Id property, but the "id" key is being used... - if (prop == null && value == "id") prop = _modelManager.GetIdProperty(objectType); + var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value); if (value == "links") { @@ -690,15 +688,17 @@ private object Deserialize(Type objectType, JsonReader reader) //TODO: linked resources (Done??) DeserializeLinkedResources(retval, reader); } - else if (prop != null) + else if (modelProperty != null) { - if (!prop.PropertyType.CanWriteAsJsonApiAttribute()) + if (!(modelProperty is FieldModelProperty)) { reader.Read(); // burn the PropertyName token //TODO: Embedded would be dropped here! continue; // These aren't supposed to be here, they're supposed to be in "links"! } + var prop = modelProperty.Property; + object propVal; Type enumType; if (prop.PropertyType == typeof(string) && @@ -812,10 +812,11 @@ private void DeserializeLinkedResources(object obj, JsonReader reader) { string value = (string)reader.Value; reader.Read(); // burn the PropertyName token - PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value); - if (prop != null && !prop.PropertyType.CanWriteAsJsonApiAttribute()) + var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value) as RelationshipModelProperty; + if (modelProperty != null) { - if (_modelManager.IsSerializedAsMany(prop.PropertyType)) + var prop = modelProperty.Property; + if (modelProperty.IsToMany) { // Is a hasMany From 6536a5db47319a8386bd369c59668550fd70ee80 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 6 Mar 2015 16:46:52 -0600 Subject: [PATCH 033/186] Allow overriding property json key with attribute --- JSONAPI.Tests/Core/ModelManagerTests.cs | 80 +++++++++++++++++++++++-- JSONAPI/Core/IModelManager.cs | 8 --- JSONAPI/Core/ModelManager.cs | 22 +++++-- JSONAPI/Json/JsonApiFormatter.cs | 2 +- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 155139b5..33a4739b 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -1,4 +1,5 @@ using System; +using JSONAPI.Attributes; using Microsoft.VisualStudio.TestTools.UnitTesting; using JSONAPI.Core; using JSONAPI.Tests.Models; @@ -6,6 +7,7 @@ using System.Collections.Generic; using System.Collections; using FluentAssertions; +using Newtonsoft.Json; namespace JSONAPI.Tests.Core { @@ -19,7 +21,7 @@ private class InvalidModel // No Id discernable! private class CustomIdModel { - [JSONAPI.Attributes.UseAsId] + [UseAsId] public Guid Uuid { get; set; } public string Data { get; set; } @@ -30,6 +32,26 @@ private class DerivedPost : Post } + private class Band + { + [UseAsId] + public string BandName { get; set; } + + [JsonProperty("THE-GENRE")] + public string Genre { get; set; } + } + + private class Salad + { + public string Id { get; set; } + + [JsonProperty("salad-type")] + public string TheSaladType { get; set; } + + [JsonProperty("salad-type")] + public string AnotherSaladType { get; set; } + } + [TestMethod] public void FindsIdNamedId() { @@ -56,7 +78,7 @@ public void Cant_register_model_with_missing_id() // Assert action.ShouldThrow() .Which.Message.Should() - .Be("Unable to determine Id property for type JSONAPI.Tests.Core.ModelManagerTests+InvalidModel"); + .Be("Unable to determine Id property for type `invalid-models`."); } [TestMethod] @@ -180,9 +202,9 @@ public void GetJsonKeyForPropertyTest() var mm = new ModelManager(pluralizationService); // Act - var idKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Id")); - var nameKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Name")); - var postsKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Posts")); + var idKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Id")); + var nameKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Name")); + var postsKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Posts")); // Assert Assert.AreEqual("id", idKey); @@ -216,6 +238,54 @@ public void GetPropertyForJsonKeyTest() postsProp.Should().BeOfType(); } + [TestMethod] + public void GetPropertyForJsonKey_returns_correct_value_for_custom_id() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + Type bandType = typeof(Band); + mm.RegisterResourceType(bandType); + + // Act + var idProp = mm.GetPropertyForJsonKey(bandType, "id"); + + // Assert + idProp.Property.Should().BeSameAs(bandType.GetProperty("BandName")); + idProp.Should().BeOfType(); + } + + [TestMethod] + public void GetPropertyForJsonKey_returns_correct_value_for_JsonProperty_attribute() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + Type bandType = typeof(Band); + mm.RegisterResourceType(bandType); + + // Act + var prop = mm.GetPropertyForJsonKey(bandType, "THE-GENRE"); + + // Assert + prop.Property.Should().BeSameAs(bandType.GetProperty("Genre")); + prop.Should().BeOfType(); + } + + [TestMethod] + public void Cant_register_type_with_two_properties_with_the_same_name() + { + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + Type saladType = typeof(Salad); + + // Act + Action action = () => mm.RegisterResourceType(saladType); + + // Assert + action.ShouldThrow().Which.Message.Should().Be("The type `salads` already contains a property keyed at `salad-type`."); + } + [TestMethod] public void IsSerializedAsManyTest() { diff --git a/JSONAPI/Core/IModelManager.cs b/JSONAPI/Core/IModelManager.cs index 0cb83f26..338a281c 100644 --- a/JSONAPI/Core/IModelManager.cs +++ b/JSONAPI/Core/IModelManager.cs @@ -40,14 +40,6 @@ public interface IModelManager /// The type that has been registered for this resource type name. Type GetTypeByResourceTypeName(string resourceTypeName); - /// - /// Returns the key that will be used to represent the given property in serialized - /// JSON. Inverse of GetPropertyForJsonKey. - /// - /// The serializable property - /// The string denoting the given property within a JSON document. - string GetJsonKeyForProperty(PropertyInfo propInfo); //TODO: Do we need to have a type parameter here, in case the property is inherited? - /// /// Returns the property corresponding to a given JSON Key. Inverse of GetJsonKeyForProperty. /// diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index ad65fec7..fd7aaa02 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using JSONAPI.Extensions; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JSONAPI.Core { @@ -170,7 +171,7 @@ public void RegisterResourceType(Type type, string resourceTypeName) var idProperty = CalculateIdProperty(type); if (idProperty == null) throw new InvalidOperationException(String.Format( - "Unable to determine Id property for type {0}", type)); + "Unable to determine Id property for type `{0}`.", resourceTypeName)); registration.IdProperty = idProperty; @@ -179,7 +180,11 @@ public void RegisterResourceType(Type type, string resourceTypeName) { var jsonKey = prop == registration.IdProperty ? "id" - : GetJsonKeyForProperty(prop); + : CalculateJsonKeyForProperty(prop); + if (propertyMap.ContainsKey(jsonKey)) + throw new InvalidOperationException( + String.Format("The type `{0}` already contains a property keyed at `{1}`.", + resourceTypeName, jsonKey)); var property = CreateModelProperty(prop, jsonKey); propertyMap[jsonKey] = property; } @@ -244,13 +249,18 @@ protected virtual string CalculateResourceTypeNameForType(Type type) return FormatPropertyName(PluralizationService.Pluralize(title)).Dasherize(); } - public string GetJsonKeyForProperty(PropertyInfo propInfo) + /// + /// Determines the key that a property will be serialized as. + /// + /// The property + /// The key to serialize the given property as + protected internal virtual string CalculateJsonKeyForProperty(PropertyInfo propInfo) { - return FormatPropertyName(propInfo.Name); - //TODO: Respect [JsonProperty(PropertyName = "FooBar")], and probably cache the result. + var jsonPropertyAttribute = (JsonPropertyAttribute)propInfo.GetCustomAttributes(typeof (JsonPropertyAttribute)).FirstOrDefault(); + return jsonPropertyAttribute != null ? jsonPropertyAttribute.PropertyName : FormatPropertyName(propInfo.Name); } - protected static string FormatPropertyName(string propertyName) + private static string FormatPropertyName(string propertyName) { string result = propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1); return result; diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 752d44bf..7589f7cb 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -277,7 +277,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js } if (skip) continue; - writer.WritePropertyName(_modelManager.GetJsonKeyForProperty(prop)); + writer.WritePropertyName(relationshipModelProperty.JsonKey); // Now look for enumerable-ness: if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) From 017768390a028feddd12e512167de8beabf2a861 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 12 Mar 2015 20:05:50 -0400 Subject: [PATCH 034/186] use PATCH instead of PUT for updates --- .../Acceptance/AcceptanceTestsBase.cs | 6 +- ...tchWithArrayRelationshipValueRequest.json} | 0 ...n => PatchWithAttributeUpdateRequest.json} | 0 ... => PatchWithMissingToManyIdsRequest.json} | 0 ...=> PatchWithMissingToManyTypeRequest.json} | 0 ...on => PatchWithMissingToOneIdRequest.json} | 0 ... => PatchWithMissingToOneTypeRequest.json} | 0 ...n => PatchWithNullToOneUpdateRequest.json} | 0 ...chWithStringRelationshipValueRequest.json} | 0 ...atchWithToManyEmptyDataUpdateRequest.json} | 0 ...thToManyHomogeneousDataUpdateRequest.json} | 0 ...json => PatchWithToManyUpdateRequest.json} | 0 ....json => PatchWithToOneUpdateRequest.json} | 0 ...chWithArrayRelationshipValueResponse.json} | 0 ... => PatchWithAttributeUpdateResponse.json} | 0 ...=> PatchWithMissingToManyIdsResponse.json} | 0 ...> PatchWithMissingToManyTypeResponse.json} | 0 ...n => PatchWithMissingToOneIdResponse.json} | 0 ...=> PatchWithMissingToOneTypeResponse.json} | 0 ... => PatchWithNullToOneUpdateResponse.json} | 0 ...hWithStringRelationshipValueResponse.json} | 0 ...tchWithToManyEmptyDataUpdateResponse.json} | 0 ...hToManyHomogeneousDataUpdateResponse.json} | 0 ...son => PatchWithToManyUpdateResponse.json} | 0 ...json => PatchWithToOneUpdateResponse.json} | 0 .../Acceptance/PostsTests.cs | 72 +++++++++---------- .../JSONAPI.EntityFramework.Tests.csproj | 48 ++++++------- JSONAPI.EntityFramework/Http/ApiController.cs | 2 +- JSONAPI/Http/ApiController.cs | 2 +- 29 files changed, 65 insertions(+), 65 deletions(-) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithArrayRelationshipValueRequest.json => PatchWithArrayRelationshipValueRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithAttributeUpdateRequest.json => PatchWithAttributeUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithMissingToManyIdsRequest.json => PatchWithMissingToManyIdsRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithMissingToManyTypeRequest.json => PatchWithMissingToManyTypeRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithMissingToOneIdRequest.json => PatchWithMissingToOneIdRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithMissingToOneTypeRequest.json => PatchWithMissingToOneTypeRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithNullToOneUpdateRequest.json => PatchWithNullToOneUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithStringRelationshipValueRequest.json => PatchWithStringRelationshipValueRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithToManyEmptyDataUpdateRequest.json => PatchWithToManyEmptyDataUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithToManyHomogeneousDataUpdateRequest.json => PatchWithToManyHomogeneousDataUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithToManyUpdateRequest.json => PatchWithToManyUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PutWithToOneUpdateRequest.json => PatchWithToOneUpdateRequest.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithArrayRelationshipValueResponse.json => PatchWithArrayRelationshipValueResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithAttributeUpdateResponse.json => PatchWithAttributeUpdateResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithMissingToManyIdsResponse.json => PatchWithMissingToManyIdsResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithMissingToManyTypeResponse.json => PatchWithMissingToManyTypeResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithMissingToOneIdResponse.json => PatchWithMissingToOneIdResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithMissingToOneTypeResponse.json => PatchWithMissingToOneTypeResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithNullToOneUpdateResponse.json => PatchWithNullToOneUpdateResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithStringRelationshipValueResponse.json => PatchWithStringRelationshipValueResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithToManyEmptyDataUpdateResponse.json => PatchWithToManyEmptyDataUpdateResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithToManyHomogeneousDataUpdateResponse.json => PatchWithToManyHomogeneousDataUpdateResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithToManyUpdateResponse.json => PatchWithToManyUpdateResponse.json} (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PutWithToOneUpdateResponse.json => PatchWithToOneUpdateResponse.json} (100%) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index f1c5e065..d1be68eb 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -85,9 +85,9 @@ protected async Task SubmitPost(DbConnection effortConnecti } #endregion - #region PUT + #region PATCH - protected async Task SubmitPut(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) + protected async Task SubmitPatch(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) { using (var server = TestServer.Create(app => { @@ -102,7 +102,7 @@ protected async Task SubmitPut(DbConnection effortConnectio .And(request => { request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); - }).SendAsync("PUT"); + }).SendAsync("PATCH"); return response; } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithArrayRelationshipValueRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithAttributeUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyIdsRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyIdsRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyIdsRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToManyTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneIdRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneIdRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneIdRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithMissingToOneTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithNullToOneUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithStringRelationshipValueRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyDataUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyEmptyDataUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyDataUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyHomogeneousDataUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToManyUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PutWithToOneUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithAttributeUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyIdsResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyIdsResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyIdsResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToManyTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithMissingToOneTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithNullToOneUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyDataUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyEmptyDataUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyDataUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyHomogeneousDataUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToManyUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithToOneUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index f9e5ed79..d9cbc9aa 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -94,13 +94,13 @@ public async Task Post() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithAttributeUpdate() + public async Task PatchWithAttributeUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithAttributeUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithAttributeUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithAttributeUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -123,13 +123,13 @@ public async Task PutWithAttributeUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithToManyUpdate() + public async Task PatchWithToManyUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -152,13 +152,13 @@ public async Task PutWithToManyUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithToManyHomogeneousDataUpdate() + public async Task PatchWithToManyHomogeneousDataUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyHomogeneousDataUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -181,13 +181,13 @@ public async Task PutWithToManyHomogeneousDataUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithToManyEmptyDataUpdate() + public async Task PatchWithToManyEmptyDataUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToManyEmptyDataUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyEmptyDataUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToManyEmptyDataUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyEmptyDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -210,13 +210,13 @@ public async Task PutWithToManyEmptyDataUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithToOneUpdate() + public async Task PatchWithToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -239,13 +239,13 @@ public async Task PutWithToOneUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithNullToOneUpdate() + public async Task PatchWithNullToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithNullToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithNullToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -268,13 +268,13 @@ public async Task PutWithNullToOneUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithMissingToOneId() + public async Task PatchWithMissingToOneId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneIdResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneIdResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -297,13 +297,13 @@ public async Task PutWithMissingToOneId() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithMissingToOneType() + public async Task PatchWithMissingToOneType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToOneTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToOneTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneTypeResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -326,13 +326,13 @@ public async Task PutWithMissingToOneType() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithMissingToManyIds() + public async Task PatchWithMissingToManyIds() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyIdsRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyIdsRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyIdsResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyIdsResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -355,13 +355,13 @@ public async Task PutWithMissingToManyIds() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithMissingToManyType() + public async Task PatchWithMissingToManyType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithMissingToManyTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithMissingToManyTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyTypeResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -384,13 +384,13 @@ public async Task PutWithMissingToManyType() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithArrayRelationshipValue() + public async Task PatchWithArrayRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithArrayRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -413,13 +413,13 @@ public async Task PutWithArrayRelationshipValue() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PutWithStringRelationshipValue() + public async Task PatchWithStringRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithStringRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 88eb1ce1..0f544ccd 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -138,29 +138,29 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + Designer @@ -170,7 +170,7 @@ - + Always diff --git a/JSONAPI.EntityFramework/Http/ApiController.cs b/JSONAPI.EntityFramework/Http/ApiController.cs index 491ffef8..93473040 100644 --- a/JSONAPI.EntityFramework/Http/ApiController.cs +++ b/JSONAPI.EntityFramework/Http/ApiController.cs @@ -63,7 +63,7 @@ public override async Task> Post(IList postedObjs) return materialList; } - public override async Task> Put(string id, IList putObjs) + public override async Task> Patch(string id, IList putObjs) { var materializer = this.MaterializerFactory(); DbContext context = materializer.DbContext; diff --git a/JSONAPI/Http/ApiController.cs b/JSONAPI/Http/ApiController.cs index 947fbcc0..5078536d 100644 --- a/JSONAPI/Http/ApiController.cs +++ b/JSONAPI/Http/ApiController.cs @@ -102,7 +102,7 @@ public virtual Task> Post([FromBody] IList postedObjs) /// /// /// - public virtual async Task> Put(string id, IList putObjs) + public virtual async Task> Patch(string id, IList putObjs) { IMaterializer materializer = this.MaterializerFactory(); IList materialList = new List(); From 83a988f64728a65d7078ad0fc66d8b9c3601daf2 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 13:08:03 -0400 Subject: [PATCH 035/186] refactor to streamline API surface --- .../Startup.cs | 39 ++-- .../Acceptance/AcceptanceTestsBase.cs | 1 - ...synchronousEnumerationTransformerTests.cs} | 41 +--- .../JSONAPI.EntityFramework.Tests.csproj | 2 +- .../AsynchronousEnumerationTransformer.cs | 27 +++ .../EnumerateQueryableAsyncAttribute.cs | 39 ---- .../JSONAPI.EntityFramework.csproj | 3 +- .../JsonApiConfigurationExtensions.cs | 23 +++ ...cs => DefaultFilteringTransformerTests.cs} | 189 ++++++++---------- .../DefaultSortingTransformerTests.cs | 146 ++++++++++++++ .../EnableSortingAttributeTests.cs | 182 ----------------- .../QueryableTransformerTestsBase.cs | 15 ++ JSONAPI.Tests/JSONAPI.Tests.csproj | 5 +- JSONAPI.TodoMVC.API/Startup.cs | 33 +-- ...bute.cs => DefaultFilteringTransformer.cs} | 55 +++-- .../DefaultSortingTransformer.cs | 86 ++++++++ .../ActionFilters/EnableSortingAttribute.cs | 128 ------------ .../IQueryableEnumerationTransformer.cs | 22 ++ .../IQueryableFilteringTransformer.cs | 20 ++ .../IQueryableSortingTransformer.cs | 20 ++ .../JsonApiQueryableAttribute.cs | 104 ++++++++++ .../QueryableTransformException.cs | 13 ++ .../SynchronousEnumerationTransformer.cs | 17 ++ JSONAPI/Core/JsonApiConfiguration.cs | 148 ++++++++++++++ JSONAPI/Extensions/QueryableExtensions.cs | 14 ++ JSONAPI/JSONAPI.csproj | 12 +- JSONAPI/Properties/AssemblyInfo.cs | 1 + 27 files changed, 811 insertions(+), 574 deletions(-) rename JSONAPI.EntityFramework.Tests/ActionFilters/{EnumerateQueryableAsyncAttributeTests.cs => AsynchronousEnumerationTransformerTests.cs} (61%) create mode 100644 JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs delete mode 100644 JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs create mode 100644 JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs rename JSONAPI.Tests/ActionFilters/{EnableFilteringAttributeTests.cs => DefaultFilteringTransformerTests.cs} (76%) create mode 100644 JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs delete mode 100644 JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs create mode 100644 JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs rename JSONAPI/ActionFilters/{EnableFilteringAttribute.cs => DefaultFilteringTransformer.cs} (92%) create mode 100644 JSONAPI/ActionFilters/DefaultSortingTransformer.cs delete mode 100644 JSONAPI/ActionFilters/EnableSortingAttribute.cs create mode 100644 JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs create mode 100644 JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs create mode 100644 JSONAPI/ActionFilters/IQueryableSortingTransformer.cs create mode 100644 JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs create mode 100644 JSONAPI/ActionFilters/QueryableTransformException.cs create mode 100644 JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs create mode 100644 JSONAPI/Core/JsonApiConfiguration.cs create mode 100644 JSONAPI/Extensions/QueryableExtensions.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index 8487230c..a26cc087 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -1,17 +1,11 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Web; using System.Web.Http; -using System.Web.Http.Dispatcher; using Autofac; using Autofac.Integration.WebApi; -using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; -using JSONAPI.Json; using Microsoft.Owin; using Owin; @@ -62,32 +56,25 @@ public void Configuration(IAppBuilder app) private static HttpConfiguration GetWebApiConfiguration() { - var config = new HttpConfiguration(); - var pluralizationService = new PluralizationService(); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Comment)); - modelManager.RegisterResourceType(typeof(Post)); - modelManager.RegisterResourceType(typeof(Tag)); - modelManager.RegisterResourceType(typeof(User)); - modelManager.RegisterResourceType(typeof(UserGroup)); - - var formatter = new JsonApiFormatter(modelManager); - config.Formatters.Clear(); - config.Formatters.Add(formatter); + var httpConfig = new HttpConfiguration(); - // Global filters - config.Filters.Add(new EnumerateQueryableAsyncAttribute()); - config.Filters.Add(new EnableSortingAttribute(modelManager)); - config.Filters.Add(new EnableFilteringAttribute(modelManager)); + // Configure JSON API + new JsonApiConfiguration() + .PluralizeResourceTypesWith(pluralizationService) + .UseEntityFramework() + .RegisterResourceType(typeof(Comment)) + .RegisterResourceType(typeof(Post)) + .RegisterResourceType(typeof(Tag)) + .RegisterResourceType(typeof(User)) + .RegisterResourceType(typeof(UserGroup)) + .Apply(httpConfig); - // Override controller selector - config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); // Web API routes - config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); + httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - return config; + return httpConfig; } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index d1be68eb..1b590311 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -1,6 +1,5 @@ using System; using System.Data.Common; -using System.Linq; using System.Net; using System.Net.Http; using System.Text; diff --git a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs similarity index 61% rename from JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs rename to JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs index 42f86fd1..609c95ed 100644 --- a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs +++ b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Formatting; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; @@ -20,7 +19,7 @@ namespace JSONAPI.EntityFramework.Tests.ActionFilters { [TestClass] - public class EnumerateQueryableAsyncAttributeTests + public class AsynchronousEnumerationTransformerTests { public class Dummy { @@ -54,7 +53,7 @@ public void SetupFixtures() }.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator asyncEnumerator) + private IQueryable CreateQueryable(IDbAsyncEnumerator asyncEnumerator) { var mockSet = new Mock>(); mockSet.As>() @@ -69,39 +68,17 @@ private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator mockSet.As>().Setup(m => m.ElementType).Returns(_fixtures.ElementType); mockSet.As>().Setup(m => m.GetEnumerator()).Returns(_fixtures.GetEnumerator()); - var formatter = new JsonMediaTypeFormatter(); - - var httpContent = new ObjectContent(typeof(IQueryable), mockSet.Object, formatter); - - return new HttpActionExecutedContext - { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, "http://api.example.com/dummies") - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; + return mockSet.Object; } [TestMethod] public async Task ResolvesQueryable() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); - - var context = CreateActionExecutedContext(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - - await actionFilter.OnActionExecutedAsync(context, new CancellationToken()); + var transformer = new AsynchronousEnumerationTransformer(); - var objectContent = context.Response.Content as ObjectContent; - objectContent.Should().NotBeNull(); + var query = CreateQueryable(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - var array = objectContent.Value as Dummy[]; + var array = await transformer.Enumerate(query, new CancellationToken()); array.Should().NotBeNull(); array.Length.Should().Be(3); array[0].Id.Should().Be("1"); @@ -112,16 +89,16 @@ public async Task ResolvesQueryable() [TestMethod] public void CancelsProperly() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); + var actionFilter = new AsynchronousEnumerationTransformer(); - var context = CreateActionExecutedContext(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); + var context = CreateQueryable(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); var cts = new CancellationTokenSource(); cts.CancelAfter(300); Func action = async () => { - await actionFilter.OnActionExecutedAsync(context, cts.Token); + await actionFilter.Enumerate(context, cts.Token); }; action.ShouldThrow(); } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 0f544ccd..214450ca 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -115,7 +115,7 @@ - + diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs new file mode 100644 index 00000000..55d05581 --- /dev/null +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -0,0 +1,27 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.ActionFilters; + +namespace JSONAPI.EntityFramework.ActionFilters +{ + internal class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer + { + private readonly Lazy _toArrayAsyncMethod = new Lazy(() => + typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); + + public async Task Enumerate(IQueryable query, CancellationToken cancellationToken) + { + var queryableElementType = typeof (T); + var openToArrayAsyncMethod = _toArrayAsyncMethod.Value; + var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); + var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new object[] { query, cancellationToken }); + + var resultArray = await invocation; + return resultArray; + } + } +} diff --git a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs b/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs deleted file mode 100644 index 40e0f47b..00000000 --- a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Data.Entity; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Filters; - -namespace JSONAPI.EntityFramework.ActionFilters -{ - public class EnumerateQueryableAsyncAttribute : ActionFilterAttribute - { - private readonly Lazy _toArrayAsyncMethod = new Lazy(() => - typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); - - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - if (actionExecutedContext.Response != null) - { - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent != null) - { - var objectType = objectContent.ObjectType; - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - var queryableElementType = objectType.GenericTypeArguments[0]; - var openToArrayAsyncMethod = _toArrayAsyncMethod.Value; - var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); - var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new[] { objectContent.Value, cancellationToken }); - - var resultArray = await invocation; - actionExecutedContext.Response.Content = new ObjectContent(resultArray.GetType(), resultArray, objectContent.Formatter); - } - } - } - } - } -} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 5953bb90..f92e7dce 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -70,10 +70,11 @@ - + + diff --git a/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs b/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs new file mode 100644 index 00000000..08906ba6 --- /dev/null +++ b/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs @@ -0,0 +1,23 @@ +using JSONAPI.Core; +using JSONAPI.EntityFramework.ActionFilters; + +namespace JSONAPI.EntityFramework +{ + /// + /// Extension Methods for JSONAPI.JsonApiConfiguration + /// + public static class JsonApiConfigurationExtensions + { + /// + /// Add Entity Framework specific handling to the configuration + /// + /// The configuration object to modify + /// The same configuration object that was passed in + public static JsonApiConfiguration UseEntityFramework(this JsonApiConfiguration jsonApiConfig) + { + jsonApiConfig.EnumerateQueriesWith(new AsynchronousEnumerationTransformer()); + + return jsonApiConfig; + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs similarity index 76% rename from JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs rename to JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 61223884..32154ad7 100644 --- a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -3,20 +3,16 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters { [TestClass] - public class EnableFilteringAttributeTests + public class DefaultFilteringTransformerTests : QueryableTransformerTestsBase { private enum SomeEnum { @@ -539,48 +535,21 @@ public void SetupFixtures() _fixturesQuery = _fixtures.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) + private DefaultFilteringTransformer GetTransformer() { - var formatter = new JsonApiFormatter(modelManager); - - var httpContent = new ObjectContent(typeof(IQueryable), _fixturesQuery, formatter); - - return new HttpActionExecutedContext + var pluralizationService = new PluralizationService(new Dictionary { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, new Uri(uri)) - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; - } - - private T[] GetArray(string uri) - { - var modelManager = new ModelManager(new PluralizationService()); + {"Dummy", "Dummies"} + }); + var modelManager = new ModelManager(pluralizationService); modelManager.RegisterResourceType(typeof(Dummy)); modelManager.RegisterResourceType(typeof(RelatedItemWithId)); + return new DefaultFilteringTransformer(modelManager); + } - var filter = new EnableFilteringAttribute(modelManager); - - var context = CreateActionExecutedContext(modelManager, uri); - - filter.OnActionExecuted(context); - - var returnedContent = context.Response.Content as ObjectContent; - returnedContent.Should().NotBeNull(); - returnedContent.ObjectType.Should().Be(typeof(IQueryable)); - - var returnedQueryable = returnedContent.Value as IQueryable; - returnedQueryable.Should().NotBeNull(); - - return returnedQueryable.ToArray(); + private Dummy[] GetArray(string uri) + { + return Transform(GetTransformer(), _fixturesQuery, uri).ToArray(); } #region String @@ -588,7 +557,7 @@ private T[] GetArray(string uri) [TestMethod] public void Filters_by_matching_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); + var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("100"); } @@ -596,7 +565,7 @@ public void Filters_by_matching_string_property() [TestMethod] public void Filters_by_missing_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField="); + var returnedArray = GetArray("http://api.example.com/dummies?stringField="); returnedArray.Length.Should().Be(_fixtures.Count - 3); returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); } @@ -608,7 +577,7 @@ public void Filters_by_missing_string_property() [TestMethod] public void Filters_by_matching_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("110"); } @@ -616,14 +585,14 @@ public void Filters_by_matching_datetime_property() [TestMethod] public void Filters_by_missing_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("120"); } @@ -631,7 +600,7 @@ public void Filters_by_matching_nullable_datetime_property() [TestMethod] public void Filters_by_missing_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "120").Should().BeFalse(); } @@ -643,7 +612,7 @@ public void Filters_by_missing_nullable_datetime_property() [TestMethod] public void Filters_by_matching_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("130"); } @@ -651,14 +620,14 @@ public void Filters_by_matching_datetimeoffset_property() [TestMethod] public void Filters_by_missing_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("140"); } @@ -666,7 +635,7 @@ public void Filters_by_matching_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "140").Should().BeFalse(); } @@ -678,7 +647,7 @@ public void Filters_by_missing_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_matching_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); + var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("150"); } @@ -686,14 +655,14 @@ public void Filters_by_matching_enum_property() [TestMethod] public void Filters_by_missing_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField="); + var returnedArray = GetArray("http://api.example.com/dummies?enumField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("160"); } @@ -701,7 +670,7 @@ public void Filters_by_matching_nullable_enum_property() [TestMethod] public void Filters_by_missing_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "160").Should().BeFalse(); } @@ -713,7 +682,7 @@ public void Filters_by_missing_nullable_enum_property() [TestMethod] public void Filters_by_matching_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); + var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("170"); } @@ -721,14 +690,14 @@ public void Filters_by_matching_decimal_property() [TestMethod] public void Filters_by_missing_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("180"); } @@ -736,7 +705,7 @@ public void Filters_by_matching_nullable_decimal_property() [TestMethod] public void Filters_by_missing_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "180").Should().BeFalse(); } @@ -748,7 +717,7 @@ public void Filters_by_missing_nullable_decimal_property() [TestMethod] public void Filters_by_matching_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); + var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("190"); } @@ -756,14 +725,14 @@ public void Filters_by_matching_boolean_property() [TestMethod] public void Filters_by_missing_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("200"); } @@ -771,7 +740,7 @@ public void Filters_by_matching_nullable_boolean_property() [TestMethod] public void Filters_by_missing_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "200").Should().BeFalse(); } @@ -783,7 +752,7 @@ public void Filters_by_missing_nullable_boolean_property() [TestMethod] public void Filters_by_matching_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); + var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("210"); } @@ -791,14 +760,14 @@ public void Filters_by_matching_sbyte_property() [TestMethod] public void Filters_by_missing_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("220"); } @@ -806,7 +775,7 @@ public void Filters_by_matching_nullable_sbyte_property() [TestMethod] public void Filters_by_missing_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "220").Should().BeFalse(); } @@ -818,7 +787,7 @@ public void Filters_by_missing_nullable_sbyte_property() [TestMethod] public void Filters_by_matching_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); + var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("230"); } @@ -826,14 +795,14 @@ public void Filters_by_matching_byte_property() [TestMethod] public void Filters_by_missing_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField="); + var returnedArray = GetArray("http://api.example.com/dummies?byteField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("240"); } @@ -841,7 +810,7 @@ public void Filters_by_matching_nullable_byte_property() [TestMethod] public void Filters_by_missing_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "240").Should().BeFalse(); } @@ -853,7 +822,7 @@ public void Filters_by_missing_nullable_byte_property() [TestMethod] public void Filters_by_matching_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("250"); } @@ -861,14 +830,14 @@ public void Filters_by_matching_int16_property() [TestMethod] public void Filters_by_missing_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("260"); } @@ -876,7 +845,7 @@ public void Filters_by_matching_nullable_int16_property() [TestMethod] public void Filters_by_missing_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "260").Should().BeFalse(); } @@ -888,7 +857,7 @@ public void Filters_by_missing_nullable_int16_property() [TestMethod] public void Filters_by_matching_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("270"); } @@ -896,14 +865,14 @@ public void Filters_by_matching_uint16_property() [TestMethod] public void Filters_by_missing_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("280"); } @@ -911,7 +880,7 @@ public void Filters_by_matching_nullable_uint16_property() [TestMethod] public void Filters_by_missing_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "280").Should().BeFalse(); } @@ -923,7 +892,7 @@ public void Filters_by_missing_nullable_uint16_property() [TestMethod] public void Filters_by_matching_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); + var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("290"); } @@ -931,14 +900,14 @@ public void Filters_by_matching_int32_property() [TestMethod] public void Filters_by_missing_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("300"); } @@ -946,7 +915,7 @@ public void Filters_by_matching_nullable_int32_property() [TestMethod] public void Filters_by_missing_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "300").Should().BeFalse(); } @@ -958,7 +927,7 @@ public void Filters_by_missing_nullable_int32_property() [TestMethod] public void Filters_by_matching_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("310"); } @@ -966,14 +935,14 @@ public void Filters_by_matching_uint32_property() [TestMethod] public void Filters_by_missing_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("320"); } @@ -981,7 +950,7 @@ public void Filters_by_matching_nullable_uint32_property() [TestMethod] public void Filters_by_missing_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "320").Should().BeFalse(); } @@ -993,7 +962,7 @@ public void Filters_by_missing_nullable_uint32_property() [TestMethod] public void Filters_by_matching_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("330"); } @@ -1001,14 +970,14 @@ public void Filters_by_matching_int64_property() [TestMethod] public void Filters_by_missing_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("340"); } @@ -1016,7 +985,7 @@ public void Filters_by_matching_nullable_int64_property() [TestMethod] public void Filters_by_missing_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "340").Should().BeFalse(); } @@ -1028,7 +997,7 @@ public void Filters_by_missing_nullable_int64_property() [TestMethod] public void Filters_by_matching_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("350"); } @@ -1036,14 +1005,14 @@ public void Filters_by_matching_uint64_property() [TestMethod] public void Filters_by_missing_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("360"); } @@ -1051,7 +1020,7 @@ public void Filters_by_matching_nullable_uint64_property() [TestMethod] public void Filters_by_missing_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "360").Should().BeFalse(); } @@ -1063,7 +1032,7 @@ public void Filters_by_missing_nullable_uint64_property() [TestMethod] public void Filters_by_matching_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); + var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("370"); } @@ -1071,14 +1040,14 @@ public void Filters_by_matching_single_property() [TestMethod] public void Filters_by_missing_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField="); + var returnedArray = GetArray("http://api.example.com/dummies?singleField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("380"); } @@ -1086,7 +1055,7 @@ public void Filters_by_matching_nullable_single_property() [TestMethod] public void Filters_by_missing_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "380").Should().BeFalse(); } @@ -1098,7 +1067,7 @@ public void Filters_by_missing_nullable_single_property() [TestMethod] public void Filters_by_matching_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("390"); } @@ -1106,14 +1075,14 @@ public void Filters_by_matching_double_property() [TestMethod] public void Filters_by_missing_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("400"); } @@ -1121,7 +1090,7 @@ public void Filters_by_matching_nullable_double_property() [TestMethod] public void Filters_by_missing_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "400").Should().BeFalse(); } @@ -1133,7 +1102,7 @@ public void Filters_by_missing_nullable_double_property() [TestMethod] public void Does_not_filter_unknown_type() { - Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); + Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); action.ShouldThrow().Which.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } @@ -1144,7 +1113,7 @@ public void Does_not_filter_unknown_type() [TestMethod] public void Filters_by_matching_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); + var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1100"); } @@ -1152,7 +1121,7 @@ public void Filters_by_matching_to_one_relationship_id() [TestMethod] public void Filters_by_missing_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); + var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1100" || d.Id == "1102").Should().BeFalse(); } @@ -1164,7 +1133,7 @@ public void Filters_by_missing_to_one_relationship_id() [TestMethod] public void Filters_by_matching_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); + var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1110"); } @@ -1172,7 +1141,7 @@ public void Filters_by_matching_id_in_to_many_relationship() [TestMethod] public void Filters_by_missing_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); + var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1110" || d.Id == "1120").Should().BeFalse(); } @@ -1184,7 +1153,7 @@ public void Filters_by_missing_id_in_to_many_relationship() [TestMethod] public void Ands_together_filters() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("102"); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs new file mode 100644 index 00000000..dbcbb4fa --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class DefaultSortingTransformerTests : QueryableTransformerTestsBase + { + private class Dummy + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, + new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, + new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, + new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, + new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, + new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, + new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, + new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, + new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } + }; + _fixturesQuery = _fixtures.AsQueryable(); + } + + private DefaultSortingTransformer GetTransformer() + { + var pluralizationService = new PluralizationService(new Dictionary + { + {"Dummy", "Dummies"} + }); + var modelManager = new ModelManager(pluralizationService); + modelManager.RegisterResourceType(typeof(Dummy)); + return new DefaultSortingTransformer(modelManager); + } + + private Dummy[] GetArray(string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return GetTransformer().Sort(_fixturesQuery, request).ToArray(); + } + + private void RunTransformAndExpectFailure(string uri, string expectedMessage) + { + Action action = () => + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // ReSharper disable once UnusedVariable + var result = GetTransformer().Sort(_fixturesQuery, request).ToArray(); + }; + action.ShouldThrow().Which.Message.Should().Be(expectedMessage); + } + + [TestMethod] + public void Sorts_by_attribute_ascending() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); + array.Should().BeInAscendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_attribute_descending() + { + var array = GetArray("http://api.example.com/dummies?sort=-firstName"); + array.Should().BeInDescendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_two_ascending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Sorts_by_two_descending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); + array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_empty() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_whitespace() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort= ", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_missing() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_whitespace() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_no_property_exists() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2Bfoobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); + } + + [TestMethod] + public void Returns_400_if_the_same_property_is_specified_more_than_once() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs deleted file mode 100644 index 03a86adf..00000000 --- a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using FluentAssertions; -using JSONAPI.ActionFilters; -using JSONAPI.Core; -using JSONAPI.Json; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.Tests.ActionFilters -{ - [TestClass] - public class EnableSortingAttributeTests - { - private class Dummy - { - public string Id { get; set; } - - public string FirstName { get; set; } - - public string LastName { get; set; } - } - - private IList _fixtures; - private IQueryable _fixturesQuery; - - [TestInitialize] - public void SetupFixtures() - { - _fixtures = new List - { - new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, - new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, - new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, - new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, - new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, - new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, - new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, - new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, - new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } - }; - _fixturesQuery = _fixtures.AsQueryable(); - } - - private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) - { - var formatter = new JsonApiFormatter(modelManager); - - var httpContent = new ObjectContent(typeof (IQueryable), _fixturesQuery, formatter); - - return new HttpActionExecutedContext - { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, new Uri(uri)) - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; - } - - private HttpResponseMessage GetActionFilterResponse(string uri) - { - var modelManager = new ModelManager(new PluralizationService(new Dictionary - { - { "Dummy", "Dummies" } - })); - modelManager.RegisterResourceType(typeof(Dummy)); - - var filter = new EnableSortingAttribute(modelManager); - - var context = CreateActionExecutedContext(modelManager, uri); - - filter.OnActionExecuted(context); - - return context.Response; - } - - private T[] GetArray(string uri) - { - var response = GetActionFilterResponse(uri); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var returnedContent = (ObjectContent)response.Content; - var returnedQueryable = (IQueryable)returnedContent.Value; - return returnedQueryable.ToArray(); - } - - private void Expect400(string uri, string expectedMessage) - { - Action action = () => - { - GetActionFilterResponse(uri); - }; - var response = action.ShouldThrow().Which.Response; - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var value = (HttpError)((ObjectContent)response.Content).Value; - value.Message.Should().Be(expectedMessage); - } - - [TestMethod] - public void Sorts_by_attribute_ascending() - { - var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); - array.Should().BeInAscendingOrder(d => d.FirstName); - } - - [TestMethod] - public void Sorts_by_attribute_descending() - { - var array = GetArray("http://api.example.com/dummies?sort=-firstName"); - array.Should().BeInDescendingOrder(d => d.FirstName); - } - - [TestMethod] - public void Sorts_by_two_ascending_attributes() - { - var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); - array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); - } - - [TestMethod] - public void Sorts_by_two_descending_attributes() - { - var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); - array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); - } - - [TestMethod] - public void Returns_400_if_sort_argument_is_empty() - { - Expect400("http://api.example.com/dummies?sort=", "The sort expression \"\" is invalid."); - } - - [TestMethod] - public void Returns_400_if_sort_argument_is_whitespace() - { - Expect400("http://api.example.com/dummies?sort= ", "The sort expression \"\" is invalid."); - } - - [TestMethod] - public void Returns_400_if_property_name_is_missing() - { - Expect400("http://api.example.com/dummies?sort=%2B", "The property name is missing."); - } - - [TestMethod] - public void Returns_400_if_property_name_is_whitespace() - { - Expect400("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); - } - - [TestMethod] - public void Returns_400_if_no_property_exists() - { - Expect400("http://api.example.com/dummies?sort=%2Bfoobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); - } - - [TestMethod] - public void Returns_400_if_the_same_property_is_specified_more_than_once() - { - Expect400("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); - } - - [TestMethod] - public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() - { - Expect400("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); - } - } -} diff --git a/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs new file mode 100644 index 00000000..6a621587 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Net.Http; +using JSONAPI.ActionFilters; + +namespace JSONAPI.Tests.ActionFilters +{ + public abstract class QueryableTransformerTestsBase + { + internal IQueryable Transform(IQueryableFilteringTransformer filteringTransformer, IQueryable query, string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return filteringTransformer.Filter(query, request); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 4f377db4..141efa70 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -79,8 +79,9 @@ - - + + + diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index 72d785b5..fa3c1869 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -1,10 +1,6 @@ using System.Web.Http; -using System.Web.Http.Dispatcher; -using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; -using JSONAPI.Http; -using JSONAPI.Json; +using JSONAPI.EntityFramework; using JSONAPI.TodoMVC.API.Models; using Owin; using PluralizationService = JSONAPI.Core.PluralizationService; @@ -22,28 +18,21 @@ public void Configuration(IAppBuilder app) private static HttpConfiguration GetWebApiConfiguration() { var pluralizationService = new PluralizationService(); + pluralizationService.AddMapping("todo", "todos"); - var config = new HttpConfiguration(); + var httpConfig = new HttpConfiguration(); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Todo)); - - var formatter = new JsonApiFormatter(modelManager); - config.Formatters.Clear(); - config.Formatters.Add(formatter); - - // Global filters - config.Filters.Add(new EnumerateQueryableAsyncAttribute()); - config.Filters.Add(new EnableSortingAttribute(modelManager)); - config.Filters.Add(new EnableFilteringAttribute(modelManager)); - - // Override controller selector - config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); + // Configure JSON API + new JsonApiConfiguration() + .PluralizeResourceTypesWith(pluralizationService) + .UseEntityFramework() + .RegisterResourceType(typeof(Todo)) + .Apply(httpConfig); // Web API routes - config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); + httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - return config; + return httpConfig; } } } \ No newline at end of file diff --git a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs similarity index 92% rename from JSONAPI/ActionFilters/EnableFilteringAttribute.cs rename to JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index 94427193..d29890f6 100644 --- a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -5,21 +5,36 @@ using System.Net.Http; using System.Reflection; using System.Web.Http; -using System.Web.Http.Filters; using JSONAPI.Core; namespace JSONAPI.ActionFilters { - public class EnableFilteringAttribute : ActionFilterAttribute + /// + /// This transformer filters an IQueryable payload based on query-string values. + /// + public class DefaultFilteringTransformer : IQueryableFilteringTransformer { private readonly IModelManager _modelManager; - public EnableFilteringAttribute(IModelManager modelManager) + /// + /// Creates a new FilteringQueryableTransformer + /// + /// The model manager used to look up registered type information. + public DefaultFilteringTransformer(IModelManager modelManager) { _modelManager = modelManager; } + public IQueryable Filter(IQueryable query, HttpRequestMessage request) + { + var parameter = Expression.Parameter(typeof(T)); + var bodyExpr = GetPredicateBody(request, parameter); + var lambdaExpr = Expression.Lambda>(bodyExpr, parameter); + return query.Where(lambdaExpr); + } + // Borrowed from http://stackoverflow.com/questions/3631547/select-right-generic-method-with-reflection + // ReSharper disable once UnusedMember.Local private readonly Lazy _whereMethod = new Lazy(() => typeof(Queryable).GetMethods() .Where(x => x.Name == "Where") @@ -39,30 +54,6 @@ public EnableFilteringAttribute(IModelManager modelManager) .SingleOrDefault() ); - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - if (actionExecutedContext.Response != null) - { - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent != null) - { - var objectType = objectContent.ObjectType; - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - var queryableElementType = objectType.GenericTypeArguments[0]; - var parameter = Expression.Parameter(queryableElementType); - var bodyExpr = GetPredicateBody(actionExecutedContext.Request, parameter); - var lambdaExpr = Expression.Lambda(bodyExpr, parameter); - - var genericMethod = _whereMethod.Value.MakeGenericMethod(queryableElementType); - var filteredQuery = genericMethod.Invoke(null, new[] { objectContent.Value, lambdaExpr }); - - actionExecutedContext.Response.Content = new ObjectContent(objectType, filteredQuery, objectContent.Formatter); - } - } - } - } - private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpression param) { Expression workingExpr = null; @@ -74,6 +65,10 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (String.IsNullOrWhiteSpace(queryPair.Key)) continue; + // TODO: Filtering needs to change to use the `filter` query parameter so that sorting no longer presents a conflict. + if (queryPair.Key == "sort") + continue; + ModelProperty modelProperty; try { @@ -100,12 +95,16 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (relationshipModelProperty != null) expr = GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); + if (expr == null) throw new HttpResponseException(HttpStatusCode.BadRequest); + workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); } return workingExpr ?? Expression.Constant(true); // No filters, so return everything } + // ReSharper disable once FunctionComplexityOverflow + // TODO: should probably break this method up private Expression GetPredicateBodyForField(FieldModelProperty modelProperty, string queryValue, ParameterExpression param) { var prop = modelProperty.Property; @@ -427,4 +426,4 @@ private static Expression GetEnumPropertyExpression(int? value, PropertyInfo pro return Expression.Equal(castedPropertyExpr, castedValueExpr); } } -} \ No newline at end of file +} diff --git a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs new file mode 100644 index 00000000..ed4a7db0 --- /dev/null +++ b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using JSONAPI.Core; + +namespace JSONAPI.ActionFilters +{ + /// + /// This transform sorts an IQueryable payload according to query parameters. + /// + public class DefaultSortingTransformer : IQueryableSortingTransformer + { + private readonly IModelManager _modelManager; + + /// + /// Creates a new SortingQueryableTransformer + /// + /// The model manager used to look up registered type information. + public DefaultSortingTransformer(IModelManager modelManager) + { + _modelManager = modelManager; + } + + private const string SortQueryParamKey = "sort"; + + public IQueryable Sort(IQueryable query, HttpRequestMessage request) + { + var queryParams = request.GetQueryNameValuePairs(); + var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); + if (sortParam.Key != SortQueryParamKey) return query; // Nothing to sort here, move along + + var selectors = new List>>>(); + + var usedProperties = new Dictionary(); + + var sortExpressions = sortParam.Value.Split(','); + foreach (var sortExpression in sortExpressions) + { + if (string.IsNullOrWhiteSpace(sortExpression)) + throw new QueryableTransformException(string.Format("The sort expression \"{0}\" is invalid.", sortExpression)); + + var ascending = sortExpression[0] == '+'; + var descending = sortExpression[0] == '-'; + if (!ascending && !descending) + throw new QueryableTransformException(string.Format("The sort expression \"{0}\" does not begin with a direction indicator (+ or -).", sortExpression)); + + var propertyName = sortExpression.Substring(1); + if (string.IsNullOrWhiteSpace(propertyName)) + throw new QueryableTransformException("The property name is missing."); + + var modelProperty = _modelManager.GetPropertyForJsonKey(typeof(T), propertyName); + + if (modelProperty == null) + throw new QueryableTransformException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", + propertyName, _modelManager.GetResourceTypeNameForType(typeof (T)))); + + var property = modelProperty.Property; + + if (usedProperties.ContainsKey(property)) + throw new QueryableTransformException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); + + usedProperties[property] = null; + + var paramExpr = Expression.Parameter(typeof (T)); + var propertyExpr = Expression.Property(paramExpr, property); + var selector = Expression.Lambda>(propertyExpr, paramExpr); + + selectors.Add(Tuple.Create(ascending, selector)); + } + + var firstSelector = selectors.First(); + + IOrderedQueryable workingQuery = + firstSelector.Item1 + ? query.OrderBy(firstSelector.Item2) + : query.OrderByDescending(firstSelector.Item2); + + return selectors.Skip(1).Aggregate(workingQuery, + (current, selector) => + selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); + } + } +} diff --git a/JSONAPI/ActionFilters/EnableSortingAttribute.cs b/JSONAPI/ActionFilters/EnableSortingAttribute.cs deleted file mode 100644 index 5ebb967a..00000000 --- a/JSONAPI/ActionFilters/EnableSortingAttribute.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Web.Http; -using System.Web.Http.Filters; -using JSONAPI.Core; - -namespace JSONAPI.ActionFilters -{ - /// - /// Sorts the IQueryable response content according to json-api - /// - public class EnableSortingAttribute : ActionFilterAttribute - { - private const string SortQueryParamKey = "sort"; - - private readonly IModelManager _modelManager; - - /// The model manager to use to look up model properties by json key name - public EnableSortingAttribute(IModelManager modelManager) - { - _modelManager = modelManager; - } - - private readonly Lazy _openMakeOrderedQueryMethod = - new Lazy(() => typeof(EnableSortingAttribute).GetMethod("MakeOrderedQuery", BindingFlags.NonPublic | BindingFlags.Instance)); - - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - if (actionExecutedContext.Response != null) - { - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent == null) return; - - var objectType = objectContent.ObjectType; - if (!objectType.IsGenericType || objectType.GetGenericTypeDefinition() != typeof (IQueryable<>)) return; - - var queryParams = actionExecutedContext.Request.GetQueryNameValuePairs(); - var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); - if (sortParam.Key != SortQueryParamKey) return; - - var queryableElementType = objectType.GenericTypeArguments[0]; - var makeOrderedQueryMethod = _openMakeOrderedQueryMethod.Value.MakeGenericMethod(queryableElementType); - - try - { - var orderedQuery = makeOrderedQueryMethod.Invoke(this, new[] {objectContent.Value, sortParam.Value}); - - actionExecutedContext.Response.Content = new ObjectContent(objectType, orderedQuery, - objectContent.Formatter); - } - catch (TargetInvocationException ex) - { - var statusCode = ex.InnerException is SortingException - ? HttpStatusCode.BadRequest - : HttpStatusCode.InternalServerError; - throw new HttpResponseException( - actionExecutedContext.Request.CreateErrorResponse(statusCode, ex.InnerException.Message)); - } - } - } - - // ReSharper disable once UnusedMember.Local - private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sortParam) - { - var selectors = new List>>>(); - - var usedProperties = new Dictionary(); - - var sortExpressions = sortParam.Split(','); - foreach (var sortExpression in sortExpressions) - { - if (string.IsNullOrWhiteSpace(sortExpression)) - throw new SortingException(string.Format("The sort expression \"{0}\" is invalid.", sortExpression)); - - var ascending = sortExpression[0] == '+'; - var descending = sortExpression[0] == '-'; - if (!ascending && !descending) - throw new SortingException(string.Format("The sort expression \"{0}\" does not begin with a direction indicator (+ or -).", sortExpression)); - - var propertyName = sortExpression.Substring(1); - if (string.IsNullOrWhiteSpace(propertyName)) - throw new SortingException("The property name is missing."); - - var property = _modelManager.GetPropertyForJsonKey(typeof(T), propertyName); - - if (property == null) - throw new SortingException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", - propertyName, _modelManager.GetResourceTypeNameForType(typeof (T)))); - - if (usedProperties.ContainsKey(property)) - throw new SortingException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); - - usedProperties[property] = null; - - var paramExpr = Expression.Parameter(typeof (T)); - var propertyExpr = Expression.Property(paramExpr, property.Property); - var selector = Expression.Lambda>(propertyExpr, paramExpr); - - selectors.Add(Tuple.Create(ascending, selector)); - } - - var firstSelector = selectors.First(); - - IOrderedQueryable workingQuery = - firstSelector.Item1 - ? sourceQuery.OrderBy(firstSelector.Item2) - : sourceQuery.OrderByDescending(firstSelector.Item2); - - return selectors.Skip(1).Aggregate(workingQuery, - (current, selector) => - selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); - } - } - - internal class SortingException : Exception - { - public SortingException(string message) - : base(message) - { - - } - } -} diff --git a/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs b/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs new file mode 100644 index 00000000..5fd785fd --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace JSONAPI.ActionFilters +{ + /// + /// Provides a service to asynchronously materialize the results of an IQueryable into + /// a concrete in-memory representation for serialization. + /// + public interface IQueryableEnumerationTransformer + { + /// + /// Enumerates the specified query. + /// + /// The query to enumerate + /// The request's cancellation token. If this token is cancelled during enumeration, enumeration must halt. + /// The queryable element type + /// A task yielding the enumerated results of the query + Task Enumerate(IQueryable query, CancellationToken cancellationToken); + } +} diff --git a/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs b/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs new file mode 100644 index 00000000..845644e7 --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Service for filtering an IQueryable according to an HTTP request. + /// + public interface IQueryableFilteringTransformer + { + /// + /// Filters the provided queryable based on information from the request message. + /// + /// The input query + /// The request message + /// The element type of the query + /// The filtered query + IQueryable Filter(IQueryable query, HttpRequestMessage request); + } +} diff --git a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs new file mode 100644 index 00000000..394400ec --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Service for sorting an IQueryable according to an HTTP request. + /// + public interface IQueryableSortingTransformer + { + /// + /// Sorts the provided queryable based on information from the request message. + /// + /// The input query + /// The request message + /// The element type of the query + /// The sorted query + IQueryable Sort(IQueryable query, HttpRequestMessage request); + } +} \ No newline at end of file diff --git a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs new file mode 100644 index 00000000..714b3967 --- /dev/null +++ b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Filters; + +namespace JSONAPI.ActionFilters +{ + /// + /// This is an action filter that you can insert into your Web API pipeline to perform various transforms + /// on IQueryable payloads. + /// + public class JsonApiQueryableAttribute : ActionFilterAttribute + { + private readonly IQueryableEnumerationTransformer _enumerationTransformer; + private readonly IQueryableFilteringTransformer _filteringTransformer; + private readonly IQueryableSortingTransformer _sortingTransformer; + + /// + /// Creates a new JsonApiQueryableAttribute. + /// + /// The transform to be used for enumerating IQueryable payloads. + /// The transform to be used for filtering IQueryable payloads + /// The transform to be used for sorting IQueryable payloads. + public JsonApiQueryableAttribute( + IQueryableEnumerationTransformer enumerationTransformer, + IQueryableFilteringTransformer filteringTransformer = null, + IQueryableSortingTransformer sortingTransformer = null) + { + _sortingTransformer = sortingTransformer; + _enumerationTransformer = enumerationTransformer; + _filteringTransformer = filteringTransformer; + } + + private readonly Lazy _openApplyTransformsMethod = + new Lazy(() => typeof(JsonApiQueryableAttribute).GetMethod("ApplyTransforms", BindingFlags.NonPublic | BindingFlags.Instance)); + + public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + if (actionExecutedContext.Response != null) + { + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent != null) + { + var objectType = objectContent.ObjectType; + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + { + var queryableElementType = objectType.GenericTypeArguments[0]; + var applyTransformsMethod = _openApplyTransformsMethod.Value.MakeGenericMethod(queryableElementType); + + try + { + dynamic materializedQueryTask; + try + { + materializedQueryTask = applyTransformsMethod.Invoke(this, + new[] {objectContent.Value, actionExecutedContext.Request, cancellationToken}); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + + var materializedResults = await materializedQueryTask; + + actionExecutedContext.Response.Content = new ObjectContent( + materializedResults.GetType(), + materializedResults, + objectContent.Formatter); + } + catch (QueryableTransformException ex) + { + throw new HttpResponseException( + actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex.Message)); + } + catch (Exception ex) + { + throw new HttpResponseException( + actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message)); + + } + } + } + } + } + + // ReSharper disable once UnusedMember.Local + private async Task ApplyTransforms(IQueryable query, HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (_filteringTransformer != null) + query = _filteringTransformer.Filter(query, request); + + if (_sortingTransformer != null) + query = _sortingTransformer.Sort(query, request); + + return await _enumerationTransformer.Enumerate(query, cancellationToken); + } + } +} diff --git a/JSONAPI/ActionFilters/QueryableTransformException.cs b/JSONAPI/ActionFilters/QueryableTransformException.cs new file mode 100644 index 00000000..47a0485f --- /dev/null +++ b/JSONAPI/ActionFilters/QueryableTransformException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JSONAPI.ActionFilters +{ + internal class QueryableTransformException : Exception + { + public QueryableTransformException(string message) + : base(message) + { + + } + } +} diff --git a/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs b/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs new file mode 100644 index 00000000..ea084f44 --- /dev/null +++ b/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace JSONAPI.ActionFilters +{ + /// + /// Synchronously enumerates an IQueryable + /// + public class SynchronousEnumerationTransformer : IQueryableEnumerationTransformer + { + public Task Enumerate(IQueryable query, CancellationToken cancellationToken) + { + return Task.FromResult(query.ToArray()); + } + } +} diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs new file mode 100644 index 00000000..df092139 --- /dev/null +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Web.Http; +using System.Web.Http.Dispatcher; +using System.Web.Http.Filters; +using JSONAPI.ActionFilters; +using JSONAPI.Http; +using JSONAPI.Json; + +namespace JSONAPI.Core +{ + /// + /// Configuration API for JSONAPI.NET + /// + public class JsonApiConfiguration + { + private bool _enableFiltering; + private bool _enableSorting; + private IQueryableFilteringTransformer _filteringTransformer; + private IQueryableSortingTransformer _sortingTransformer; + private IQueryableEnumerationTransformer _enumerationTransformer; + private IPluralizationService _pluralizationService; + private readonly IList _resourceTypes; + + /// + /// Creates a new configuration + /// + public JsonApiConfiguration() + { + _enableFiltering = true; + _enableSorting = true; + _filteringTransformer = null; + _sortingTransformer = null; + _enumerationTransformer = null; + _resourceTypes = new List(); + } + + /// + /// Disable filtering of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public JsonApiConfiguration DisableFiltering() + { + _enableFiltering = false; + return this; + } + + /// + /// Disable sorting of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public JsonApiConfiguration DisableSorting() + { + _enableSorting = false; + return this; + } + + /// + /// Specifies a filtering transformer to use for filtering IQueryable response payloads. + /// + /// The filtering transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration FilterWith(IQueryableFilteringTransformer filteringTransformer) + { + _filteringTransformer = filteringTransformer; + return this; + } + + /// + /// Specifies a sorting transformer to use for sorting IQueryable response payloads. + /// + /// The sorting transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration SortWith(IQueryableSortingTransformer sortingTransformer) + { + _sortingTransformer = sortingTransformer; + return this; + } + + /// + /// Specifies an enumeration transformer to use for enumerating IQueryable response payloads. + /// + /// The enumeration transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration EnumerateQueriesWith(IQueryableEnumerationTransformer enumerationTransformer) + { + _enumerationTransformer = enumerationTransformer; + return this; + } + + /// + /// Specifies a service to provide pluralizations of resource type names. + /// + /// The service + /// The same configuration object that was passed in + public JsonApiConfiguration PluralizeResourceTypesWith(IPluralizationService pluralizationService) + { + _pluralizationService = pluralizationService; + return this; + } + + /// + /// Registers a resource type for use with the model manager. + /// + /// + /// + public JsonApiConfiguration RegisterResourceType(Type type) + { + _resourceTypes.Add(type); + return this; + } + + /// + /// Applies the running configuration to an HttpConfiguration instance + /// + /// The HttpConfiguration to apply this JsonApiConfiguration to + public void Apply(HttpConfiguration httpConfig) + { + var pluralizationService = _pluralizationService ?? new PluralizationService(); + var modelManager = new ModelManager(pluralizationService); + foreach (var resourceType in _resourceTypes) + { + modelManager.RegisterResourceType(resourceType); + } + + IQueryableFilteringTransformer filteringTransformer = null; + if (_enableFiltering) + filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(modelManager); + + IQueryableSortingTransformer sortingTransformer = null; + if (_enableSorting) + sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(modelManager); + + IQueryableEnumerationTransformer enumerationTransformer = + _enumerationTransformer ?? new SynchronousEnumerationTransformer(); + + var formatter = new JsonApiFormatter(modelManager); + + httpConfig.Formatters.Clear(); + httpConfig.Formatters.Add(formatter); + + httpConfig.Filters.Add(new JsonApiQueryableAttribute(enumerationTransformer, filteringTransformer, sortingTransformer)); + + httpConfig.Services.Replace(typeof (IHttpControllerSelector), + new PascalizedControllerSelector(httpConfig)); + } + } +} diff --git a/JSONAPI/Extensions/QueryableExtensions.cs b/JSONAPI/Extensions/QueryableExtensions.cs new file mode 100644 index 00000000..e6aca042 --- /dev/null +++ b/JSONAPI/Extensions/QueryableExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Web.Http.Filters; +using JSONAPI.Core; + +namespace JSONAPI.Extensions +{ + internal static class QueryableExtensions + { + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 4feb31fb..b64e686d 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -66,8 +66,14 @@ - - + + + + + + + + @@ -77,9 +83,11 @@ + + diff --git a/JSONAPI/Properties/AssemblyInfo.cs b/JSONAPI/Properties/AssemblyInfo.cs index f61caa98..492b29a6 100644 --- a/JSONAPI/Properties/AssemblyInfo.cs +++ b/JSONAPI/Properties/AssemblyInfo.cs @@ -23,6 +23,7 @@ [assembly: Guid("5b46482f-733f-42bf-b507-37767a6bb948")] [assembly: InternalsVisibleTo("JSONAPI.Tests")] +[assembly: InternalsVisibleTo("JSONAPI.EntityFramework")] [assembly: InternalsVisibleTo("JSONAPI.EntityFramework.Tests")] // This assembly is the default dynamic assembly generated Castle DynamicProxy, From 4594fa3d7c51341e2ed857b063c19a7bc5a729d1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 13:16:42 -0400 Subject: [PATCH 036/186] ensure that AsynchronousEnumerationTransformer is public --- .../ActionFilters/AsynchronousEnumerationTransformer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs index 55d05581..55a74832 100644 --- a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -8,7 +8,10 @@ namespace JSONAPI.EntityFramework.ActionFilters { - internal class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer + /// + /// Enumerates an IQueryable asynchronously using Entity Framework's ToArrayAsync() method. + /// + public class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer { private readonly Lazy _toArrayAsyncMethod = new Lazy(() => typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); From f46e55edd3d0179304fcf8f94ea624e3b44e8e9e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 13:43:04 -0400 Subject: [PATCH 037/186] fix fixture data --- JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index dbcbb4fa..80ea20ed 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -39,7 +39,7 @@ public void SetupFixtures() new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, - new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } + new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" } }; _fixturesQuery = _fixtures.AsQueryable(); } From 2bc4776706b689e5571ea984b577adf9af2aaa80 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 14:48:26 -0400 Subject: [PATCH 038/186] add pagination transformer --- .../DefaultPaginationTransformerTests.cs | 176 ++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + .../DefaultPaginationTransformer.cs | 71 +++++++ .../IQueryablePaginationTransformer.cs | 20 ++ .../JsonApiQueryableAttribute.cs | 9 +- JSONAPI/JSONAPI.csproj | 2 + 6 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs create mode 100644 JSONAPI/ActionFilters/DefaultPaginationTransformer.cs create mode 100644 JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs new file mode 100644 index 00000000..85467f08 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class DefaultPaginationTransformerTests + { + private class Dummy + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, + new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, + new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, + new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, + new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, + new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, + new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, + new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, + new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" } + }; + _fixturesQuery = _fixtures.AsQueryable(); + } + + private DefaultPaginationTransformer GetTransformer(int maxPageSize) + { + return new DefaultPaginationTransformer("page.number", "page.size", maxPageSize); + } + + private Dummy[] GetArray(string uri, int maxPageSize = 50) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return GetTransformer(maxPageSize).ApplyPagination(_fixturesQuery, request).ToArray(); + } + + [TestMethod] + public void ApplyPagination_has_no_effect_when_no_paging_parameters_are_supplied() + { + var array = GetArray("http://api.example.com/dummies"); + array.Length.Should().Be(9); + } + + [TestMethod] + public void ApplyPagination_returns_all_results_when_they_are_within_page() + { + var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=10"); + array.Length.Should().Be(9); + } + + [TestMethod] + public void ApplyPagination_returns_no_results_when_page_size_is_zero() + { + var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=0"); + array.Length.Should().Be(0); + } + + [TestMethod] + public void ApplyPagination_returns_first_page_of_data() + { + var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[0], _fixtures[1], _fixtures[2], _fixtures[3]); + } + + [TestMethod] + public void ApplyPagination_returns_second_page_of_data() + { + var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[4], _fixtures[5], _fixtures[6], _fixtures[7]); + } + + [TestMethod] + public void ApplyPagination_returns_page_at_end() + { + var array = GetArray("http://api.example.com/dummies?page[number]=2&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[8]); + } + + [TestMethod] + public void ApplyPagination_returns_nothing_for_page_after_end() + { + var array = GetArray("http://api.example.com/dummies?page[number]=3&page[size]=4"); + array.Length.Should().Be(0); + } + + [TestMethod] + public void ApplyPagination_uses_max_page_size_when_requested_page_size_is_higher() + { + var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=8", 3); + array.Should().BeEquivalentTo(_fixtures[3], _fixtures[4], _fixtures[5]); + } + + [TestMethod] + public void ApplyPagination_returns_400_if_page_number_is_negative() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=-4&page[size]=4"); + }; + action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public void ApplyPagination_returns_400_if_page_size_is_negative() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0&page[size]=-4"); + }; + action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public void ApplyPagination_returns_400_if_page_number_specified_but_not_size() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0"); + }; + action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public void ApplyPagination_returns_400_if_page_size_specified_but_not_number() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[size]=0"); + }; + action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [TestMethod] + public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_zero() + { + Action action = () => + { + GetTransformer(0); + }; + action.ShouldThrow(); + } + + [TestMethod] + public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_negative() + { + Action action = () => + { + GetTransformer(-4); + }; + action.ShouldThrow(); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 141efa70..5db1a8e3 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs new file mode 100644 index 00000000..1caf528b --- /dev/null +++ b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Performs pagination a + /// + public class DefaultPaginationTransformer : IQueryablePaginationTransformer + { + private const int DefaultPageSize = 100; + + private readonly string _pageNumberQueryParam; + private readonly string _pageSizeQueryParam; + private readonly int? _maxPageSize; + + /// + /// Creates a DefaultPaginationTransformer + /// + /// The query parameter to use to indicate the page number + /// The query parameter to use to indicate the page size + /// The maximum page size to allow clients to request. Leave null for no restriction. + public DefaultPaginationTransformer(string pageNumberQueryParam, string pageSizeQueryParam, int? maxPageSize = null) + { + if (maxPageSize <= 0) throw new ArgumentOutOfRangeException("maxPageSize", "The maximum page size must be 1 or greater."); + + _pageNumberQueryParam = pageNumberQueryParam; + _pageSizeQueryParam = pageSizeQueryParam; + _maxPageSize = maxPageSize; + } + + public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage request) + { + var hasPageNumberParam = false; + var hasPageSizeParam = false; + var pageNumber = 0; + var pageSize = _maxPageSize ?? DefaultPageSize; + foreach (var kvp in request.GetQueryNameValuePairs()) + { + if (kvp.Key == _pageNumberQueryParam) + { + hasPageNumberParam = true; + if (!int.TryParse(kvp.Value, out pageNumber)) throw new HttpResponseException(HttpStatusCode.BadRequest); + } + else if (kvp.Key == _pageSizeQueryParam) + { + hasPageSizeParam = true; + if (!int.TryParse(kvp.Value, out pageSize)) throw new HttpResponseException(HttpStatusCode.BadRequest); + } + } + + if (!hasPageNumberParam && !hasPageSizeParam) + return query; + + if ((hasPageNumberParam && !hasPageSizeParam) || (!hasPageNumberParam && hasPageSizeParam)) + throw new HttpResponseException(HttpStatusCode.BadRequest); + + if (pageNumber < 0 || pageSize < 0) + throw new HttpResponseException(HttpStatusCode.BadRequest); + + if (_maxPageSize != null && pageSize > _maxPageSize.Value) + pageSize = _maxPageSize.Value; + + var skip = pageNumber * pageSize; + return query.Skip(skip).Take(pageSize); + } + } +} diff --git a/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs b/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs new file mode 100644 index 00000000..510374bc --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Provides a service to provide a page of data based on information from the request. + /// + public interface IQueryablePaginationTransformer + { + /// + /// Pages the query according to information from the request. + /// + /// The query to page + /// The request message + /// The queryable element type + /// An IQueryable configured to return a page of data + IQueryable ApplyPagination(IQueryable query, HttpRequestMessage request); + } +} diff --git a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs index 714b3967..b136278d 100644 --- a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs +++ b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs @@ -19,6 +19,7 @@ public class JsonApiQueryableAttribute : ActionFilterAttribute private readonly IQueryableEnumerationTransformer _enumerationTransformer; private readonly IQueryableFilteringTransformer _filteringTransformer; private readonly IQueryableSortingTransformer _sortingTransformer; + private readonly IQueryablePaginationTransformer _paginationTransformer; /// /// Creates a new JsonApiQueryableAttribute. @@ -26,12 +27,15 @@ public class JsonApiQueryableAttribute : ActionFilterAttribute /// The transform to be used for enumerating IQueryable payloads. /// The transform to be used for filtering IQueryable payloads /// The transform to be used for sorting IQueryable payloads. + /// The transform to be used for pagination of IQueryable payloads. public JsonApiQueryableAttribute( IQueryableEnumerationTransformer enumerationTransformer, IQueryableFilteringTransformer filteringTransformer = null, - IQueryableSortingTransformer sortingTransformer = null) + IQueryableSortingTransformer sortingTransformer = null, + IQueryablePaginationTransformer paginationTransformer = null) { _sortingTransformer = sortingTransformer; + _paginationTransformer = paginationTransformer; _enumerationTransformer = enumerationTransformer; _filteringTransformer = filteringTransformer; } @@ -98,6 +102,9 @@ private async Task ApplyTransforms(IQueryable query, HttpRequestMessa if (_sortingTransformer != null) query = _sortingTransformer.Sort(query, request); + if (_paginationTransformer != null) + query = _paginationTransformer.ApplyPagination(query, request); + return await _enumerationTransformer.Enumerate(query, cancellationToken); } } diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index b64e686d..48fdaa01 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -67,8 +67,10 @@ + + From 7e600011260e772e6d299d601d3721fcc40c0e4e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 14:52:47 -0400 Subject: [PATCH 039/186] Add paging to default configuration --- JSONAPI/Core/JsonApiConfiguration.cs | 37 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index df092139..4d10110e 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Web.Http; using System.Web.Http.Dispatcher; -using System.Web.Http.Filters; using JSONAPI.ActionFilters; using JSONAPI.Http; using JSONAPI.Json; @@ -16,8 +15,10 @@ public class JsonApiConfiguration { private bool _enableFiltering; private bool _enableSorting; + private bool _enablePagination; private IQueryableFilteringTransformer _filteringTransformer; private IQueryableSortingTransformer _sortingTransformer; + private IQueryablePaginationTransformer _paginationTransformer; private IQueryableEnumerationTransformer _enumerationTransformer; private IPluralizationService _pluralizationService; private readonly IList _resourceTypes; @@ -29,14 +30,16 @@ public JsonApiConfiguration() { _enableFiltering = true; _enableSorting = true; + _enablePagination = true; _filteringTransformer = null; _sortingTransformer = null; + _paginationTransformer = null; _enumerationTransformer = null; _resourceTypes = new List(); } /// - /// Disable filtering of IQueryables in GET methods + /// Disables filtering of IQueryables in GET methods /// /// The same configuration object the method was called on. public JsonApiConfiguration DisableFiltering() @@ -46,7 +49,7 @@ public JsonApiConfiguration DisableFiltering() } /// - /// Disable sorting of IQueryables in GET methods + /// Disables sorting of IQueryables in GET methods /// /// The same configuration object the method was called on. public JsonApiConfiguration DisableSorting() @@ -55,6 +58,16 @@ public JsonApiConfiguration DisableSorting() return this; } + /// + /// Disables pagination of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public JsonApiConfiguration DisablePagination() + { + _enablePagination = false; + return this; + } + /// /// Specifies a filtering transformer to use for filtering IQueryable response payloads. /// @@ -77,6 +90,17 @@ public JsonApiConfiguration SortWith(IQueryableSortingTransformer sortingTransfo return this; } + /// + /// Specifies a pagination transformer to use for paging IQueryable response payloads. + /// + /// The pagination transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration PageWith(IQueryablePaginationTransformer paginationTransformer) + { + _paginationTransformer = paginationTransformer; + return this; + } + /// /// Specifies an enumeration transformer to use for enumerating IQueryable response payloads. /// @@ -131,6 +155,11 @@ public void Apply(HttpConfiguration httpConfig) if (_enableSorting) sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(modelManager); + IQueryablePaginationTransformer paginationTransformer = null; + if (_enablePagination) + paginationTransformer = + _paginationTransformer ?? new DefaultPaginationTransformer("page.number", "page.size", null); + IQueryableEnumerationTransformer enumerationTransformer = _enumerationTransformer ?? new SynchronousEnumerationTransformer(); @@ -139,7 +168,7 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(formatter); - httpConfig.Filters.Add(new JsonApiQueryableAttribute(enumerationTransformer, filteringTransformer, sortingTransformer)); + httpConfig.Filters.Add(new JsonApiQueryableAttribute(enumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer)); httpConfig.Services.Replace(typeof (IHttpControllerSelector), new PascalizedControllerSelector(httpConfig)); From f8cb6428d02f091a0a75ca8cbecdf2144cd11c2b Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 13 Mar 2015 15:54:49 -0400 Subject: [PATCH 040/186] Use relative paths for project build output Project output should not be an absolute path on the C:\ drive. --- .../JSONAPI.EntityFramework.Tests.csproj | 2 +- JSONAPI.Tests/JSONAPI.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 214450ca..0b37cbf7 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -22,7 +22,7 @@ true full false - C:\temp\JSONAPI.EntityFramework.Tests\bin\Debug\ + bin\Debug\ DEBUG;TRACE prompt 4 diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 5db1a8e3..678ab3b8 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -22,7 +22,7 @@ true full false - C:\temp\JSONAPI.Tests\bin\Debug\ + bin\Debug\ DEBUG;TRACE prompt 4 From 3cc0280e21116728ec1263d0b269306abb236cf7 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 11:44:54 -0400 Subject: [PATCH 041/186] only fetch link ID if link template requests it --- JSONAPI/Json/JsonApiFormatter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 7589f7cb..afcb5091 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -319,8 +319,9 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js break; case SerializeAsOptions.Link: if (lt == null) throw new JsonSerializationException("A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); - //TODO: Check for "{0}" in linkTemplate and (only) if it's there, get the Ids of all objects and "implode" them. - string href = String.Format(lt, null, GetIdFor(value)); + + string linkId = lt.Contains("{0}") ? GetIdFor(value) : null; + string href = String.Format(lt, null, linkId); //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); From 584b4e820be56850df815196ac8bc1d5f4da8024 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 11:49:46 -0400 Subject: [PATCH 042/186] Revert "only fetch link ID if link template requests it" This reverts commit 3cc0280e21116728ec1263d0b269306abb236cf7. --- JSONAPI/Json/JsonApiFormatter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index afcb5091..7589f7cb 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -319,9 +319,8 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js break; case SerializeAsOptions.Link: if (lt == null) throw new JsonSerializationException("A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); - - string linkId = lt.Contains("{0}") ? GetIdFor(value) : null; - string href = String.Format(lt, null, linkId); + //TODO: Check for "{0}" in linkTemplate and (only) if it's there, get the Ids of all objects and "implode" them. + string href = String.Format(lt, null, GetIdFor(value)); //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); From 350f28acc62319980b55601e0267d0be703049e0 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 11:58:32 -0400 Subject: [PATCH 043/186] fetch ID for related object only if link template needs it --- JSONAPI/Json/JsonApiFormatter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 7589f7cb..e776a39e 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -348,13 +348,13 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js continue; } - string objId = GetIdFor(propertyValue); + Lazy objId = new Lazy(() => GetIdFor(propertyValue)); switch (sa) { case SerializeAsOptions.Ids: //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); - serializer.Serialize(writer, objId); + serializer.Serialize(writer, objId.Value); if (iip) if (aggregator != null) aggregator.Add(prop.PropertyType, propertyValue); @@ -363,8 +363,8 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js if (lt == null) throw new JsonSerializationException( "A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); - string link = String.Format(lt, objId, - GetIdFor(value)); //value.GetType().GetProperty("Id").GetValue(value, null)); + var relatedObjectId = lt.Contains("{0}") ? objId.Value : null; + string link = String.Format(lt, relatedObjectId, GetIdFor(value)); //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); writer.WriteStartObject(); @@ -376,7 +376,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js // Not really supported by Ember Data yet, incidentally...but easy to implement here. //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //serializer.Serialize(writer, prop.GetValue(value, null)); - this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); + this.Serialize(propertyValue, writeStream, writer, serializer, aggregator); break; } } From 693953485bde8d0aa319ba1f18a818d91f45675d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 16:14:10 -0400 Subject: [PATCH 044/186] don't filter by ignored properties --- JSONAPI/ActionFilters/DefaultFilteringTransformer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index d29890f6..f68d0a00 100644 --- a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -85,6 +85,8 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress Expression expr = null; + if (modelProperty.IgnoreByDefault) continue; + // See if it is a field property var fieldModelProperty = modelProperty as FieldModelProperty; if (fieldModelProperty != null) From 7a4159b0d67fce3d740247fb5987c1e370bf10b9 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 16:33:01 -0400 Subject: [PATCH 045/186] ignore page query param in filtering transform Temporary fix until filtering transform uses the filter param --- JSONAPI/ActionFilters/DefaultFilteringTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index f68d0a00..d02eabd4 100644 --- a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -66,7 +66,7 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress continue; // TODO: Filtering needs to change to use the `filter` query parameter so that sorting no longer presents a conflict. - if (queryPair.Key == "sort") + if (queryPair.Key == "sort" || queryPair.Key.StartsWith("page.")) continue; ModelProperty modelProperty; From 51373f0de24fe9e88739a80ecf5929ee82eda9c1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 18:42:10 -0400 Subject: [PATCH 046/186] require IOrderedQueryable from sort transform --- .../ActionFilters/DefaultSortingTransformer.cs | 15 +++++++++++---- .../ActionFilters/IQueryableSortingTransformer.cs | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs index ed4a7db0..1f86a883 100644 --- a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs @@ -26,17 +26,24 @@ public DefaultSortingTransformer(IModelManager modelManager) private const string SortQueryParamKey = "sort"; - public IQueryable Sort(IQueryable query, HttpRequestMessage request) + public IOrderedQueryable Sort(IQueryable query, HttpRequestMessage request) { var queryParams = request.GetQueryNameValuePairs(); var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); - if (sortParam.Key != SortQueryParamKey) return query; // Nothing to sort here, move along - var selectors = new List>>>(); + string[] sortExpressions; + if (sortParam.Key != SortQueryParamKey) + { + sortExpressions = new[] { "+id" }; // We have to sort by something, so make it the ID. + } + else + { + sortExpressions = sortParam.Value.Split(','); + } + var selectors = new List>>>(); var usedProperties = new Dictionary(); - var sortExpressions = sortParam.Value.Split(','); foreach (var sortExpression in sortExpressions) { if (string.IsNullOrWhiteSpace(sortExpression)) diff --git a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs index 394400ec..ced08b89 100644 --- a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs +++ b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs @@ -15,6 +15,6 @@ public interface IQueryableSortingTransformer /// The request message /// The element type of the query /// The sorted query - IQueryable Sort(IQueryable query, HttpRequestMessage request); + IOrderedQueryable Sort(IQueryable query, HttpRequestMessage request); } } \ No newline at end of file From 302e9aa093e0d79f56a237397a3bf23297e46288 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 14 Mar 2015 21:35:02 -0400 Subject: [PATCH 047/186] Use filter query param for filtering --- .../Acceptance/PostsTests.cs | 2 +- .../DefaultFilteringTransformerTests.cs | 136 +++++++++--------- .../DefaultFilteringTransformer.cs | 7 +- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index d9cbc9aa..55a96bf6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -38,7 +38,7 @@ public async Task GetWithFilter() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "posts?title=Post 4"); + var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json", HttpStatusCode.OK); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 32154ad7..8b0626f8 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -557,7 +557,7 @@ private Dummy[] GetArray(string uri) [TestMethod] public void Filters_by_matching_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]=String value 1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("100"); } @@ -565,7 +565,7 @@ public void Filters_by_matching_string_property() [TestMethod] public void Filters_by_missing_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]="); returnedArray.Length.Should().Be(_fixtures.Count - 3); returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); } @@ -577,7 +577,7 @@ public void Filters_by_missing_string_property() [TestMethod] public void Filters_by_matching_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeField]=1930-11-07"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("110"); } @@ -585,14 +585,14 @@ public void Filters_by_matching_datetime_property() [TestMethod] public void Filters_by_missing_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeField]=1961-02-18"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("120"); } @@ -600,7 +600,7 @@ public void Filters_by_matching_nullable_datetime_property() [TestMethod] public void Filters_by_missing_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "120").Should().BeFalse(); } @@ -612,7 +612,7 @@ public void Filters_by_missing_nullable_datetime_property() [TestMethod] public void Filters_by_matching_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeOffsetField]=1991-01-03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("130"); } @@ -620,14 +620,14 @@ public void Filters_by_matching_datetimeoffset_property() [TestMethod] public void Filters_by_missing_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeOffsetField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeOffsetField]=2014-05-05"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("140"); } @@ -635,7 +635,7 @@ public void Filters_by_matching_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeOffsetField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "140").Should().BeFalse(); } @@ -647,7 +647,7 @@ public void Filters_by_missing_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_matching_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enumField]=1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("150"); } @@ -655,14 +655,14 @@ public void Filters_by_matching_enum_property() [TestMethod] public void Filters_by_missing_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enumField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableEnumField]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("160"); } @@ -670,7 +670,7 @@ public void Filters_by_matching_nullable_enum_property() [TestMethod] public void Filters_by_missing_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableEnumField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "160").Should().BeFalse(); } @@ -682,7 +682,7 @@ public void Filters_by_missing_nullable_enum_property() [TestMethod] public void Filters_by_matching_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimalField]=4.03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("170"); } @@ -690,14 +690,14 @@ public void Filters_by_matching_decimal_property() [TestMethod] public void Filters_by_missing_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimalField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDecimalField]=12.09"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("180"); } @@ -705,7 +705,7 @@ public void Filters_by_matching_nullable_decimal_property() [TestMethod] public void Filters_by_missing_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDecimalField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "180").Should().BeFalse(); } @@ -717,7 +717,7 @@ public void Filters_by_missing_nullable_decimal_property() [TestMethod] public void Filters_by_matching_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[booleanField]=true"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("190"); } @@ -725,14 +725,14 @@ public void Filters_by_matching_boolean_property() [TestMethod] public void Filters_by_missing_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[booleanField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableBooleanField]=false"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("200"); } @@ -740,7 +740,7 @@ public void Filters_by_matching_nullable_boolean_property() [TestMethod] public void Filters_by_missing_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableBooleanField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "200").Should().BeFalse(); } @@ -752,7 +752,7 @@ public void Filters_by_missing_nullable_boolean_property() [TestMethod] public void Filters_by_matching_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sByteField]=63"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("210"); } @@ -760,14 +760,14 @@ public void Filters_by_matching_sbyte_property() [TestMethod] public void Filters_by_missing_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sByteField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSByteField]=91"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("220"); } @@ -775,7 +775,7 @@ public void Filters_by_matching_nullable_sbyte_property() [TestMethod] public void Filters_by_missing_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSByteField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "220").Should().BeFalse(); } @@ -787,7 +787,7 @@ public void Filters_by_missing_nullable_sbyte_property() [TestMethod] public void Filters_by_matching_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byteField]=250"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("230"); } @@ -795,14 +795,14 @@ public void Filters_by_matching_byte_property() [TestMethod] public void Filters_by_missing_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byteField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableByteField]=44"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("240"); } @@ -810,7 +810,7 @@ public void Filters_by_matching_nullable_byte_property() [TestMethod] public void Filters_by_missing_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableByteField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "240").Should().BeFalse(); } @@ -822,7 +822,7 @@ public void Filters_by_missing_nullable_byte_property() [TestMethod] public void Filters_by_matching_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16Field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("250"); } @@ -830,14 +830,14 @@ public void Filters_by_matching_int16_property() [TestMethod] public void Filters_by_missing_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt16Field]=32764"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("260"); } @@ -845,7 +845,7 @@ public void Filters_by_matching_nullable_int16_property() [TestMethod] public void Filters_by_missing_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt16Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "260").Should().BeFalse(); } @@ -857,7 +857,7 @@ public void Filters_by_missing_nullable_int16_property() [TestMethod] public void Filters_by_matching_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt16Field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("270"); } @@ -865,14 +865,14 @@ public void Filters_by_matching_uint16_property() [TestMethod] public void Filters_by_missing_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt16Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt16Field]=65000"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("280"); } @@ -880,7 +880,7 @@ public void Filters_by_matching_nullable_uint16_property() [TestMethod] public void Filters_by_missing_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt16Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "280").Should().BeFalse(); } @@ -892,7 +892,7 @@ public void Filters_by_missing_nullable_uint16_property() [TestMethod] public void Filters_by_matching_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32Field]=100000006"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("290"); } @@ -900,14 +900,14 @@ public void Filters_by_matching_int32_property() [TestMethod] public void Filters_by_missing_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt32Field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("300"); } @@ -915,7 +915,7 @@ public void Filters_by_matching_nullable_int32_property() [TestMethod] public void Filters_by_missing_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt32Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "300").Should().BeFalse(); } @@ -927,7 +927,7 @@ public void Filters_by_missing_nullable_int32_property() [TestMethod] public void Filters_by_matching_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt32Field]=123456789"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("310"); } @@ -935,14 +935,14 @@ public void Filters_by_matching_uint32_property() [TestMethod] public void Filters_by_missing_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt32Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt32Field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("320"); } @@ -950,7 +950,7 @@ public void Filters_by_matching_nullable_uint32_property() [TestMethod] public void Filters_by_missing_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt32Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "320").Should().BeFalse(); } @@ -962,7 +962,7 @@ public void Filters_by_missing_nullable_uint32_property() [TestMethod] public void Filters_by_matching_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64Field]=123453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("330"); } @@ -970,14 +970,14 @@ public void Filters_by_matching_int64_property() [TestMethod] public void Filters_by_missing_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt64Field]=345671901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("340"); } @@ -985,7 +985,7 @@ public void Filters_by_matching_nullable_int64_property() [TestMethod] public void Filters_by_missing_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt64Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "340").Should().BeFalse(); } @@ -997,7 +997,7 @@ public void Filters_by_missing_nullable_int64_property() [TestMethod] public void Filters_by_matching_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt64Field]=123456789012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("350"); } @@ -1005,14 +1005,14 @@ public void Filters_by_matching_uint64_property() [TestMethod] public void Filters_by_missing_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt64Field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt64Field]=345678901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("360"); } @@ -1020,7 +1020,7 @@ public void Filters_by_matching_nullable_uint64_property() [TestMethod] public void Filters_by_missing_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt64Field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "360").Should().BeFalse(); } @@ -1032,7 +1032,7 @@ public void Filters_by_missing_nullable_uint64_property() [TestMethod] public void Filters_by_matching_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[singleField]=21.56901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("370"); } @@ -1040,14 +1040,14 @@ public void Filters_by_matching_single_property() [TestMethod] public void Filters_by_missing_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[singleField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSingleField]=1.3456"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("380"); } @@ -1055,7 +1055,7 @@ public void Filters_by_matching_nullable_single_property() [TestMethod] public void Filters_by_missing_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSingleField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "380").Should().BeFalse(); } @@ -1067,7 +1067,7 @@ public void Filters_by_missing_nullable_single_property() [TestMethod] public void Filters_by_matching_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[doubleField]=12.3453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("390"); } @@ -1075,14 +1075,14 @@ public void Filters_by_matching_double_property() [TestMethod] public void Filters_by_missing_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[doubleField]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDoubleField]=34567.1901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("400"); } @@ -1090,7 +1090,7 @@ public void Filters_by_matching_nullable_double_property() [TestMethod] public void Filters_by_missing_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDoubleField]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "400").Should().BeFalse(); } @@ -1102,7 +1102,7 @@ public void Filters_by_missing_nullable_double_property() [TestMethod] public void Does_not_filter_unknown_type() { - Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); + Action action = () => GetArray("http://api.example.com/dummies?filter[unknownTypeField]=asdfasd"); action.ShouldThrow().Which.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } @@ -1113,7 +1113,7 @@ public void Does_not_filter_unknown_type() [TestMethod] public void Filters_by_matching_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[toOneRelatedItem]=1101"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1100"); } @@ -1121,7 +1121,7 @@ public void Filters_by_matching_to_one_relationship_id() [TestMethod] public void Filters_by_missing_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[toOneRelatedItem]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1100" || d.Id == "1102").Should().BeFalse(); } @@ -1133,7 +1133,7 @@ public void Filters_by_missing_to_one_relationship_id() [TestMethod] public void Filters_by_matching_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[toManyRelatedItems]=1111"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1110"); } @@ -1141,7 +1141,7 @@ public void Filters_by_matching_id_in_to_many_relationship() [TestMethod] public void Filters_by_missing_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[toManyRelatedItems]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1110" || d.Id == "1120").Should().BeFalse(); } @@ -1153,7 +1153,7 @@ public void Filters_by_missing_id_in_to_many_relationship() [TestMethod] public void Ands_together_filters() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]=String value 2&filter[enumField]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("102"); } diff --git a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index d02eabd4..1f1a5937 100644 --- a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -65,14 +65,15 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (String.IsNullOrWhiteSpace(queryPair.Key)) continue; - // TODO: Filtering needs to change to use the `filter` query parameter so that sorting no longer presents a conflict. - if (queryPair.Key == "sort" || queryPair.Key.StartsWith("page.")) + if (!queryPair.Key.StartsWith("filter.")) continue; + var filterField = queryPair.Key.Substring(7); // Skip "filter." + ModelProperty modelProperty; try { - modelProperty = _modelManager.GetPropertyForJsonKey(type, queryPair.Key); + modelProperty = _modelManager.GetPropertyForJsonKey(type, filterField); } catch (InvalidOperationException) { From 1be6db38a178104d54a55b931cda93b012e5321d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 15 Mar 2015 16:35:13 -0400 Subject: [PATCH 048/186] allow specifying model manager to fluent config This change lets the user specify a model manager for the fluent config to use. This means that type registrations will be done directly against the model manager instead of the fluent config. This isn't the nicest solution, but without taking over the IDependencyResolver, I couldn't think of anything better. --- .../Startup.cs | 18 ++++---- JSONAPI.TodoMVC.API/Startup.cs | 6 +-- JSONAPI/Core/JsonApiConfiguration.cs | 44 ++++--------------- JSONAPI/Core/ModelManager.cs | 8 ++-- 4 files changed, 26 insertions(+), 50 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index a26cc087..aea371e0 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -56,18 +56,20 @@ public void Configuration(IAppBuilder app) private static HttpConfiguration GetWebApiConfiguration() { - var pluralizationService = new PluralizationService(); var httpConfig = new HttpConfiguration(); + + // Configure the model manager + var pluralizationService = new PluralizationService(); + var modelManager = new ModelManager(pluralizationService) + .RegisterResourceType(typeof (Comment)) + .RegisterResourceType(typeof (Post)) + .RegisterResourceType(typeof (Tag)) + .RegisterResourceType(typeof (User)) + .RegisterResourceType(typeof (UserGroup)); // Configure JSON API - new JsonApiConfiguration() - .PluralizeResourceTypesWith(pluralizationService) + new JsonApiConfiguration(modelManager) .UseEntityFramework() - .RegisterResourceType(typeof(Comment)) - .RegisterResourceType(typeof(Post)) - .RegisterResourceType(typeof(Tag)) - .RegisterResourceType(typeof(User)) - .RegisterResourceType(typeof(UserGroup)) .Apply(httpConfig); diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index fa3c1869..cba4dff1 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -19,14 +19,14 @@ private static HttpConfiguration GetWebApiConfiguration() { var pluralizationService = new PluralizationService(); pluralizationService.AddMapping("todo", "todos"); + var modelManager = new ModelManager(pluralizationService); + modelManager.RegisterResourceType(typeof (Todo)); var httpConfig = new HttpConfiguration(); // Configure JSON API - new JsonApiConfiguration() - .PluralizeResourceTypesWith(pluralizationService) + new JsonApiConfiguration(modelManager) .UseEntityFramework() - .RegisterResourceType(typeof(Todo)) .Apply(httpConfig); // Web API routes diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index 4d10110e..85c667e7 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -20,14 +20,15 @@ public class JsonApiConfiguration private IQueryableSortingTransformer _sortingTransformer; private IQueryablePaginationTransformer _paginationTransformer; private IQueryableEnumerationTransformer _enumerationTransformer; - private IPluralizationService _pluralizationService; - private readonly IList _resourceTypes; + private readonly IModelManager _modelManager; /// /// Creates a new configuration /// - public JsonApiConfiguration() + public JsonApiConfiguration(IModelManager modelManager) { + if (modelManager == null) throw new Exception("You must provide "); + _enableFiltering = true; _enableSorting = true; _enablePagination = true; @@ -35,7 +36,7 @@ public JsonApiConfiguration() _sortingTransformer = null; _paginationTransformer = null; _enumerationTransformer = null; - _resourceTypes = new List(); + _modelManager = modelManager; } /// @@ -112,48 +113,19 @@ public JsonApiConfiguration EnumerateQueriesWith(IQueryableEnumerationTransforme return this; } - /// - /// Specifies a service to provide pluralizations of resource type names. - /// - /// The service - /// The same configuration object that was passed in - public JsonApiConfiguration PluralizeResourceTypesWith(IPluralizationService pluralizationService) - { - _pluralizationService = pluralizationService; - return this; - } - - /// - /// Registers a resource type for use with the model manager. - /// - /// - /// - public JsonApiConfiguration RegisterResourceType(Type type) - { - _resourceTypes.Add(type); - return this; - } - /// /// Applies the running configuration to an HttpConfiguration instance /// /// The HttpConfiguration to apply this JsonApiConfiguration to public void Apply(HttpConfiguration httpConfig) { - var pluralizationService = _pluralizationService ?? new PluralizationService(); - var modelManager = new ModelManager(pluralizationService); - foreach (var resourceType in _resourceTypes) - { - modelManager.RegisterResourceType(resourceType); - } - IQueryableFilteringTransformer filteringTransformer = null; if (_enableFiltering) - filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(modelManager); + filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(_modelManager); IQueryableSortingTransformer sortingTransformer = null; if (_enableSorting) - sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(modelManager); + sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(_modelManager); IQueryablePaginationTransformer paginationTransformer = null; if (_enablePagination) @@ -163,7 +135,7 @@ public void Apply(HttpConfiguration httpConfig) IQueryableEnumerationTransformer enumerationTransformer = _enumerationTransformer ?? new SynchronousEnumerationTransformer(); - var formatter = new JsonApiFormatter(modelManager); + var formatter = new JsonApiFormatter(_modelManager); httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(formatter); diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index fd7aaa02..319596dc 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -135,10 +135,10 @@ private TypeRegistration GetRegistrationByType(Type type) /// Registers a type with this ModelManager. /// /// The type to register. - public void RegisterResourceType(Type type) + public ModelManager RegisterResourceType(Type type) { var resourceTypeName = CalculateResourceTypeNameForType(type); - RegisterResourceType(type, resourceTypeName); + return RegisterResourceType(type, resourceTypeName); } /// @@ -146,7 +146,7 @@ public void RegisterResourceType(Type type) /// /// The type to register. /// The resource type name to use - public void RegisterResourceType(Type type, string resourceTypeName) + public ModelManager RegisterResourceType(Type type, string resourceTypeName) { lock (RegistrationsByType) { @@ -196,6 +196,8 @@ public void RegisterResourceType(Type type, string resourceTypeName) RegistrationsByName.Add(resourceTypeName, registration); } } + + return this; } /// From 0a2610181834bf318ffd4b8c89cb020b5c5ec090 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 16 Mar 2015 16:10:53 -0400 Subject: [PATCH 049/186] add model manager method to determine whether type is registered --- JSONAPI.Tests/Core/ModelManagerTests.cs | 88 +++++++++++++++++++++++++ JSONAPI/Core/IModelManager.cs | 7 ++ JSONAPI/Core/ModelManager.cs | 16 ++++- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs index 33a4739b..3168125a 100644 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ b/JSONAPI.Tests/Core/ModelManagerTests.cs @@ -194,6 +194,94 @@ public void GetTypeByResourceTypeName_fails_when_getting_unregistered_name() action.ShouldThrow().WithMessage("The resource type name `posts` was not registered."); } + [TestMethod] + public void TypeIsRegistered_returns_true_if_type_is_registered() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof (Post)); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof (Post)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_if_parent_type_is_registered() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof(DerivedPost)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_for_collection_of_registered_types() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_for_collection_of_children_of_registered_types() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + mm.RegisterResourceType(typeof(Post)); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof(Comment)); + + // Assert + isRegistered.Should().BeFalse(); + } + + [TestMethod] + public void TypeIsRegistered_returns_false_for_collection_of_unregistered_types() + { + // Arrange + var pluralizationService = new PluralizationService(); + var mm = new ModelManager(pluralizationService); + + // Act + var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); + + // Assert + isRegistered.Should().BeFalse(); + } + [TestMethod] public void GetJsonKeyForPropertyTest() { diff --git a/JSONAPI/Core/IModelManager.cs b/JSONAPI/Core/IModelManager.cs index 338a281c..ecfde6fa 100644 --- a/JSONAPI/Core/IModelManager.cs +++ b/JSONAPI/Core/IModelManager.cs @@ -40,6 +40,13 @@ public interface IModelManager /// The type that has been registered for this resource type name. Type GetTypeByResourceTypeName(string resourceTypeName); + /// + /// Determines whether a given type has been registered. + /// + /// The type + /// Whether the type is registered + bool TypeIsRegistered(Type type); + /// /// Returns the property corresponding to a given JSON Key. Inverse of GetJsonKeyForProperty. /// diff --git a/JSONAPI/Core/ModelManager.cs b/JSONAPI/Core/ModelManager.cs index 319596dc..776ffb7f 100644 --- a/JSONAPI/Core/ModelManager.cs +++ b/JSONAPI/Core/ModelManager.cs @@ -109,7 +109,13 @@ public Type GetTypeByResourceTypeName(string resourceTypeName) } } - private TypeRegistration GetRegistrationByType(Type type) + public bool TypeIsRegistered(Type type) + { + var registration = FindRegistrationForType(type); + return registration != null; + } + + private TypeRegistration FindRegistrationForType(Type type) { lock (RegistrationsByType) { @@ -128,6 +134,14 @@ private TypeRegistration GetRegistrationByType(Type type) } } + return null; + } + + private TypeRegistration GetRegistrationByType(Type type) + { + var registration = FindRegistrationForType(type); + if (registration != null) return registration; + throw new InvalidOperationException(String.Format("The type `{0}` was not registered.", type.FullName)); } From 44fa79beedd59483aa003205e99bb248afc92cd0 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 19 Mar 2015 18:04:25 -0400 Subject: [PATCH 050/186] extract key-name determination facility Figured this made more sense as an extension method. --- ...erTests.cs => DbContextExtensionsTests.cs} | 22 ++------- .../JSONAPI.EntityFramework.Tests.csproj | 2 +- .../DbContextExtensions.cs | 48 +++++++++++++++++++ .../JSONAPI.EntityFramework.csproj | 1 + 4 files changed, 54 insertions(+), 19 deletions(-) rename JSONAPI.EntityFramework.Tests/{EntityFrameworkMaterializerTests.cs => DbContextExtensionsTests.cs} (67%) create mode 100644 JSONAPI.EntityFramework/DbContextExtensions.cs diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs similarity index 67% rename from JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs rename to JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs index 5c81c3ce..c55dc8b1 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs @@ -7,13 +7,11 @@ using FluentAssertions; using System.Collections.Generic; using System.Data.Entity; -using JSONAPI.Core; -using Moq; namespace JSONAPI.EntityFramework.Tests { [TestClass] - public class EntityFrameworkMaterializerTests + public class DbContextExtensionsTests { private class TestDbContext : DbContext { @@ -60,12 +58,8 @@ private void CleanupTest() [TestMethod] public void GetKeyNamesStandardIdTest() { - // Arrange - var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); - // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Post)).ToArray(); + IEnumerable keyNames = _context.GetKeyNames(typeof(Post)).ToArray(); // Assert keyNames.Count().Should().Be(1); @@ -75,12 +69,8 @@ public void GetKeyNamesStandardIdTest() [TestMethod] public void GetKeyNamesNonStandardIdTest() { - // Arrange - var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); - // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Backlink)).ToArray(); + IEnumerable keyNames = _context.GetKeyNames(typeof(Backlink)).ToArray(); // Assert keyNames.Count().Should().Be(1); @@ -90,12 +80,8 @@ public void GetKeyNamesNonStandardIdTest() [TestMethod] public void GetKeyNamesNotAnEntityTest() { - // Arrange - var mockMetadataManager = new Mock(MockBehavior.Strict); - var materializer = new EntityFrameworkMaterializer(_context, mockMetadataManager.Object); - // Act - Action action = () => materializer.GetKeyNames(typeof (NotAnEntity)); + Action action = () => _context.GetKeyNames(typeof (NotAnEntity)); action.ShouldThrow().Which.Message.Should().Be("The Type NotAnEntity was not found in the DbContext with Type TestDbContext"); } } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 0b37cbf7..1124e888 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -116,7 +116,7 @@ - + diff --git a/JSONAPI.EntityFramework/DbContextExtensions.cs b/JSONAPI.EntityFramework/DbContextExtensions.cs new file mode 100644 index 00000000..f1d6870f --- /dev/null +++ b/JSONAPI.EntityFramework/DbContextExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace JSONAPI.EntityFramework +{ + internal static class DbContextExtensions + { + internal static IEnumerable GetKeyNames(this DbContext dbContext, Type type) + { + var openMethod = typeof(DbContextExtensions).GetMethod("GetKeyNamesFromGeneric", BindingFlags.NonPublic | BindingFlags.Static); + var method = openMethod.MakeGenericMethod(type); + try + { + return (IEnumerable)method.Invoke(null, new object[] { dbContext }); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + // ReSharper disable once UnusedMember.Local + private static IEnumerable GetKeyNamesFromGeneric(this DbContext dbContext) where T : class + { + var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; + ObjectSet objectSet; + try + { + objectSet = objectContext.CreateObjectSet(); + + } + catch (InvalidOperationException e) + { + throw new ArgumentException( + String.Format("The Type {0} was not found in the DbContext with Type {1}", typeof(T).Name, dbContext.GetType().Name), + e + ); + } + return objectSet.EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); + } + + } +} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index f92e7dce..6633a64f 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -71,6 +71,7 @@ + From 0f5f9cd581221b6330441da045f6324b32d9fa65 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Mar 2015 12:41:22 -0400 Subject: [PATCH 051/186] make DbContextExtensions public --- JSONAPI.EntityFramework/DbContextExtensions.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/JSONAPI.EntityFramework/DbContextExtensions.cs b/JSONAPI.EntityFramework/DbContextExtensions.cs index f1d6870f..1c0d4d7d 100644 --- a/JSONAPI.EntityFramework/DbContextExtensions.cs +++ b/JSONAPI.EntityFramework/DbContextExtensions.cs @@ -8,9 +8,18 @@ namespace JSONAPI.EntityFramework { - internal static class DbContextExtensions + /// + /// Extensions on DbContext useful for JSONAPI.NET + /// + public static class DbContextExtensions { - internal static IEnumerable GetKeyNames(this DbContext dbContext, Type type) + /// + /// Gets the ID key names for an entity type + /// + /// + /// + /// + public static IEnumerable GetKeyNames(this DbContext dbContext, Type type) { var openMethod = typeof(DbContextExtensions).GetMethod("GetKeyNamesFromGeneric", BindingFlags.NonPublic | BindingFlags.Static); var method = openMethod.MakeGenericMethod(type); From d60024ebb53196e2f12943a7e8d6542f2962cbfd Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 20 Mar 2015 14:33:44 -0400 Subject: [PATCH 052/186] rethrow HttpResponseException --- JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs index b136278d..2cd8b7bc 100644 --- a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs +++ b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs @@ -76,6 +76,10 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio materializedResults, objectContent.Formatter); } + catch (HttpResponseException) + { + throw; + } catch (QueryableTransformException ex) { throw new HttpResponseException( From 6f2e16b8b82f8932c729ece343aad3773e220fb7 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Apr 2015 19:53:51 -0400 Subject: [PATCH 053/186] return more useful exception message for pagination errors --- .../DefaultPaginationTransformerTests.cs | 8 +++---- .../DefaultPaginationTransformer.cs | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs index 85467f08..8ab3508c 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -120,7 +120,7 @@ public void ApplyPagination_returns_400_if_page_number_is_negative() { GetArray("http://api.example.com/dummies?page[number]=-4&page[size]=4"); }; - action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + action.ShouldThrow().And.Message.Should().Be("page.number must be not be negative."); } [TestMethod] @@ -130,7 +130,7 @@ public void ApplyPagination_returns_400_if_page_size_is_negative() { GetArray("http://api.example.com/dummies?page[number]=0&page[size]=-4"); }; - action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + action.ShouldThrow().And.Message.Should().Be("page.size must be not be negative."); } [TestMethod] @@ -140,7 +140,7 @@ public void ApplyPagination_returns_400_if_page_number_specified_but_not_size() { GetArray("http://api.example.com/dummies?page[number]=0"); }; - action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + action.ShouldThrow().And.Message.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); } [TestMethod] @@ -150,7 +150,7 @@ public void ApplyPagination_returns_400_if_page_size_specified_but_not_number() { GetArray("http://api.example.com/dummies?page[size]=0"); }; - action.ShouldThrow().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + action.ShouldThrow().And.Message.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); } [TestMethod] diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs index 1caf528b..9f7e2495 100644 --- a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs @@ -43,12 +43,17 @@ public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage if (kvp.Key == _pageNumberQueryParam) { hasPageNumberParam = true; - if (!int.TryParse(kvp.Value, out pageNumber)) throw new HttpResponseException(HttpStatusCode.BadRequest); + if (!int.TryParse(kvp.Value, out pageNumber)) + throw new QueryableTransformException( + String.Format("{0} must be a positive integer.", _pageNumberQueryParam)); + } else if (kvp.Key == _pageSizeQueryParam) { hasPageSizeParam = true; - if (!int.TryParse(kvp.Value, out pageSize)) throw new HttpResponseException(HttpStatusCode.BadRequest); + if (!int.TryParse(kvp.Value, out pageSize)) + throw new QueryableTransformException( + String.Format("{0} must be a positive integer.", _pageSizeQueryParam)); } } @@ -56,10 +61,17 @@ public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage return query; if ((hasPageNumberParam && !hasPageSizeParam) || (!hasPageNumberParam && hasPageSizeParam)) - throw new HttpResponseException(HttpStatusCode.BadRequest); + throw new QueryableTransformException( + String.Format("In order for paging to work properly, if either {0} or {1} is set, both must be.", + _pageNumberQueryParam, _pageSizeQueryParam)); + + if (pageNumber < 0) + throw new QueryableTransformException( + String.Format("{0} must be not be negative.", _pageNumberQueryParam)); - if (pageNumber < 0 || pageSize < 0) - throw new HttpResponseException(HttpStatusCode.BadRequest); + if (pageSize < 0) + throw new QueryableTransformException( + String.Format("{0} must be not be negative.", _pageSizeQueryParam)); if (_maxPageSize != null && pageSize > _maxPageSize.Value) pageSize = _maxPageSize.Value; From 775330c32aa3a1b7b5b5ed17942bda9d9c1695f1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 14 Apr 2015 00:28:49 -0400 Subject: [PATCH 054/186] use related key instead of href --- JSONAPI.Tests/Data/LinkTemplateTest.json | 2 +- JSONAPI/Json/JsonApiFormatter.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json index d5607aca..90ebee37 100644 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ b/JSONAPI.Tests/Data/LinkTemplateTest.json @@ -5,7 +5,7 @@ "title": "How to fry an egg", "links": { "author": { - "href": "/users/5" + "related": "/users/5" } } } diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index e776a39e..502936a8 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -324,7 +324,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); - writer.WritePropertyName("href"); + writer.WritePropertyName("related"); writer.WriteValue(href); writer.WriteEndObject(); break; @@ -368,7 +368,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); writer.WriteStartObject(); - writer.WritePropertyName("href"); + writer.WritePropertyName("related"); writer.WriteValue(link); writer.WriteEndObject(); break; From 740c0cebb7d5aae3ceedf484a09b3c508c28f7e1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 14 Apr 2015 16:38:25 -0400 Subject: [PATCH 055/186] Properly serialize null values Before, returning null from an action would throw an exception. Now, it serializes null or an empty array, depending on the cardinality of the action result type. --- JSONAPI.Tests/Data/EmptyArrayResult.json | 3 + JSONAPI.Tests/Data/NullResourceResult.json | 3 + JSONAPI.Tests/JSONAPI.Tests.csproj | 6 ++ .../Json/JsonApiMediaFormatterTests.cs | 60 +++++++++++++++++++ JSONAPI/Json/JsonApiFormatter.cs | 42 +++++++++---- 5 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 JSONAPI.Tests/Data/EmptyArrayResult.json create mode 100644 JSONAPI.Tests/Data/NullResourceResult.json diff --git a/JSONAPI.Tests/Data/EmptyArrayResult.json b/JSONAPI.Tests/Data/EmptyArrayResult.json new file mode 100644 index 00000000..b269b256 --- /dev/null +++ b/JSONAPI.Tests/Data/EmptyArrayResult.json @@ -0,0 +1,3 @@ +{ + "data": [ ] +} diff --git a/JSONAPI.Tests/Data/NullResourceResult.json b/JSONAPI.Tests/Data/NullResourceResult.json new file mode 100644 index 00000000..04ba24bd --- /dev/null +++ b/JSONAPI.Tests/Data/NullResourceResult.json @@ -0,0 +1,3 @@ +{ + "data": null +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 678ab3b8..7b2d5664 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -106,6 +106,12 @@ + + Always + + + Always + Always diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index abb0720f..d0e8d1a1 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -307,6 +307,66 @@ public void Reformats_raw_json_string_with_unquoted_keys() output.Should().Be(minifiedExpectedJson); } + [TestMethod] + [DeploymentItem(@"Data\NullResourceResult.json")] + public void Serializes_null_resource_properly() + { + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); + MemoryStream stream = new MemoryStream(); + + // Act + formatter.WriteToStreamAsync(typeof(Comment), null, stream, null, null); + + // Assert + var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("NullResourceResult.json")); + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\EmptyArrayResult.json")] + public void Serializes_null_resource_array_as_empty_array() + { + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); + MemoryStream stream = new MemoryStream(); + + // Act + formatter.WriteToStreamAsync(typeof(Comment[]), null, stream, null, null); + + // Assert + var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("EmptyArrayResult.json")); + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\EmptyArrayResult.json")] + public void Serializes_null_list_as_empty_array() + { + // Arrange + var modelManager = new ModelManager(new PluralizationService()); + modelManager.RegisterResourceType(typeof(Comment)); + var formatter = new JsonApiFormatter(modelManager); + MemoryStream stream = new MemoryStream(); + + // Act + formatter.WriteToStreamAsync(typeof(List), null, stream, null, null); + + // Assert + var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("EmptyArrayResult.json")); + string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + output.Should().Be(minifiedExpectedJson); + } + [TestMethod] [DeploymentItem(@"Data\MalformedRawJsonString.json")] public void Does_not_serialize_malformed_raw_json_string() diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 502936a8..9b62978c 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -117,23 +117,39 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w } else { - Type valtype = GetSingleType(value.GetType()); - if (_modelManager.IsSerializedAsMany(value.GetType())) - aggregator.AddPrimary(valtype, (IEnumerable) value); - else - aggregator.AddPrimary(valtype, value); - - //writer.Formatting = Formatting.Indented; - writer.WriteStartObject(); writer.WritePropertyName(PrimaryDataKeyName); - if (_modelManager.IsSerializedAsMany(value.GetType())) - this.SerializeMany(value, writeStream, writer, serializer, aggregator); + + if (value == null) + { + if (_modelManager.IsSerializedAsMany(type)) + { + writer.WriteStartArray(); + writer.WriteEndArray(); + } + else + { + writer.WriteNull(); + } + } else - this.Serialize(value, writeStream, writer, serializer, aggregator); + { + Type valtype = GetSingleType(value.GetType()); + if (_modelManager.IsSerializedAsMany(value.GetType())) + aggregator.AddPrimary(valtype, (IEnumerable) value); + else + aggregator.AddPrimary(valtype, value); + + //writer.Formatting = Formatting.Indented; - // Include links from aggregator - SerializeLinkedResources(writeStream, writer, serializer, aggregator); + if (_modelManager.IsSerializedAsMany(value.GetType())) + this.SerializeMany(value, writeStream, writer, serializer, aggregator); + else + this.Serialize(value, writeStream, writer, serializer, aggregator); + + // Include links from aggregator + SerializeLinkedResources(writeStream, writer, serializer, aggregator); + } writer.WriteEndObject(); } From a9f8c5372533e159d73ce50cd12f51d796b0c049 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 16 Apr 2015 00:57:42 -0400 Subject: [PATCH 056/186] support deserialization of linkage objects --- .../PatchWithArrayForToOneLinkageRequest.json | 13 + ...PatchWithMissingToManyLinkageRequest.json} | 1 - ...PatchWithNullForToManyLinkageRequest.json} | 2 +- .../PatchWithNullToOneUpdateRequest.json | 4 +- ...atchWithObjectForToManyLinkageRequest.json | 13 + ...atchWithStringForToManyLinkageRequest.json | 13 + ...atchWithStringForToOneLinkageRequest.json} | 2 +- ...hWithToManyEmptyLinkageUpdateRequest.json} | 2 +- ...ithToManyHomogeneousDataUpdateRequest.json | 2 +- ...thToManyLinkageObjectMissingIdRequest.json | 13 + ...ToManyLinkageObjectMissingTypeRequest.json | 13 + .../PatchWithToManyUpdateRequest.json | 3 +- ...ithToOneLinkageObjectMissingIdRequest.json | 13 + ...ToOneLinkageObjectMissingTypeRequest.json} | 2 +- .../Requests/PatchWithToOneUpdateRequest.json | 6 +- .../Fixtures/Posts/Requests/PostRequest.json | 7 +- ...atchWithArrayForToOneLinkageResponse.json} | 2 +- ...tchWithArrayRelationshipValueResponse.json | 2 +- ...PatchWithMissingToManyLinkageResponse.json | 12 + ...atchWithNullForToManyLinkageResponse.json} | 2 +- ...chWithObjectForToManyLinkageResponse.json} | 2 +- ...chWithStringForToManyLinkageResponse.json} | 2 +- ...atchWithStringForToOneLinkageResponse.json | 12 + ...chWithStringRelationshipValueResponse.json | 2 +- ...WithToManyEmptyLinkageUpdateResponse.json} | 0 ...hToManyLinkageObjectMissingIdResponse.json | 12 + ...oManyLinkageObjectMissingTypeResponse.json | 12 + ...thToOneLinkageObjectMissingIdResponse.json | 12 + ...ToOneLinkageObjectMissingTypeResponse.json | 12 + .../Acceptance/PostsTests.cs | 205 +++++++++- .../JSONAPI.EntityFramework.Tests.csproj | 32 +- .../Data/DeserializeCollectionRequest.json | 15 +- ...adataManagerPropertyWasPresentRequest.json | 6 +- ...eformatsRawJsonStringWithUnquotedKeys.json | 2 +- .../Json/JsonApiMediaFormatterTests.cs | 7 +- JSONAPI/Json/JsonApiFormatter.cs | 355 +++++++----------- 36 files changed, 551 insertions(+), 264 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PatchWithToManyEmptyDataUpdateRequest.json => PatchWithMissingToManyLinkageRequest.json} (85%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PatchWithMissingToManyTypeRequest.json => PatchWithNullForToManyLinkageRequest.json} (83%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PatchWithMissingToOneIdRequest.json => PatchWithStringForToOneLinkageRequest.json} (83%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PatchWithMissingToManyIdsRequest.json => PatchWithToManyEmptyLinkageUpdateRequest.json} (83%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/{PatchWithMissingToOneTypeRequest.json => PatchWithToOneLinkageObjectMissingTypeRequest.json} (79%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PatchWithMissingToOneTypeResponse.json => PatchWithArrayForToOneLinkageResponse.json} (76%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PatchWithMissingToOneIdResponse.json => PatchWithNullForToManyLinkageResponse.json} (76%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PatchWithMissingToManyIdsResponse.json => PatchWithObjectForToManyLinkageResponse.json} (75%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PatchWithMissingToManyTypeResponse.json => PatchWithStringForToManyLinkageResponse.json} (75%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/{PatchWithToManyEmptyDataUpdateResponse.json => PatchWithToManyEmptyLinkageUpdateResponse.json} (100%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json new file mode 100644 index 00000000..b259486d --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "author": { + "linkage": [ { "type": "users", "id": "403" } ] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json similarity index 85% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyDataUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json index f87eb1c1..b8e4a2af 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json @@ -5,7 +5,6 @@ "id": "202", "links": { "tags": { - "data": [] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json similarity index 83% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json index 8649da07..c360c795 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json @@ -5,7 +5,7 @@ "id": "202", "links": { "tags": { - "ids": [ "301" ] + "linkage": null } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json index ffeb70e2..75bc2796 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json @@ -4,7 +4,9 @@ "type": "posts", "id": "202", "links": { - "author": null + "author": { + "linkage": null + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json new file mode 100644 index 00000000..692472ca --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "tags": { + "linkage": { "type": "tags", "id": "301" } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json new file mode 100644 index 00000000..2537e5db --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "tags": { + "linkage": "301" + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json similarity index 83% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneIdRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json index d08c7f90..3a6b508f 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json @@ -5,7 +5,7 @@ "id": "202", "links": { "author": { - "type": "users" + "linkage": "403" } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyIdsRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json similarity index 83% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyIdsRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json index 51f807c3..b2643e19 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyIdsRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json @@ -5,7 +5,7 @@ "id": "202", "links": { "tags": { - "type": "tags" + "linkage": [] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json index d4a56c0a..527ff671 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json @@ -5,7 +5,7 @@ "id": "202", "links": { "tags": { - "data": [ + "linkage": [ { "id": "301", "type": "tags" diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json new file mode 100644 index 00000000..688e21d2 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "tags": { + "linkage": [ { "type": "tags" } ] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json new file mode 100644 index 00000000..f89a4f48 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "tags": { + "linkage": [ { "id": "301" } ] + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json index 9dea2bc2..c87cc2dc 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json @@ -5,8 +5,7 @@ "id": "202", "links": { "tags": { - "type": "tags", - "ids": ["301"] + "linkage": [ { "type": "tags", "id": "301" } ] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json new file mode 100644 index 00000000..3687916e --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "type": "posts", + "id": "202", + "links": { + "author": { + "linkage": { "type": "users" } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json similarity index 79% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json index 69a9860b..f59e54f4 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json @@ -5,7 +5,7 @@ "id": "202", "links": { "author": { - "id": "403" + "linkage": { "id": "403" } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json index 675c67a3..c2b50f31 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json @@ -5,8 +5,10 @@ "id": "202", "links": { "author": { - "type": "users", - "id": "403" + "linkage": { + "type": "users", + "id": "403" + } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json index 9cfdb44e..aed51861 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json @@ -8,8 +8,11 @@ "created": "2015-03-11T04:31:00+00:00", "links": { "author": { - "type": "users", - "id": "401" + "linkage": { + "type": "users", + "id": "401" + + } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json similarity index 76% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json index 18b4f910..2083692b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneTypeResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Nothing was specified for the `type` property.", + "detail": "Expected an object value for `linkage` but got Array.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json index f7fd4197..c69467a9 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "The value of a to-many relationship must be an object.", + "detail": "Each relationship key on a links object must have an object value.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json new file mode 100644 index 00000000..f01ca7cc --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Expected an array value for `linkage` but no `linkage` key was found.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json similarity index 76% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json index 5e021e73..2cc9a741 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Nothing was specified for the `id` property.", + "detail": "Expected an array value for `linkage` but got Null.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyIdsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json similarity index 75% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyIdsResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json index c247c761..69566939 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyIdsResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "If `data` is not specified, then `ids` must be specified.", + "detail": "Expected an array value for `linkage` but got Object.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json similarity index 75% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json index 5e05ae2c..48910763 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyTypeResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "If `data` is not specified, then `type` must be specified.", + "detail": "Expected an array value for `linkage` but got String.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json new file mode 100644 index 00000000..d9775cdd --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Expected an object value for `linkage` but got String.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json index ce669b8d..c69467a9 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "500", "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "The value of a to-one relationship must be an object or null.", + "detail": "Each relationship key on a links object must have an object value.", "stackTrace": "{{STACK_TRACE}}", "inner": null } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyDataUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json new file mode 100644 index 00000000..c7cf76f2 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Each linkage object must have a string value for the key `id`.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json new file mode 100644 index 00000000..1b599a87 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Each linkage object must have a string value for the key `type`.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json new file mode 100644 index 00000000..c7cf76f2 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Each linkage object must have a string value for the key `id`.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json new file mode 100644 index 00000000..1b599a87 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", + "detail": "Each linkage object must have a string value for the key `type`.", + "stackTrace": "{{STACK_TRACE}}", + "inner": null + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index 55a96bf6..7085b692 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -181,13 +181,13 @@ public async Task PatchWithToManyHomogeneousDataUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PatchWithToManyEmptyDataUpdate() + public async Task PatchWithToManyEmptyLinkageUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyEmptyDataUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyEmptyDataUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -268,13 +268,13 @@ public async Task PatchWithNullToOneUpdate() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PatchWithMissingToOneId() + public async Task PatchWithToOneLinkageObjectMissingId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneIdResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -297,13 +297,13 @@ public async Task PatchWithMissingToOneId() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PatchWithMissingToOneType() + public async Task PatchWithToOneLinkageObjectMissingType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -326,13 +326,13 @@ public async Task PatchWithMissingToOneType() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PatchWithMissingToManyIds() + public async Task PatchWithArrayForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyIdsRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyIdsResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -355,13 +355,188 @@ public async Task PatchWithMissingToManyIds() [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task PatchWithMissingToManyType() + public async Task PatchWithStringForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithMissingToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithToManyLinkageObjectMissingId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithToManyLinkageObjectMissingType() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithObjectForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithObjectForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithStringForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest); + + using (var dbContext = new TestDbContext(effortConnection, false)) + + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithNullForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithNullForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 1124e888..660960a6 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -139,18 +139,18 @@ - - - - + + + + - - - - + + + + @@ -159,9 +159,21 @@ - - + + + + + + + + + + + + + + Designer diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json index 1d3bb1bb..6ba4c6ab 100644 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -5,8 +5,13 @@ "title": "Linkbait!", "links": { "author": { - "type": "authors", - "id": "1" + "linkage": { + "type": "authors", + "id": "1" + } + }, + "comments": { + "linkage": [{ "type": "comments", "id": "400" }, { "type": "comments", "id": "401" }] } } }, @@ -15,8 +20,10 @@ "title": "Rant #1023", "links": { "author": { - "type": "authors", - "id": "1" + "linkage": { + "type": "authors", + "id": "1" + } } } } diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json index f4294e50..97f46dbe 100644 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -4,8 +4,10 @@ "id": "42", "links": { "author": { - "id": "18", - "type": "authors" + "linkage": { + "id": "18", + "type": "authors" + } } } } diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json index 6fb8b6dc..c7f8feff 100644 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json @@ -12,4 +12,4 @@ } } ] -} +} \ No newline at end of file diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index d0e8d1a1..6185e258 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -464,6 +464,9 @@ public void Deserializes_collections_properly() posts[0].Id.Should().Be(p.Id); posts[0].Title.Should().Be(p.Title); posts[0].Author.Id.Should().Be(a.Id); + posts[0].Comments.Count.Should().Be(2); + posts[0].Comments[0].Id.Should().Be(400); + posts[0].Comments[1].Id.Should().Be(401); posts[1].Id.Should().Be(p2.Id); posts[1].Title.Should().Be(p2.Title); posts[1].Author.Id.Should().Be(a.Id); @@ -522,7 +525,7 @@ public void DeserializeExtraPropertyTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""type"": ""posts"",""ids"": []}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""linkage"":[]}}}}")); // Act Author a; @@ -542,7 +545,7 @@ public void DeserializeExtraRelationshipTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""type"": ""posts"",""ids"": []},""bogus"":[""PANIC!""]}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""linkage"":[]},""bogus"":{""linkage"":[]}}}}")); // Act Author a; diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 9b62978c..a9853d52 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -817,253 +817,165 @@ select prop private void DeserializeLinkedResources(object obj, JsonReader reader) { - //reader.Read(); if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("'links' property is not an object!"); Type objectType = obj.GetType(); while (reader.Read()) { - if (reader.TokenType == JsonToken.PropertyName) + if (reader.TokenType == JsonToken.EndObject) { - string value = (string)reader.Value; - reader.Read(); // burn the PropertyName token - var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value) as RelationshipModelProperty; - if (modelProperty != null) - { - var prop = modelProperty.Property; - if (modelProperty.IsToMany) - { - // Is a hasMany - - if (reader.TokenType != JsonToken.StartObject) - throw new BadRequestException("The value of a to-many relationship must be an object."); - - JArray ids = null; - string resourceType = null; - JArray relatedObjects = null; + reader.Read(); // Burn the EndObject token + break; + } - while (reader.Read()) - { - if (reader.TokenType == JsonToken.EndObject) - break; + if (reader.TokenType != JsonToken.PropertyName) + throw new BadRequestException(String.Format("Unexpected token: {0}", reader.TokenType)); - // Not sure what else could even go here, but if it's not a property name, throw an error. - if (reader.TokenType != JsonToken.PropertyName) - throw new BadRequestException("Unexpected token: " + reader.TokenType); + var value = (string)reader.Value; + reader.Read(); // burn the PropertyName token + var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value) as RelationshipModelProperty; + if (modelProperty == null) + { + reader.Skip(); + continue; + } - var propName = (string) reader.Value; - reader.Read(); + var relationshipToken = JToken.ReadFrom(reader); + if (!(relationshipToken is JObject)) + throw new BadRequestException("Each relationship key on a links object must have an object value."); - if (propName == "ids") - { - if (reader.TokenType != JsonToken.StartArray) - throw new BadRequestException("The value of `ids` must be an array."); + var relationshipObject = (JObject) relationshipToken; + var linkageToken = relationshipObject["linkage"]; - ids = JArray.Load(reader); - } - else if (propName == "type") - { - if (reader.TokenType != JsonToken.String) - throw new BadRequestException("Unexpected value for `type`: " + reader.TokenType); + var linkageObjects = new List>(); - resourceType = (string)reader.Value; - } - else if (propName == "data") - { - if (reader.TokenType != JsonToken.StartArray) - throw new BadRequestException("Unexpected value for `data`: " + reader.TokenType); - - relatedObjects = JArray.Load(reader); - } - else - { - throw new BadRequestException("Unexpected property name: " + propName); - } - } + if (modelProperty.IsToMany) + { + if (linkageToken == null) + throw new BadRequestException("Expected an array value for `linkage` but no `linkage` key was found."); - var relatedStubs = new List(); + if (linkageToken.Type != JTokenType.Array) + throw new BadRequestException("Expected an array value for `linkage` but got " + linkageToken.Type + "."); - Type relType; - if (prop.PropertyType.IsGenericType) - { - relType = prop.PropertyType.GetGenericArguments()[0]; - } - else - { - // Must be an array at this point, right?? - relType = prop.PropertyType.GetElementType(); - } - - // According to the spec, either the type and ids or data must be specified - if (relatedObjects != null) - { - if (ids != null) - throw new BadRequestException("If `data` is specified, then `ids` may not be."); + foreach (var element in (JArray) linkageToken) + { + if (!(element is JObject)) + throw new BadRequestException("Each element in the `linkage` array must be an object."); - if (resourceType != null) - throw new BadRequestException("If `data` is specified, then `type` may not be."); + var linkageObject = DeserializeLinkageObject((JObject) element); + linkageObjects.Add(linkageObject); + } + } + else + { + if (linkageToken == null) + throw new BadRequestException("Expected an object or null value for `linkage` but no `linkage` key was found."); - foreach (var relatedObject in relatedObjects) - { - if (!(relatedObject is JObject)) - throw new BadRequestException("Each element in the `data` array must be an object."); + switch (linkageToken.Type) + { + case JTokenType.Null: + break; + case JTokenType.Object: + linkageObjects.Add(DeserializeLinkageObject((JObject)linkageToken)); + break; + default: + throw new BadRequestException("Expected an object value for `linkage` but got " + linkageToken.Type + "."); + } + } - var relatedObjectType = relatedObject["type"] as JValue; - if (relatedObjectType == null || relatedObjectType.Type != JTokenType.String) - throw new BadRequestException("Each element in the `data` array must have a string value for the key `type`."); + var relatedStubs = linkageObjects.Select(lo => + { + var resourceType = _modelManager.GetTypeByResourceTypeName(lo.Item2); + return GetById(resourceType, lo.Item1); + }).ToArray(); - var relatedObjectId = relatedObject["id"] as JValue; - if (relatedObjectId == null || relatedObjectId.Type != JTokenType.String) - throw new BadRequestException("Each element in the `data` array must have a string value for the key `id`."); + var prop = modelProperty.Property; + if (!modelProperty.IsToMany) + { + // To-one relationship - var relatedObjectIdValue = relatedObjectId.Value(); - if (string.IsNullOrWhiteSpace(relatedObjectIdValue)) - throw new BadRequestException("The value for `id` must be specified."); + var relatedStub = relatedStubs.FirstOrDefault(); + prop.SetValue(obj, relatedStub); + } + else + { + // To-many relationship - var stub = GetById(relType, relatedObjectIdValue); - relatedStubs.Add(stub); - } - } - else if (ids == null) - { - throw new BadRequestException("If `data` is not specified, then `ids` must be specified."); - } - else if (resourceType == null) - { - // We aren't doing anything with this value for now, but it needs to be present in the request payload. - // We will need to reference it to properly support polymorphism. - throw new BadRequestException("If `data` is not specified, then `type` must be specified."); - } - else - { - relatedStubs.AddRange(ids.Select(token => GetById(relType, token.ToObject()))); - } + Type relType; + if (prop.PropertyType.IsGenericType) + { + relType = prop.PropertyType.GetGenericArguments()[0]; + } + else + { + // Must be an array at this point, right?? + relType = prop.PropertyType.GetElementType(); + } - IEnumerable hmrel = (IEnumerable)prop.GetValue(obj, null); - if (hmrel == null) + var hmrel = (IEnumerable) prop.GetValue(obj, null); + if (hmrel == null) + { + // Hmm...now we have to create an object that fits this property. This could get messy... + if (!prop.PropertyType.IsInterface && !prop.PropertyType.IsAbstract) + { + // Whew...okay, just instantiate one of these... + hmrel = (IEnumerable) Activator.CreateInstance(prop.PropertyType); + } + else + { + // Ugh...now we're really in trouble...hopefully one of these will work: + if (prop.PropertyType.IsGenericType) { - // Hmm...now we have to create an object that fits this property. This could get messy... - if (!prop.PropertyType.IsInterface && !prop.PropertyType.IsAbstract) + if (prop.PropertyType.IsAssignableFrom(typeof (List<>).MakeGenericType(relType))) { - // Whew...okay, just instantiate one of these... - hmrel = (IEnumerable)Activator.CreateInstance(prop.PropertyType); + hmrel = + (IEnumerable) + Activator.CreateInstance(typeof (List<>).MakeGenericType(relType)); } - else + else if ( + prop.PropertyType.IsAssignableFrom( + typeof (HashSet<>).MakeGenericType(relType))) { - // Ugh...now we're really in trouble...hopefully one of these will work: - if (prop.PropertyType.IsGenericType) - { - if (prop.PropertyType.IsAssignableFrom(typeof(List<>).MakeGenericType(relType))) - { - hmrel = (IEnumerable)Activator.CreateInstance(typeof(List<>).MakeGenericType(relType)); - } - else if (prop.PropertyType.IsAssignableFrom(typeof(HashSet<>).MakeGenericType(relType))) - { - hmrel = (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(relType)); - } - //TODO: Other likely candidates?? - else - { - // punt! - throw new JsonReaderException(String.Format("Could not create empty container for relationship property {0}!", prop)); - } - } - else - { - // erm...Array??!? - hmrel = (IEnumerable)Array.CreateInstance(relType, ids.Count); - } + hmrel = + (IEnumerable) + Activator.CreateInstance( + typeof (HashSet<>).MakeGenericType(relType)); } - } - - // We're having problems with how to generalize/cast/generic-ize this code, so for the time - // being we'll brute-force it in super-dynamic language style... - Type hmtype = hmrel.GetType(); - MethodInfo add = hmtype.GetMethod("Add"); - - foreach (var stub in relatedStubs) - { - add.Invoke(hmrel, new [] { stub }); - } - - prop.SetValue(obj, hmrel); - } - else - { - // Is a belongsTo - - if (reader.TokenType == JsonToken.StartObject) - { - string id = null; - string resourceType = null; - - while (reader.Read()) + //TODO: Other likely candidates?? + else { - if (reader.TokenType == JsonToken.EndObject) - break; - - // Not sure what else could even go here, but if it's not a property name, throw an error. - if (reader.TokenType != JsonToken.PropertyName) - throw new BadRequestException("Unexpected token: " + reader.TokenType); - - var propName = (string)reader.Value; - reader.Read(); - - if (propName == "id") - { - var idValue = reader.Value; - - // The id must be a string. - if (!(idValue is string)) - throw new BadRequestException("The value of the `id` property must be a string."); - - id = (string)idValue; - } - else if (propName == "type") - { - // TODO: we don't do anything with this value yet, but we will need to in order to - // support polymorphic endpoints - resourceType = (string)reader.Value; - } + // punt! + throw new JsonReaderException( + String.Format( + "Could not create empty container for relationship property {0}!", + prop)); } - - // The id must be specified. - if (id == null) - throw new BadRequestException("Nothing was specified for the `id` property."); - - // The type must be specified. - if (resourceType == null) - throw new BadRequestException("Nothing was specified for the `type` property."); - - Type relType = prop.PropertyType; - - prop.SetValue(obj, GetById(relType, id)); - } - else if (reader.TokenType == JsonToken.Null) - { - prop.SetValue(obj, null); } else { - throw new BadRequestException("The value of a to-one relationship must be an object or null."); + // erm...Array??!? + hmrel = + (IEnumerable) Array.CreateInstance(relType, linkageObjects.Count); } } + } - // Tell the MetadataManager that we deserialized this property - MetadataManager.Instance.SetMetaForProperty(obj, prop, true); + // We're having problems with how to generalize/cast/generic-ize this code, so for the time + // being we'll brute-force it in super-dynamic language style... + Type hmtype = hmrel.GetType(); + MethodInfo add = hmtype.GetMethod("Add"); + + foreach (var stub in relatedStubs) + { + add.Invoke(hmrel, new[] {stub}); } - else - reader.Skip(); - } - else if (reader.TokenType == JsonToken.EndObject) - { - // Burn the EndObject token and get set to send back to the parent method in the call stack. - reader.Read(); - break; + + prop.SetValue(obj, hmrel); } - else - reader.Skip(); + + // Tell the MetadataManager that we deserialized this property + MetadataManager.Instance.SetMetaForProperty(obj, prop, true); } } @@ -1072,10 +984,31 @@ private object DeserializeAttribute(Type type, JsonReader reader) if (reader.TokenType == JsonToken.Null) return null; - var token = JToken.Load(reader); + var token = JToken.ReadFrom(reader); return token.ToObject(type); } + private static Tuple DeserializeLinkageObject(JObject token) + { + var relatedObjectType = token["type"] as JValue; + if (relatedObjectType == null || relatedObjectType.Type != JTokenType.String) + throw new BadRequestException("Each linkage object must have a string value for the key `type`."); + + var relatedObjectTypeValue = relatedObjectType.Value(); + if (string.IsNullOrWhiteSpace(relatedObjectTypeValue)) + throw new BadRequestException("The value for `type` must be specified."); + + var relatedObjectId = token["id"] as JValue; + if (relatedObjectId == null || relatedObjectId.Type != JTokenType.String) + throw new BadRequestException("Each linkage object must have a string value for the key `id`."); + + var relatedObjectIdValue = relatedObjectId.Value(); + if (string.IsNullOrWhiteSpace(relatedObjectIdValue)) + throw new BadRequestException("The value for `id` must be specified."); + + return Tuple.Create(relatedObjectIdValue, relatedObjectType.Value()); + } + #endregion private Type GetSingleType(Type type)//dynamic value = null) From 9d7120767ebfed79a1f9a91ebdcda738a1d2cfac Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 16 Apr 2015 01:03:56 -0400 Subject: [PATCH 057/186] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 85178ba4..c0a7cc23 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,10 @@ The NuGet packages are out! * [JSONAPI](https://www.nuget.org/packages/JSONAPI/) * [JSONAPI.EntityFramework](https://www.nuget.org/packages/JSONAPI.EntityFramework/) -Alpha Quality Software +JSON API Compliance ---------------------- -This is an extremely early access library. The tests are a mess, and there is next to no documentation. However, it is working usefully in a project of mine, so hopefully it can be of use to others as well--or at least eventually. This will continue to be developed by me and contributions are welcome! - -(Also bear in mind that I'm still relatively new to C# and .NET--I've written all this but I wouldn't say I "grok" those environments--so there may be flaws in the design. I think there is a lot with merit here though.) +The master branch is currently compliant with JSON API RC1. Work is ongoing in the [0-4-0](https://github.com/SphtKr/JSONAPI.NET/tree/0-4-0) branch to bring the library up to date with the RC3 version of the spec. What is JSONAPI.NET? ==================== From 9fbe305e9b780a3fce12f585a58a5518fb1afb22 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 16 Apr 2015 01:20:48 -0400 Subject: [PATCH 058/186] search parent classes for object sets --- .../EntityFrameworkMaterializer.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 2b6e5be7..07b7fc69 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -184,9 +184,15 @@ private Type GetSingleType(Type type) return type; } + private static Lazy OpenGetKeyNamesFromGenericMethod = + new Lazy( + () => + typeof (EntityFrameworkMaterializer).GetMethod("GetKeyNamesFromGeneric", + BindingFlags.NonPublic | BindingFlags.Static)); + protected internal virtual IEnumerable GetKeyNames(Type type) { - var openMethod = typeof (EntityFrameworkMaterializer).GetMethod("GetKeyNamesFromGeneric", BindingFlags.NonPublic | BindingFlags.Static); + var openMethod = OpenGetKeyNamesFromGenericMethod.Value; var method = openMethod.MakeGenericMethod(type); try { @@ -206,7 +212,21 @@ private static IEnumerable GetKeyNamesFromGeneric(DbContext dbContext try { objectSet = objectContext.CreateObjectSet(); + } + catch (ArgumentException e) + { + var baseClass = typeof (T).BaseType; + if (baseClass != null && baseClass != typeof (Object)) + { + var openMethod = OpenGetKeyNamesFromGenericMethod.Value; + var method = openMethod.MakeGenericMethod(baseClass); + return (IEnumerable)method.Invoke(null, new object[] { dbContext }); + } + throw new ArgumentException( + String.Format("The Type {0} was not found in the DbContext with Type {1}", typeof(T).Name, dbContext.GetType().Name), + e + ); } catch (InvalidOperationException e) { From 7b01416e0b18a8085d6ecc322aba28314ddedf9d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 16 Apr 2015 01:57:37 -0400 Subject: [PATCH 059/186] ensure materialized updates create empty collection property values This was causing null reference exceptions when materializing new objects of types that have to-many relationships. --- .../EntityFrameworkMaterializer.cs | 10 ++- JSONAPI/Extensions/TypeExtensions.cs | 41 ++++++++++++ JSONAPI/Json/JsonApiFormatter.cs | 64 +++---------------- 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 07b7fc69..61d7e168 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -104,6 +104,7 @@ public virtual async Task MaterializeAsync(Type type, object ephemeral) { // Didn't find it...create a new one! retval = Activator.CreateInstance(type); + DbContext.Set(type).Add(retval); if (!anyNull) { @@ -331,6 +332,12 @@ private async Task Merge (Type type, object ephemeral, object material) Type elementType = GetSingleType(prop.PropertyType); var materialMany = (IEnumerable)prop.GetValue(material, null); + if (materialMany == null) + { + materialMany = prop.PropertyType.CreateEnumerableInstance(); + prop.SetValue(material, materialMany); + } + var ephemeralMany = (IEnumerable)prop.GetValue(ephemeral, null); var materialKeys = new HashSet(); @@ -389,7 +396,8 @@ private async Task Merge (Type type, object ephemeral, object material) else { object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null); + var relatedMaterial = await GetByIdAsync(prop.PropertyType, idParams); + prop.SetValue(material, relatedMaterial, null); } } } diff --git a/JSONAPI/Extensions/TypeExtensions.cs b/JSONAPI/Extensions/TypeExtensions.cs index 5646c68e..8df43e56 100644 --- a/JSONAPI/Extensions/TypeExtensions.cs +++ b/JSONAPI/Extensions/TypeExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Newtonsoft.Json; namespace JSONAPI.Extensions { @@ -18,5 +20,44 @@ public static bool CanWriteAsJsonApiAttribute(this Type objectType) || objectType.IsEnum; } + public static IEnumerable CreateEnumerableInstance(this Type type) + { + Type relType; + if (type.IsGenericType) + { + relType = type.GetGenericArguments()[0]; + } + else + { + // Must be an array at this point, right?? + relType = type.GetElementType(); + } + + // Hmm...now we have to create an object that fits this property. This could get messy... + if (!type.IsInterface && !type.IsAbstract) + { + // Whew...okay, just instantiate one of these... + return (IEnumerable)Activator.CreateInstance(type); + } + + // Ugh...now we're really in trouble...hopefully one of these will work: + if (type.IsGenericType) + { + if (type.IsAssignableFrom(typeof(List<>).MakeGenericType(relType))) + { + return (IEnumerable) Activator.CreateInstance(typeof(List<>).MakeGenericType(relType)); + } + + if (type.IsAssignableFrom(typeof(HashSet<>).MakeGenericType(relType))) + { + return + (IEnumerable) Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(relType)); + } + + //TODO: Other likely candidates?? + } + + return null; + } } } diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index a9853d52..edbe189d 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -1,4 +1,5 @@ -using JSONAPI.Attributes; +using System.Collections.ObjectModel; +using JSONAPI.Attributes; using JSONAPI.Core; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -902,63 +903,16 @@ private void DeserializeLinkedResources(object obj, JsonReader reader) { // To-many relationship - Type relType; - if (prop.PropertyType.IsGenericType) - { - relType = prop.PropertyType.GetGenericArguments()[0]; - } - else - { - // Must be an array at this point, right?? - relType = prop.PropertyType.GetElementType(); - } - var hmrel = (IEnumerable) prop.GetValue(obj, null); if (hmrel == null) { - // Hmm...now we have to create an object that fits this property. This could get messy... - if (!prop.PropertyType.IsInterface && !prop.PropertyType.IsAbstract) - { - // Whew...okay, just instantiate one of these... - hmrel = (IEnumerable) Activator.CreateInstance(prop.PropertyType); - } - else - { - // Ugh...now we're really in trouble...hopefully one of these will work: - if (prop.PropertyType.IsGenericType) - { - if (prop.PropertyType.IsAssignableFrom(typeof (List<>).MakeGenericType(relType))) - { - hmrel = - (IEnumerable) - Activator.CreateInstance(typeof (List<>).MakeGenericType(relType)); - } - else if ( - prop.PropertyType.IsAssignableFrom( - typeof (HashSet<>).MakeGenericType(relType))) - { - hmrel = - (IEnumerable) - Activator.CreateInstance( - typeof (HashSet<>).MakeGenericType(relType)); - } - //TODO: Other likely candidates?? - else - { - // punt! - throw new JsonReaderException( - String.Format( - "Could not create empty container for relationship property {0}!", - prop)); - } - } - else - { - // erm...Array??!? - hmrel = - (IEnumerable) Array.CreateInstance(relType, linkageObjects.Count); - } - } + hmrel = prop.PropertyType.CreateEnumerableInstance(); + if (hmrel == null) + // punt! + throw new JsonReaderException( + String.Format( + "Could not create empty container for relationship property {0}!", + prop)); } // We're having problems with how to generalize/cast/generic-ize this code, so for the time From 94608cd4b72b54510314da8241348f985357ecc5 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 29 Apr 2015 18:30:05 -0400 Subject: [PATCH 060/186] add some tests for EF materializer --- .../EntityFrameworkMaterializerTests.cs | 51 +++++++++++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 1 + 2 files changed, 52 insertions(+) create mode 100644 JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs new file mode 100644 index 00000000..def3050a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Core; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.EntityFramework.Tests +{ + [TestClass] + public class EntityFrameworkMaterializerTests + { + [TestMethod] + public async Task Includes_one_to_many_navigation_properties_that_were_serialized() + { + using (var conn = TestHelpers.GetEffortConnection("Acceptance/Data")) + { + using (var context = new TestDbContext(conn, false)) + { + var metadataManager = new Mock(); + var materializer = new EntityFrameworkMaterializer(context, metadataManager.Object); + + var ephemeral = new Post { Id = "201" }; + var material = (Post)await materializer.MaterializeAsync(typeof (Post), ephemeral); + + material.Comments.Should().NotBeNull(); + material.Comments.Count.Should().Be(3); + } + } + } + + [TestMethod] + public async Task Includes_many_to_many_navigation_properties_that_were_serialized() + { + using (var conn = TestHelpers.GetEffortConnection("Acceptance/Data")) + { + using (var context = new TestDbContext(conn, false)) + { + var metadataManager = new Mock(); + var materializer = new EntityFrameworkMaterializer(context, metadataManager.Object); + + var ephemeral = new Post { Id = "201" }; + var material = (Post)await materializer.MaterializeAsync(typeof(Post), ephemeral); + + material.Tags.Should().NotBeNull(); + material.Tags.Count.Should().Be(2); + } + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 660960a6..47c7e337 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -117,6 +117,7 @@ + From 30929b5625220dc16908d46297b2d84b69c649f4 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 6 May 2015 10:18:52 -0400 Subject: [PATCH 061/186] make Merge protected --- JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 61d7e168..4ef85c5f 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -319,7 +319,7 @@ protected EntityKey MaterializeEntityKey(Type type, object obj) return key; } - private async Task Merge (Type type, object ephemeral, object material) + protected async Task Merge (Type type, object ephemeral, object material) { PropertyInfo[] props = type.GetProperties(); foreach (PropertyInfo prop in props) From 003803405a86f81bf46656da737018fbee14ff7b Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 6 May 2015 11:29:04 -0400 Subject: [PATCH 062/186] make metadata manager protected --- JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index 4ef85c5f..c87900a8 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -18,7 +18,7 @@ namespace JSONAPI.EntityFramework /// public partial class EntityFrameworkMaterializer : IMaterializer { - private readonly IMetadataManager _metadataManager; + protected readonly IMetadataManager MetadataManager; /// /// The DbContext instance used to perform materializer operations @@ -31,7 +31,7 @@ public partial class EntityFrameworkMaterializer : IMaterializer /// The DbContext instance used to perform materializer operations public EntityFrameworkMaterializer(DbContext context, IMetadataManager metadataManager) { - _metadataManager = metadataManager; + MetadataManager = metadataManager; DbContext = context; } @@ -325,7 +325,7 @@ protected async Task Merge (Type type, object ephemeral, object material) foreach (PropertyInfo prop in props) { // Comply with the spec, if a key was not set, it should not be updated! - if (!_metadataManager.PropertyWasPresent(ephemeral, prop)) continue; + if (!MetadataManager.PropertyWasPresent(ephemeral, prop)) continue; if (IsMany(prop.PropertyType)) { From b73859e84558d3d140079ac78831fd48ecf78b9d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 6 May 2015 12:37:42 -0400 Subject: [PATCH 063/186] Simplify IMaterializer I removed the obsolete methods as well as the non-generic versions. It was only complicating the implementation of EntityFrameworkMaterializer, and making it harder to override that class. --- .../EntityFrameworkMaterializerTests.cs | 4 +- .../EntityFrameworkMaterializer.cs | 83 +++++-------------- JSONAPI/Core/IMaterializer.cs | 16 +--- JSONAPI/Http/ApiController.cs | 1 + 4 files changed, 27 insertions(+), 77 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs index def3050a..4f959a32 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs @@ -21,7 +21,7 @@ public async Task Includes_one_to_many_navigation_properties_that_were_serialize var materializer = new EntityFrameworkMaterializer(context, metadataManager.Object); var ephemeral = new Post { Id = "201" }; - var material = (Post)await materializer.MaterializeAsync(typeof (Post), ephemeral); + var material = await materializer.MaterializeAsync(ephemeral); material.Comments.Should().NotBeNull(); material.Comments.Count.Should().Be(3); @@ -40,7 +40,7 @@ public async Task Includes_many_to_many_navigation_properties_that_were_serializ var materializer = new EntityFrameworkMaterializer(context, metadataManager.Object); var ephemeral = new Post { Id = "201" }; - var material = (Post)await materializer.MaterializeAsync(typeof(Post), ephemeral); + var material = await materializer.MaterializeAsync(ephemeral); material.Tags.Should().NotBeNull(); material.Tags.Count.Should().Be(2); diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs index c87900a8..5e3727a3 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs @@ -43,14 +43,14 @@ public EntityFrameworkMaterializer(DbContext context, IMetadataManager metadataM /// /// /// - public virtual Task GetByIdAsync(Type type, params Object[] idValues) + public async Task GetByIdAsync(params Object[] idValues) where T : class { //TODO: How to react if the type isn't in the context? // Input will probably usually be strings... make sure the right types are passed to .Find()... Object[] idv2 = new Object[idValues.Length]; int i = 0; - foreach (PropertyInfo prop in GetKeyProperties(type)) + foreach (PropertyInfo prop in GetKeyProperties(typeof(T))) { try { @@ -66,21 +66,12 @@ public virtual Task GetByIdAsync(Type type, params Object[] idValues) i++; } } - return DbContext.Set(type).FindAsync(idv2); + return await DbContext.Set().FindAsync(idv2); } - public async Task GetByIdAsync(params Object[] idValues) - { - return (T) await GetByIdAsync(typeof(T), idValues); - } - - public async Task MaterializeAsync(T ephemeral) - { - return (T) await MaterializeAsync(typeof(T), ephemeral); - } - - public virtual async Task MaterializeAsync(Type type, object ephemeral) + public virtual async Task MaterializeAsync(T ephemeral) where T : class { + var type = typeof (T); IEnumerable keyNames = GetKeyNames(type); List idValues = new List(); bool anyNull = false; @@ -95,15 +86,15 @@ public virtual async Task MaterializeAsync(Type type, object ephemeral) } idValues.Add(value); } - object retval = null; + T retval = null; if (!anyNull) { - retval = await DbContext.Set(type).FindAsync(idValues.ToArray()); + retval = await DbContext.Set().FindAsync(idValues.ToArray()); } if (retval == null) { // Didn't find it...create a new one! - retval = Activator.CreateInstance(type); + retval = (T)Activator.CreateInstance(type); DbContext.Set(type).Add(retval); if (!anyNull) @@ -116,54 +107,15 @@ public virtual async Task MaterializeAsync(Type type, object ephemeral) return retval; } - public async Task MaterializeUpdateAsync(T ephemeral) - { - return (T) await MaterializeUpdateAsync(typeof(T), ephemeral); - } - - public async Task MaterializeUpdateAsync(Type type, object ephemeral) + public async Task MaterializeUpdateAsync(T ephemeral) where T : class { - object material = await MaterializeAsync(type, ephemeral); - await this.Merge(type, ephemeral, material); + var material = await MaterializeAsync(ephemeral); + await Merge(typeof(T), ephemeral, material); return material; } #endregion - #region Obsolete IMaterializer contract methods - - public T GetById(params object[] keyValues) - { - return GetByIdAsync(keyValues).Result; - } - - public object GetById(Type type, params object[] keyValues) - { - return GetByIdAsync(type, keyValues).Result; - } - - public T Materialize(T ephemeral) - { - return MaterializeAsync(ephemeral).Result; - } - - public object Materialize(Type type, object ephemeral) - { - return MaterializeAsync(type, ephemeral).Result; - } - - public T MaterializeUpdate(T ephemeral) - { - return MaterializeUpdateAsync(ephemeral).Result; - } - - public object MaterializeUpdate(Type type, object ephemeral) - { - return MaterializeUpdateAsync(type, ephemeral).Result; - } - - #endregion - private bool IsMany(Type type) { //TODO: Should we check for arrays also? (They aren't generics.) @@ -359,12 +311,15 @@ protected async Task Merge (Type type, object ephemeral, object material) MethodInfo mmadd = mmtype.GetMethod("Add"); MethodInfo mmremove = mmtype.GetMethod("Remove"); + var openGenericGetByIdAsyncMethod = GetType().GetMethod("GetByIdAsync"); + var closedGenericGetByIdAsyncMethod = openGenericGetByIdAsyncMethod.MakeGenericMethod(elementType); + // Add to hasMany if (mmadd != null) foreach (EntityKey key in ephemeralKeys.Except(materialKeys)) { object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - object obj = await GetByIdAsync(elementType, idParams); + var obj = (object)await (dynamic)closedGenericGetByIdAsyncMethod.Invoke(this, new[] { idParams }); mmadd.Invoke(materialMany, new [] { obj }); } // Remove from hasMany @@ -372,7 +327,7 @@ protected async Task Merge (Type type, object ephemeral, object material) foreach (EntityKey key in materialKeys.Except(ephemeralKeys)) { object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - object obj = await GetByIdAsync(elementType, idParams); + var obj = (object)await (dynamic) closedGenericGetByIdAsyncMethod.Invoke(this, new[] {idParams}); mmremove.Invoke(materialMany, new [] { obj }); } } @@ -395,8 +350,12 @@ protected async Task Merge (Type type, object ephemeral, object material) } else { + + var openGenericGetByIdAsyncMethod = GetType().GetMethod("GetByIdAsync"); + var closedGenericGetByIdAsyncMethod = openGenericGetByIdAsyncMethod.MakeGenericMethod(prop.PropertyType); + object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - var relatedMaterial = await GetByIdAsync(prop.PropertyType, idParams); + var relatedMaterial = (object)await (dynamic)closedGenericGetByIdAsyncMethod.Invoke(this, new[] { idParams }); prop.SetValue(material, relatedMaterial, null); } } diff --git a/JSONAPI/Core/IMaterializer.cs b/JSONAPI/Core/IMaterializer.cs index c669c406..8d8fe796 100644 --- a/JSONAPI/Core/IMaterializer.cs +++ b/JSONAPI/Core/IMaterializer.cs @@ -8,18 +8,8 @@ namespace JSONAPI.Core { public interface IMaterializer { - Task GetByIdAsync(params Object[] keyValues); - Task GetByIdAsync(Type type, params Object[] keyValues); - Task MaterializeAsync(T ephemeral); - Task MaterializeAsync(Type type, object ephemeral); - Task MaterializeUpdateAsync(T ephemeral); - Task MaterializeUpdateAsync(Type type, object ephemeral); - - [Obsolete]T GetById(params Object[] keyValues); - [Obsolete]object GetById(Type type, params Object[] keyValues); - [Obsolete]T Materialize(T ephemeral); - [Obsolete]object Materialize(Type type, object ephemeral); - [Obsolete]T MaterializeUpdate(T ephemeral); - [Obsolete]object MaterializeUpdate(Type type, object ephemeral); + Task GetByIdAsync(params Object[] keyValues) where T : class; + Task MaterializeAsync(T ephemeral) where T : class; + Task MaterializeUpdateAsync(T ephemeral) where T : class; } } diff --git a/JSONAPI/Http/ApiController.cs b/JSONAPI/Http/ApiController.cs index 5078536d..54a7b037 100644 --- a/JSONAPI/Http/ApiController.cs +++ b/JSONAPI/Http/ApiController.cs @@ -13,6 +13,7 @@ namespace JSONAPI.Http { //TODO: Authorization checking framework, maybe? public class ApiController : System.Web.Http.ApiController + where T : class { protected virtual IMaterializer MaterializerFactory() { From 06e88a119a4b8373e586dee680235e1430ad6037 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 18 May 2015 22:06:34 -0400 Subject: [PATCH 064/186] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index c0a7cc23..b75c7ca0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The NuGet packages are out! JSON API Compliance ---------------------- -The master branch is currently compliant with JSON API RC1. Work is ongoing in the [0-4-0](https://github.com/SphtKr/JSONAPI.NET/tree/0-4-0) branch to bring the library up to date with the RC3 version of the spec. +The master branch is roughly compatible with the RC3 version of JSON API. The major missing feature is inclusion of related resources. Many changes made to the spec since RC3 are not yet available in this library. Full 1.0 compliance is planned, so stay tuned! What is JSONAPI.NET? ==================== @@ -64,7 +64,3 @@ A `JSONAPI.IMaterializer` object can be added to that `ApiController` to broker # Didn't I read something about using Entity Framework? The classes in the `JSONAPI.EntityFramework` namespace take great advantage of the patterns set out in the `JSONAPI` namespace. The `EntityFrameworkMaterializer` is an `IMaterializer` that can operate with your own `DbContext` class to retrieve objects by Id/Primary Key, and can retrieve and update existing objects from your context in a way that Entity Framework expects for change tracking…that means, in theory, you can use the provided `JSONAPI.EntityFramework.ApiController` base class to handle GET, PUT, POST, and DELETE without writing any additional code! You will still almost certainly subclass `ApiController` to implement your business logic, but that means you only have to worry about your business logic--not implementing the JSON API spec or messing with your persistence layer. - -``` -//TODO: More documentation here. :-) -``` From bf9e4c85b54b2f2e8810fb103de7ba399d74e48e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 9 Jun 2015 18:45:25 -0400 Subject: [PATCH 065/186] allow controller actions to specify top-level metadata --- .../Controllers/PresidentsController.cs | 59 +++++++++++++++++++ .../Controllers/UsersController.cs | 4 +- ...PI.EntityFramework.Tests.TestWebApp.csproj | 1 + .../Responses/GetReturnsIPayloadResponse.json | 29 +++++++++ .../Acceptance/PayloadTests.cs | 21 +++++++ .../JSONAPI.EntityFramework.Tests.csproj | 2 + JSONAPI/IPayload.cs | 21 +++++++ JSONAPI/JSONAPI.csproj | 1 + JSONAPI/Json/JsonApiFormatter.cs | 14 ++++- 9 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs create mode 100644 JSONAPI/IPayload.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs new file mode 100644 index 00000000..3182af4a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class PresidentsController : ApiController + { + public class MyArrayPayload : IPayload + { + private readonly T[] _array; + + public MyArrayPayload(T[] array) + { + _array = array; + } + + public object PrimaryData { get { return _array; } } + + public JObject Metadata + { + get + { + var obj = new JObject(); + obj["count"] = _array.Length; + return obj; + } + } + } + + // This endpoint exists to demonstrate returning IPayload + [Route("presidents")] + public IHttpActionResult GetPresidents() + { + var users = new[] + { + new User + { + Id = "6500", + FirstName = "George", + LastName = "Washington" + }, + new User + { + Id = "6501", + FirstName = "Abraham", + LastName = "Lincoln" + } + }; + + var payload = new MyArrayPayload(users); + return Ok(payload); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs index 8bc13b8e..2bed59d8 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs @@ -1,4 +1,6 @@ -using JSONAPI.Core; +using System.Threading.Tasks; +using System.Web.Http; +using JSONAPI.Core; using JSONAPI.EntityFramework.Http; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index 49f3828a..59a71cdf 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -115,6 +115,7 @@ + diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json new file mode 100644 index 00000000..734c670a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "type": "users", + "id": "6500", + "firstName": "George", + "lastName": "Washington", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + }, + { + "type": "users", + "id": "6501", + "firstName": "Abraham", + "lastName": "Lincoln", + "links": { + "comments": [ ], + "posts": [ ], + "userGroups": [ ] + } + } + ], + "meta": { + "count": 2 + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs new file mode 100644 index 00000000..321fe182 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs @@ -0,0 +1,21 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class PayloadTests : AcceptanceTestsBase + { + [TestMethod] + public async Task Get_returns_IPayload() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "presidents"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Payload\Responses\GetReturnsIPayloadResponse.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 47c7e337..620623ba 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -111,6 +111,7 @@ + @@ -175,6 +176,7 @@ + Designer diff --git a/JSONAPI/IPayload.cs b/JSONAPI/IPayload.cs new file mode 100644 index 00000000..fc9db346 --- /dev/null +++ b/JSONAPI/IPayload.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; + +namespace JSONAPI +{ + /// + /// Actions may return objects that implement this interface in order to + /// add metadata to the response. + /// + public interface IPayload + { + /// + /// The primary data of the response + /// + object PrimaryData { get; } + + /// + /// Metadata to serialize at the top-level of the response + /// + JObject Metadata { get; } + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 48fdaa01..491ecc41 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -94,6 +94,7 @@ + diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index edbe189d..871c3209 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -106,7 +106,7 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w this.RelationAggregators[writeStream] = aggregator; } } - + var contentHeaders = content == null ? null : content.Headers; var effectiveEncoding = SelectCharacterEncoding(contentHeaders); JsonWriter writer = this.CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); @@ -118,6 +118,12 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w } else { + var payload = value as IPayload; + if (payload != null) + { + value = payload.PrimaryData; + } + writer.WriteStartObject(); writer.WritePropertyName(PrimaryDataKeyName); @@ -152,6 +158,12 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w SerializeLinkedResources(writeStream, writer, serializer, aggregator); } + if (payload != null && payload.Metadata != null) + { + writer.WritePropertyName("meta"); + serializer.Serialize(writer, payload.Metadata); + } + writer.WriteEndObject(); } writer.Flush(); From 3a18c0f7d23f3e4cedb3dce5271f303a2cad19d5 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 15:24:16 -0400 Subject: [PATCH 066/186] move IPayload namespace --- .../Controllers/PresidentsController.cs | 1 + JSONAPI/JSONAPI.csproj | 2 +- JSONAPI/Json/JsonApiFormatter.cs | 1 + JSONAPI/{ => Payload}/IPayload.cs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) rename JSONAPI/{ => Payload}/IPayload.cs (95%) diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs index 3182af4a..483d2cb1 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs @@ -4,6 +4,7 @@ using System.Web; using System.Web.Http; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Payload; using Newtonsoft.Json.Linq; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 491ecc41..ad721c33 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -94,7 +94,7 @@ - + diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 871c3209..739a9bef 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -18,6 +18,7 @@ using System.Threading.Tasks; using System.Web.Http; using JSONAPI.Extensions; +using JSONAPI.Payload; namespace JSONAPI.Json { diff --git a/JSONAPI/IPayload.cs b/JSONAPI/Payload/IPayload.cs similarity index 95% rename from JSONAPI/IPayload.cs rename to JSONAPI/Payload/IPayload.cs index fc9db346..ec4a6328 100644 --- a/JSONAPI/IPayload.cs +++ b/JSONAPI/Payload/IPayload.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace JSONAPI +namespace JSONAPI.Payload { /// /// Actions may return objects that implement this interface in order to From e9e22dd9696bce758f8396319dc671e3bfc98919 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 15:44:52 -0400 Subject: [PATCH 067/186] allow pluggable queryable payload builders --- .../JsonApiQueryableAttribute.cs | 48 +++------------ JSONAPI/Core/JsonApiConfiguration.cs | 4 +- JSONAPI/JSONAPI.csproj | 3 + .../Payload/DefaultQueryablePayloadBuilder.cs | 58 +++++++++++++++++++ JSONAPI/Payload/IQueryablePayloadBuilder.cs | 23 ++++++++ JSONAPI/Payload/Payload.cs | 13 +++++ 6 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs create mode 100644 JSONAPI/Payload/IQueryablePayloadBuilder.cs create mode 100644 JSONAPI/Payload/Payload.cs diff --git a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs index 2cd8b7bc..d798eac9 100644 --- a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs +++ b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Filters; +using JSONAPI.Payload; namespace JSONAPI.ActionFilters { @@ -16,33 +17,19 @@ namespace JSONAPI.ActionFilters /// public class JsonApiQueryableAttribute : ActionFilterAttribute { - private readonly IQueryableEnumerationTransformer _enumerationTransformer; - private readonly IQueryableFilteringTransformer _filteringTransformer; - private readonly IQueryableSortingTransformer _sortingTransformer; - private readonly IQueryablePaginationTransformer _paginationTransformer; + private readonly IQueryablePayloadBuilder _payloadBuilder; + private readonly Lazy _openBuildPayloadMethod; /// /// Creates a new JsonApiQueryableAttribute. /// - /// The transform to be used for enumerating IQueryable payloads. - /// The transform to be used for filtering IQueryable payloads - /// The transform to be used for sorting IQueryable payloads. - /// The transform to be used for pagination of IQueryable payloads. - public JsonApiQueryableAttribute( - IQueryableEnumerationTransformer enumerationTransformer, - IQueryableFilteringTransformer filteringTransformer = null, - IQueryableSortingTransformer sortingTransformer = null, - IQueryablePaginationTransformer paginationTransformer = null) + public JsonApiQueryableAttribute(IQueryablePayloadBuilder payloadBuilder) { - _sortingTransformer = sortingTransformer; - _paginationTransformer = paginationTransformer; - _enumerationTransformer = enumerationTransformer; - _filteringTransformer = filteringTransformer; + _payloadBuilder = payloadBuilder; + _openBuildPayloadMethod = + new Lazy(() => _payloadBuilder.GetType().GetMethod("BuildPayload", BindingFlags.Instance | BindingFlags.Public)); } - private readonly Lazy _openApplyTransformsMethod = - new Lazy(() => typeof(JsonApiQueryableAttribute).GetMethod("ApplyTransforms", BindingFlags.NonPublic | BindingFlags.Instance)); - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) { if (actionExecutedContext.Response != null) @@ -54,14 +41,14 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) { var queryableElementType = objectType.GenericTypeArguments[0]; - var applyTransformsMethod = _openApplyTransformsMethod.Value.MakeGenericMethod(queryableElementType); + var buildPayloadMethod = _openBuildPayloadMethod.Value.MakeGenericMethod(queryableElementType); try { dynamic materializedQueryTask; try { - materializedQueryTask = applyTransformsMethod.Invoke(this, + materializedQueryTask = buildPayloadMethod.Invoke(_payloadBuilder, new[] {objectContent.Value, actionExecutedContext.Request, cancellationToken}); } catch (TargetInvocationException ex) @@ -89,27 +76,10 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio { throw new HttpResponseException( actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message)); - } } } } } - - // ReSharper disable once UnusedMember.Local - private async Task ApplyTransforms(IQueryable query, HttpRequestMessage request, - CancellationToken cancellationToken) - { - if (_filteringTransformer != null) - query = _filteringTransformer.Filter(query, request); - - if (_sortingTransformer != null) - query = _sortingTransformer.Sort(query, request); - - if (_paginationTransformer != null) - query = _paginationTransformer.ApplyPagination(query, request); - - return await _enumerationTransformer.Enumerate(query, cancellationToken); - } } } diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index 85c667e7..c6c2b43a 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -5,6 +5,7 @@ using JSONAPI.ActionFilters; using JSONAPI.Http; using JSONAPI.Json; +using JSONAPI.Payload; namespace JSONAPI.Core { @@ -140,7 +141,8 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(formatter); - httpConfig.Filters.Add(new JsonApiQueryableAttribute(enumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer)); + var queryablePayloadBuilder = new DefaultQueryablePayloadBuilder(enumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer); + httpConfig.Filters.Add(new JsonApiQueryableAttribute(queryablePayloadBuilder)); httpConfig.Services.Replace(typeof (IHttpControllerSelector), new PascalizedControllerSelector(httpConfig)); diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index ad721c33..60b1c4df 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -94,6 +94,7 @@ + @@ -102,6 +103,8 @@ + + diff --git a/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs b/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs new file mode 100644 index 00000000..7c3dd143 --- /dev/null +++ b/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.ActionFilters; + +namespace JSONAPI.Payload +{ + /// + /// Provides a default implementation of an IQueryablePayloadBuilder + /// + public class DefaultQueryablePayloadBuilder : IQueryablePayloadBuilder + { + private readonly IQueryableEnumerationTransformer _enumerationTransformer; + private readonly IQueryableFilteringTransformer _filteringTransformer; + private readonly IQueryableSortingTransformer _sortingTransformer; + private readonly IQueryablePaginationTransformer _paginationTransformer; + + /// + /// + /// + /// + /// + /// + /// + public DefaultQueryablePayloadBuilder( + IQueryableEnumerationTransformer enumerationTransformer, + IQueryableFilteringTransformer filteringTransformer = null, + IQueryableSortingTransformer sortingTransformer = null, + IQueryablePaginationTransformer paginationTransformer = null) + { + _enumerationTransformer = enumerationTransformer; + _filteringTransformer = filteringTransformer; + _sortingTransformer = sortingTransformer; + _paginationTransformer = paginationTransformer; + } + + public async Task BuildPayload(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_filteringTransformer != null) + query = _filteringTransformer.Filter(query, request); + + if (_sortingTransformer != null) + query = _sortingTransformer.Sort(query, request); + + if (_paginationTransformer != null) + query = _paginationTransformer.ApplyPagination(query, request); + + var results = await _enumerationTransformer.Enumerate(query, cancellationToken); + + return new Payload + { + PrimaryData = results + }; + } + } +} diff --git a/JSONAPI/Payload/IQueryablePayloadBuilder.cs b/JSONAPI/Payload/IQueryablePayloadBuilder.cs new file mode 100644 index 00000000..28d22884 --- /dev/null +++ b/JSONAPI/Payload/IQueryablePayloadBuilder.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace JSONAPI.Payload +{ + /// + /// This interface is responsible for building IPayload objects based on IQueryable ObjectContent + /// + public interface IQueryablePayloadBuilder + { + /// + /// Builds a payload object for the given query + /// + /// + /// + /// + /// + /// + Task BuildPayload(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken); + } +} diff --git a/JSONAPI/Payload/Payload.cs b/JSONAPI/Payload/Payload.cs new file mode 100644 index 00000000..ee98e043 --- /dev/null +++ b/JSONAPI/Payload/Payload.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Payload +{ + /// + /// Default implementation of IPayload + /// + public class Payload : IPayload + { + public object PrimaryData { get; set; } + public JObject Metadata { get; set; } + } +} From 44c83aed70fffffae0e85192a14e23fe0bb03a6f Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 15:58:28 -0400 Subject: [PATCH 068/186] Move payload builder configuration into separate object --- .../Startup.cs | 2 +- ...lePayloadBuilderConfigurationExtensions.cs | 23 ++++ .../JSONAPI.EntityFramework.csproj | 2 +- .../JsonApiConfigurationExtensions.cs | 23 ---- JSONAPI.TodoMVC.API/Startup.cs | 2 +- ...ultQueryablePayloadBuilderConfiguration.cs | 121 ++++++++++++++++++ JSONAPI/Core/JsonApiConfiguration.cs | 110 ++-------------- JSONAPI/JSONAPI.csproj | 1 + 8 files changed, 160 insertions(+), 124 deletions(-) create mode 100644 JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs delete mode 100644 JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs create mode 100644 JSONAPI/Core/DefaultQueryablePayloadBuilderConfiguration.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index aea371e0..e6e7f8e3 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -69,7 +69,7 @@ private static HttpConfiguration GetWebApiConfiguration() // Configure JSON API new JsonApiConfiguration(modelManager) - .UseEntityFramework() + .UsingDefaultQueryablePayloadBuilder(c => c.EnumerateQueriesAsynchronously()) .Apply(httpConfig); diff --git a/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs b/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs new file mode 100644 index 00000000..eba0476e --- /dev/null +++ b/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs @@ -0,0 +1,23 @@ +using JSONAPI.Core; +using JSONAPI.EntityFramework.ActionFilters; + +namespace JSONAPI.EntityFramework +{ + /// + /// Extension Methods for JSONAPI.Core.DefaultQueryablePayloadBuilderConfiguration + /// + public static class DefaultQueryablePayloadBuilderConfigurationExtensions + { + /// + /// Add Entity Framework specific handling to the configuration + /// + /// The configuration object to modify + /// The same configuration object that was passed in + public static DefaultQueryablePayloadBuilderConfiguration EnumerateQueriesAsynchronously(this DefaultQueryablePayloadBuilderConfiguration config) + { + config.EnumerateQueriesWith(new AsynchronousEnumerationTransformer()); + + return config; + } + } +} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 6633a64f..4dbd21ab 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -75,7 +75,7 @@ - + diff --git a/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs b/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs deleted file mode 100644 index 08906ba6..00000000 --- a/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; - -namespace JSONAPI.EntityFramework -{ - /// - /// Extension Methods for JSONAPI.JsonApiConfiguration - /// - public static class JsonApiConfigurationExtensions - { - /// - /// Add Entity Framework specific handling to the configuration - /// - /// The configuration object to modify - /// The same configuration object that was passed in - public static JsonApiConfiguration UseEntityFramework(this JsonApiConfiguration jsonApiConfig) - { - jsonApiConfig.EnumerateQueriesWith(new AsynchronousEnumerationTransformer()); - - return jsonApiConfig; - } - } -} diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index cba4dff1..9673492f 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -26,7 +26,7 @@ private static HttpConfiguration GetWebApiConfiguration() // Configure JSON API new JsonApiConfiguration(modelManager) - .UseEntityFramework() + .UsingDefaultQueryablePayloadBuilder(c => c.EnumerateQueriesAsynchronously()) .Apply(httpConfig); // Web API routes diff --git a/JSONAPI/Core/DefaultQueryablePayloadBuilderConfiguration.cs b/JSONAPI/Core/DefaultQueryablePayloadBuilderConfiguration.cs new file mode 100644 index 00000000..8083153f --- /dev/null +++ b/JSONAPI/Core/DefaultQueryablePayloadBuilderConfiguration.cs @@ -0,0 +1,121 @@ +using JSONAPI.ActionFilters; +using JSONAPI.Payload; + +namespace JSONAPI.Core +{ + /// + /// Provides a fluent configuration system for DefaultQueryablePayloadBuilder + /// + public sealed class DefaultQueryablePayloadBuilderConfiguration + { + private bool _enableFiltering; + private bool _enableSorting; + private bool _enablePagination; + private IQueryableFilteringTransformer _filteringTransformer; + private IQueryableSortingTransformer _sortingTransformer; + private IQueryablePaginationTransformer _paginationTransformer; + private IQueryableEnumerationTransformer _enumerationTransformer; + + internal DefaultQueryablePayloadBuilderConfiguration() + { + _enableFiltering = true; + _enableSorting = true; + _enablePagination = true; + } + + /// + /// Disables filtering of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration DisableFiltering() + { + _enableFiltering = false; + return this; + } + + /// + /// Disables sorting of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration DisableSorting() + { + _enableSorting = false; + return this; + } + + /// + /// Disables pagination of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration DisablePagination() + { + _enablePagination = false; + return this; + } + + /// + /// Specifies a filtering transformer to use for filtering IQueryable response payloads. + /// + /// The filtering transformer. + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration FilterWith(IQueryableFilteringTransformer filteringTransformer) + { + _filteringTransformer = filteringTransformer; + return this; + } + + /// + /// Specifies a sorting transformer to use for sorting IQueryable response payloads. + /// + /// The sorting transformer. + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration SortWith(IQueryableSortingTransformer sortingTransformer) + { + _sortingTransformer = sortingTransformer; + return this; + } + + /// + /// Specifies a pagination transformer to use for paging IQueryable response payloads. + /// + /// The pagination transformer. + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration PageWith(IQueryablePaginationTransformer paginationTransformer) + { + _paginationTransformer = paginationTransformer; + return this; + } + + /// + /// Specifies an enumeration transformer to use for enumerating IQueryable response payloads. + /// + /// The enumeration transformer. + /// The same configuration object the method was called on. + public DefaultQueryablePayloadBuilderConfiguration EnumerateQueriesWith(IQueryableEnumerationTransformer enumerationTransformer) + { + _enumerationTransformer = enumerationTransformer; + return this; + } + + internal DefaultQueryablePayloadBuilder GetBuilder(IModelManager modelManager) + { + IQueryableFilteringTransformer filteringTransformer = null; + if (_enableFiltering) + filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(modelManager); + + IQueryableSortingTransformer sortingTransformer = null; + if (_enableSorting) + sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(modelManager); + + IQueryablePaginationTransformer paginationTransformer = null; + if (_enablePagination) + paginationTransformer = + _paginationTransformer ?? new DefaultPaginationTransformer("page.number", "page.size"); + + IQueryableEnumerationTransformer enumerationTransformer = + _enumerationTransformer ?? new SynchronousEnumerationTransformer(); + + return new DefaultQueryablePayloadBuilder(enumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer); + } + } +} diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index c6c2b43a..be753a0f 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -14,14 +14,8 @@ namespace JSONAPI.Core /// public class JsonApiConfiguration { - private bool _enableFiltering; - private bool _enableSorting; - private bool _enablePagination; - private IQueryableFilteringTransformer _filteringTransformer; - private IQueryableSortingTransformer _sortingTransformer; - private IQueryablePaginationTransformer _paginationTransformer; - private IQueryableEnumerationTransformer _enumerationTransformer; private readonly IModelManager _modelManager; + private Func _payloadBuilderFactory; /// /// Creates a new configuration @@ -30,87 +24,23 @@ public JsonApiConfiguration(IModelManager modelManager) { if (modelManager == null) throw new Exception("You must provide "); - _enableFiltering = true; - _enableSorting = true; - _enablePagination = true; - _filteringTransformer = null; - _sortingTransformer = null; - _paginationTransformer = null; - _enumerationTransformer = null; _modelManager = modelManager; + _payloadBuilderFactory = () => new DefaultQueryablePayloadBuilderConfiguration().GetBuilder(modelManager); } /// - /// Disables filtering of IQueryables in GET methods + /// Allows configuring the default queryable payload builder /// + /// Provides access to a fluent DefaultQueryablePayloadBuilderConfiguration object /// The same configuration object the method was called on. - public JsonApiConfiguration DisableFiltering() + public JsonApiConfiguration UsingDefaultQueryablePayloadBuilder(Action configurationAction) { - _enableFiltering = false; - return this; - } - - /// - /// Disables sorting of IQueryables in GET methods - /// - /// The same configuration object the method was called on. - public JsonApiConfiguration DisableSorting() - { - _enableSorting = false; - return this; - } - - /// - /// Disables pagination of IQueryables in GET methods - /// - /// The same configuration object the method was called on. - public JsonApiConfiguration DisablePagination() - { - _enablePagination = false; - return this; - } - - /// - /// Specifies a filtering transformer to use for filtering IQueryable response payloads. - /// - /// The filtering transformer. - /// The same configuration object the method was called on. - public JsonApiConfiguration FilterWith(IQueryableFilteringTransformer filteringTransformer) - { - _filteringTransformer = filteringTransformer; - return this; - } - - /// - /// Specifies a sorting transformer to use for sorting IQueryable response payloads. - /// - /// The sorting transformer. - /// The same configuration object the method was called on. - public JsonApiConfiguration SortWith(IQueryableSortingTransformer sortingTransformer) - { - _sortingTransformer = sortingTransformer; - return this; - } - - /// - /// Specifies a pagination transformer to use for paging IQueryable response payloads. - /// - /// The pagination transformer. - /// The same configuration object the method was called on. - public JsonApiConfiguration PageWith(IQueryablePaginationTransformer paginationTransformer) - { - _paginationTransformer = paginationTransformer; - return this; - } - - /// - /// Specifies an enumeration transformer to use for enumerating IQueryable response payloads. - /// - /// The enumeration transformer. - /// The same configuration object the method was called on. - public JsonApiConfiguration EnumerateQueriesWith(IQueryableEnumerationTransformer enumerationTransformer) - { - _enumerationTransformer = enumerationTransformer; + _payloadBuilderFactory = () => + { + var configuration = new DefaultQueryablePayloadBuilderConfiguration(); + configurationAction(configuration); + return configuration.GetBuilder(_modelManager); + }; return this; } @@ -120,28 +50,12 @@ public JsonApiConfiguration EnumerateQueriesWith(IQueryableEnumerationTransforme /// The HttpConfiguration to apply this JsonApiConfiguration to public void Apply(HttpConfiguration httpConfig) { - IQueryableFilteringTransformer filteringTransformer = null; - if (_enableFiltering) - filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(_modelManager); - - IQueryableSortingTransformer sortingTransformer = null; - if (_enableSorting) - sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(_modelManager); - - IQueryablePaginationTransformer paginationTransformer = null; - if (_enablePagination) - paginationTransformer = - _paginationTransformer ?? new DefaultPaginationTransformer("page.number", "page.size", null); - - IQueryableEnumerationTransformer enumerationTransformer = - _enumerationTransformer ?? new SynchronousEnumerationTransformer(); - var formatter = new JsonApiFormatter(_modelManager); httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(formatter); - var queryablePayloadBuilder = new DefaultQueryablePayloadBuilder(enumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer); + var queryablePayloadBuilder = _payloadBuilderFactory(); httpConfig.Filters.Add(new JsonApiQueryableAttribute(queryablePayloadBuilder)); httpConfig.Services.Replace(typeof (IHttpControllerSelector), diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 60b1c4df..9b8baa16 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -81,6 +81,7 @@ + From ef13ae484ed88fb30d9904bd61314fcecb832d56 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 16:02:22 -0400 Subject: [PATCH 069/186] fix fragmented error message --- JSONAPI/Core/JsonApiConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index be753a0f..35e2067c 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -22,7 +22,7 @@ public class JsonApiConfiguration /// public JsonApiConfiguration(IModelManager modelManager) { - if (modelManager == null) throw new Exception("You must provide "); + if (modelManager == null) throw new Exception("You must provide a model manager to begin configuration."); _modelManager = modelManager; _payloadBuilderFactory = () => new DefaultQueryablePayloadBuilderConfiguration().GetBuilder(modelManager); From 080cc8834e95f3d45dd4f8d044d8c8d041f3eb76 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 16:02:46 -0400 Subject: [PATCH 070/186] allow providing a custom payload builder --- JSONAPI/Core/JsonApiConfiguration.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index 35e2067c..ed1d1cc5 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -44,6 +44,17 @@ public JsonApiConfiguration UsingDefaultQueryablePayloadBuilder(Action + /// Allows overriding the default queryable payload builder + /// + /// The custom queryable payload builder to use + /// + public JsonApiConfiguration UseCustomQueryablePayloadBuilder(IQueryablePayloadBuilder queryablePayloadBuilder) + { + _payloadBuilderFactory = () => queryablePayloadBuilder; + return this; + } + /// /// Applies the running configuration to an HttpConfiguration instance /// From 70e220a5eba6f02c6ff841beb9e394dce9beb762 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 11 Jun 2015 16:50:15 -0400 Subject: [PATCH 071/186] change pagination transformer to return rich object This change allows the pagination transformer to communicate information about the pagination operation that was performed, in addition to the paged query itself. This paves the way for including page links at the top level. --- .../DefaultPaginationTransformerTests.cs | 2 +- .../DefaultPaginationTransformResult.cs | 16 ++++++++++ .../DefaultPaginationTransformer.cs | 20 +++++++++--- .../IQueryablePaginationTransformer.cs | 31 +++++++++++++++++-- JSONAPI/JSONAPI.csproj | 1 + .../Payload/DefaultQueryablePayloadBuilder.cs | 5 ++- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs index 8ab3508c..9cb9fc77 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -54,7 +54,7 @@ private DefaultPaginationTransformer GetTransformer(int maxPageSize) private Dummy[] GetArray(string uri, int maxPageSize = 50) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - return GetTransformer(maxPageSize).ApplyPagination(_fixturesQuery, request).ToArray(); + return GetTransformer(maxPageSize).ApplyPagination(_fixturesQuery, request).PagedQuery.ToArray(); } [TestMethod] diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs b/JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs new file mode 100644 index 00000000..253854cd --- /dev/null +++ b/JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs @@ -0,0 +1,16 @@ +using System.Linq; + +namespace JSONAPI.ActionFilters +{ + /// + /// Default implementation of IPaginationTransformResult`1 + /// + /// + public class DefaultPaginationTransformResult : IPaginationTransformResult + { + public IQueryable PagedQuery { get; set; } + public bool PaginationWasApplied { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + } +} diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs index 9f7e2495..78bededc 100644 --- a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs @@ -7,7 +7,7 @@ namespace JSONAPI.ActionFilters { /// - /// Performs pagination a + /// Performs pagination /// public class DefaultPaginationTransformer : IQueryablePaginationTransformer { @@ -32,7 +32,7 @@ public DefaultPaginationTransformer(string pageNumberQueryParam, string pageSize _maxPageSize = maxPageSize; } - public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage request) + public IPaginationTransformResult ApplyPagination(IQueryable query, HttpRequestMessage request) { var hasPageNumberParam = false; var hasPageSizeParam = false; @@ -58,7 +58,13 @@ public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage } if (!hasPageNumberParam && !hasPageSizeParam) - return query; + { + return new DefaultPaginationTransformResult + { + PagedQuery = query, + PaginationWasApplied = false + }; + } if ((hasPageNumberParam && !hasPageSizeParam) || (!hasPageNumberParam && hasPageSizeParam)) throw new QueryableTransformException( @@ -77,7 +83,13 @@ public IQueryable ApplyPagination(IQueryable query, HttpRequestMessage pageSize = _maxPageSize.Value; var skip = pageNumber * pageSize; - return query.Skip(skip).Take(pageSize); + return new DefaultPaginationTransformResult + { + PageNumber = pageNumber, + PageSize = pageSize, + PagedQuery = query.Skip(skip).Take(pageSize), + PaginationWasApplied = true + }; } } } diff --git a/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs b/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs index 510374bc..cabe302a 100644 --- a/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs +++ b/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs @@ -14,7 +14,34 @@ public interface IQueryablePaginationTransformer /// The query to page /// The request message /// The queryable element type - /// An IQueryable configured to return a page of data - IQueryable ApplyPagination(IQueryable query, HttpRequestMessage request); + /// The result of pagination + IPaginationTransformResult ApplyPagination(IQueryable query, HttpRequestMessage request); + } + + /// + /// The result of a pagination transform + /// + /// + public interface IPaginationTransformResult + { + /// + /// A query that has been paginated + /// + IQueryable PagedQuery { get; } + + /// + /// Whether the query has been paginated or not + /// + bool PaginationWasApplied { get; } + + /// + /// The current page of the query + /// + int PageNumber { get; } + + /// + /// The size of this page of data + /// + int PageSize { get; } } } diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 9b8baa16..1464e7f0 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -68,6 +68,7 @@ + diff --git a/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs b/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs index 7c3dd143..07d0741f 100644 --- a/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs +++ b/JSONAPI/Payload/DefaultQueryablePayloadBuilder.cs @@ -45,7 +45,10 @@ public async Task BuildPayload(IQueryable query, HttpRequestMess query = _sortingTransformer.Sort(query, request); if (_paginationTransformer != null) - query = _paginationTransformer.ApplyPagination(query, request); + { + var paginationResults = _paginationTransformer.ApplyPagination(query, request); + query = paginationResults.PagedQuery; + } var results = await _enumerationTransformer.Enumerate(query, cancellationToken); From 774f982f46a982398047adefcb937ed4ceab4c0e Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Wed, 10 Jun 2015 17:20:16 +0200 Subject: [PATCH 072/186] Update json format as defined in JSON API v1 - rename links to relationships - add a data object or array to the relationships --- JSONAPI/Json/JsonApiFormatter.cs | 196 ++++++++++++++++++------------- 1 file changed, 115 insertions(+), 81 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 739a9bef..ad8eeb51 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -198,91 +198,32 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js writer.WriteStartObject(); var resourceType = value.GetType(); - - // Write the type - writer.WritePropertyName("type"); var jsonTypeKey = _modelManager.GetResourceTypeNameForType(resourceType); - writer.WriteValue(jsonTypeKey); - - // Do the Id now... - writer.WritePropertyName("id"); var idProp = _modelManager.GetIdProperty(resourceType); - writer.WriteValue(GetValueForIdProperty(idProp, value)); + + // Write the type and id + WriteTypeAndId(writer, resourceType, value); // Leverage the cached map to avoid another costly call to System.Type.GetProperties() var props = _modelManager.GetProperties(value.GetType()); - // Do non-model properties first, everything else goes in "links" + // Do non-model properties first, everything else goes in "related" //TODO: Unless embedded??? var relationshipModelProperties = new List(); - foreach (var modelProperty in props) - { - var prop = modelProperty.Property; - if (prop == idProp) continue; // Don't write the "id" property twice, see above! - - if (modelProperty is FieldModelProperty) - { - if (modelProperty.IgnoreByDefault) continue; // TODO: allow overriding this - - // numbers, strings, dates... - writer.WritePropertyName(modelProperty.JsonKey); - - var propertyValue = prop.GetValue(value, null); - - if (prop.PropertyType == typeof (Decimal) || prop.PropertyType == typeof (Decimal?)) - { - if (propertyValue == null) - writer.WriteNull(); - else - writer.WriteValue(propertyValue.ToString()); - } - else if (prop.PropertyType == typeof (string) && - prop.GetCustomAttributes().Any(attr => attr is SerializeStringAsRawJsonAttribute)) - { - if (propertyValue == null) - { - writer.WriteNull(); - } - else - { - var json = (string) propertyValue; - if (ValidateRawJsonStrings) - { - try - { - var token = JToken.Parse(json); - json = token.ToString(); - } - catch (Exception) - { - json = "{}"; - } - } - var valueToSerialize = JsonHelpers.MinifyJson(json); - writer.WriteRawValue(valueToSerialize); - } - } - else - { - serializer.Serialize(writer, propertyValue); - } - } - else if (modelProperty is RelationshipModelProperty) - { - relationshipModelProperties.Add((RelationshipModelProperty)modelProperty); - } - } + // Write attributes + WriteAttributes(props, writer, idProp, value, serializer, relationshipModelProperties); // Now do other stuff if (relationshipModelProperties.Count() > 0) { - writer.WritePropertyName("links"); + writer.WritePropertyName("relastionships"); writer.WriteStartObject(); } foreach (var relationshipModelProperty in relationshipModelProperties) { - bool skip = false, iip = false; + bool skip = false, + iip = false; string lt = null; SerializeAsOptions sa = SerializeAsOptions.Ids; @@ -324,11 +265,12 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js switch (sa) { case SerializeAsOptions.Ids: - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); IEnumerable items = (IEnumerable)prop.GetValue(value, null); if (items == null) { - writer.WriteValue((IEnumerable)null); //TODO: Is it okay with the spec and Ember Data to return null for an empty array? + // Return an empty array when there are no items + writer.WriteStartArray(); + writer.WriteEndArray(); break; // LOOK OUT! Ending this case block early here!!! } this.WriteIdsArrayJson(writer, items, serializer); @@ -354,7 +296,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); - writer.WritePropertyName("related"); + writer.WritePropertyName("links"); writer.WriteValue(href); writer.WriteEndObject(); break; @@ -383,8 +325,15 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js switch (sa) { case SerializeAsOptions.Ids: - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); - serializer.Serialize(writer, objId.Value); + // Write the data element + writer.WriteStartObject(); + writer.WritePropertyName(PrimaryDataKeyName); + writer.WriteStartObject(); + // Write the type and id + WriteTypeAndId(writer, prop.PropertyType, propertyValue); + + writer.WriteEndObject(); + writer.WriteEndObject(); if (iip) if (aggregator != null) aggregator.Add(prop.PropertyType, propertyValue); @@ -398,7 +347,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); writer.WriteStartObject(); - writer.WritePropertyName("related"); + writer.WritePropertyName("links"); writer.WriteValue(link); writer.WriteEndObject(); break; @@ -492,7 +441,7 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J if (aggregator.Appendices.Count > 0) { - writer.WritePropertyName("linked"); + writer.WritePropertyName("included"); writer.WriteStartObject(); // Okay, we should have captured everything now. Now combine the type writers into the main writer... @@ -509,6 +458,76 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J } + private void WriteAttributes(ModelProperty[] props, JsonWriter writer, PropertyInfo idProp, object value, JsonSerializer serializer, List relationshipModelProperties) + { + //if (props.Count() > 0) + //{ + // writer.WritePropertyName("attributes"); + // writer.WriteStartObject(); + //} + + foreach (var modelProperty in props) + { + var prop = modelProperty.Property; + if (prop == idProp) continue; // Don't write the "id" property twice, see above! + + if (modelProperty is FieldModelProperty) + { + if (modelProperty.IgnoreByDefault) continue; // TODO: allow overriding this + + // numbers, strings, dates... + writer.WritePropertyName(modelProperty.JsonKey); + + var propertyValue = prop.GetValue(value, null); + + if (prop.PropertyType == typeof(Decimal) || prop.PropertyType == typeof(Decimal?)) + { + if (propertyValue == null) + writer.WriteNull(); + else + writer.WriteValue(propertyValue); + } + else if (prop.PropertyType == typeof(string) && + prop.GetCustomAttributes().Any(attr => attr is SerializeStringAsRawJsonAttribute)) + { + if (propertyValue == null) + { + writer.WriteNull(); + } + else + { + var json = (string)propertyValue; + if (ValidateRawJsonStrings) + { + try + { + var token = JToken.Parse(json); + json = token.ToString(); + } + catch (Exception) + { + json = "{}"; + } + } + var valueToSerialize = JsonHelpers.MinifyJson(json); + writer.WriteRawValue(valueToSerialize); + } + } + else + { + serializer.Serialize(writer, propertyValue); + } + } + else if (modelProperty is RelationshipModelProperty) + { + relationshipModelProperties.Add((RelationshipModelProperty)modelProperty); + } + } + + //if (props.Count() > 0) + // writer.WriteEndObject(); + } + #endregion Serialization #region Deserialization @@ -558,11 +577,11 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, reader.Read(); // burn the PropertyName token switch (value) { - case "linked": + case "included": //TODO: If we want to capture linked/related objects in a compound document when deserializing, do it here...do we? reader.Skip(); break; - case "links": + case "relationships": // ignore this, is it even meaningful in a PUT/POST body? reader.Skip(); break; @@ -712,7 +731,7 @@ private object Deserialize(Type objectType, JsonReader reader) string value = (string)reader.Value; var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value); - if (value == "links") + if (value == "related") { reader.Read(); // burn the PropertyName token //TODO: linked resources (Done??) @@ -724,7 +743,7 @@ private object Deserialize(Type objectType, JsonReader reader) { reader.Read(); // burn the PropertyName token //TODO: Embedded would be dropped here! - continue; // These aren't supposed to be here, they're supposed to be in "links"! + continue; // These aren't supposed to be here, they're supposed to be in "related"! } var prop = modelProperty.Property; @@ -1008,16 +1027,31 @@ protected string GetIdFor(object obj) return GetValueForIdProperty(idprop, obj); } + private void WriteTypeAndId(JsonWriter writer, Type propertyType, object propertyValue) + { + writer.WritePropertyName("type"); + writer.WriteValue(_modelManager.GetResourceTypeNameForType(propertyType)); + + writer.WritePropertyName("id"); + writer.WriteValue(GetValueForIdProperty(_modelManager.GetIdProperty(propertyType), propertyValue)); + } + private void WriteIdsArrayJson(Newtonsoft.Json.JsonWriter writer, IEnumerable value, Newtonsoft.Json.JsonSerializer serializer) { + // Write the data element + writer.WriteStartObject(); + writer.WritePropertyName(PrimaryDataKeyName); + IEnumerator collectionEnumerator = (value as IEnumerable).GetEnumerator(); writer.WriteStartArray(); while (collectionEnumerator.MoveNext()) { - var serializable = collectionEnumerator.Current; - writer.WriteValue(this.GetIdFor(serializable)); + writer.WriteStartObject(); + WriteTypeAndId(writer, collectionEnumerator.Current.GetType(), collectionEnumerator.Current); + writer.WriteEndObject(); } writer.WriteEndArray(); + writer.WriteEndObject(); } } From a65ffe38edf74774ee56e0e1e4cfd48c64e30f43 Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Thu, 11 Jun 2015 17:06:56 +0200 Subject: [PATCH 073/186] Fix relationships keyword Fix the included list to return the right objects --- JSONAPI/Json/JsonApiFormatter.cs | 92 +++++++++++++------------------- 1 file changed, 38 insertions(+), 54 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index ad8eeb51..00e46484 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -217,7 +217,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js // Now do other stuff if (relationshipModelProperties.Count() > 0) { - writer.WritePropertyName("relastionships"); + writer.WritePropertyName("relationships"); writer.WriteStartObject(); } foreach (var relationshipModelProperty in relationshipModelProperties) @@ -389,70 +389,54 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J /* Oh, and we have to keep a reference to the TextWriter of the JsonWriter * because there's no member to get it back out again. ?!? * */ - Dictionary> writers = new Dictionary>(); int numAdditions; - do + + if (aggregator.Appendices.Count > 0) { - numAdditions = 0; - Dictionary> appxs = new Dictionary>(aggregator.Appendices); // shallow clone, in case we add a new type during enumeration! - foreach (KeyValuePair> apair in appxs) + writer.WritePropertyName("included"); + writer.WriteStartArray(); + + do { - Type type = apair.Key; - ISet appendix = apair.Value; - JsonWriter jw; - if (writers.ContainsKey(type)) + //// Okay, we should have captured everything now. Now combine the type writers into the main writer... + //foreach (KeyValuePair> apair in writers) + //{ + // apair.Value.Key.WriteEnd(); // close off the array + // writer.WritePropertyName(_modelManager.GetResourceTypeNameForType(apair.Key)); + // writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter + //} + numAdditions = 0; + Dictionary> appxs = new Dictionary>(aggregator.Appendices); // shallow clone, in case we add a new type during enumeration! + foreach (KeyValuePair> apair in appxs) { - jw = writers[type].Key; - } - else - { - // Setup and start the writer for this type... - StringWriter sw = new StringWriter(); - jw = new JsonTextWriter(sw); - writers[type] = new KeyValuePair(jw, sw); - jw.WriteStartArray(); - } + Type type = apair.Key; + ISet appendix = apair.Value; - HashSet tbp; - if (processed.ContainsKey(type)) - { - toBeProcessed[type] = tbp = new HashSet(appendix.Except(processed[type])); - } - else - { - toBeProcessed[type] = tbp = new HashSet(appendix); - processed[type] = new HashSet(); - } + HashSet tbp; + if (processed.ContainsKey(type)) + { + toBeProcessed[type] = tbp = new HashSet(appendix.Except(processed[type])); + } + else + { + toBeProcessed[type] = tbp = new HashSet(appendix); + processed[type] = new HashSet(); + } - if (tbp.Count > 0) - { - numAdditions += tbp.Count; - foreach (object obj in tbp) + if (tbp.Count > 0) { - Serialize(obj, writeStream, jw, serializer, aggregator); // Note, not writer, but jw--we write each type to its own JsonWriter and combine them later. + numAdditions += tbp.Count; + foreach (object obj in tbp) + { + Serialize(obj, writeStream, writer, serializer, aggregator); // Note, not writer, but jw--we write each type to its own JsonWriter and combine them later. + } + processed[type].UnionWith(tbp); } - processed[type].UnionWith(tbp); } + } while (numAdditions > 0); - //TODO: Add traversal depth limit?? - } - } while (numAdditions > 0); - - if (aggregator.Appendices.Count > 0) - { - writer.WritePropertyName("included"); - writer.WriteStartObject(); - - // Okay, we should have captured everything now. Now combine the type writers into the main writer... - foreach (KeyValuePair> apair in writers) - { - apair.Value.Key.WriteEnd(); // close off the array - writer.WritePropertyName(_modelManager.GetResourceTypeNameForType(apair.Key)); - writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter - } - - writer.WriteEndObject(); + writer.WriteEndArray(); } From d391d589de5e54ae78cfe2174b5241c9f067404f Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Fri, 12 Jun 2015 11:01:51 +0200 Subject: [PATCH 074/186] Add attributes object within the data object --- JSONAPI/Json/JsonApiFormatter.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 00e46484..841d4dc8 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -444,11 +444,11 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J private void WriteAttributes(ModelProperty[] props, JsonWriter writer, PropertyInfo idProp, object value, JsonSerializer serializer, List relationshipModelProperties) { - //if (props.Count() > 0) - //{ - // writer.WritePropertyName("attributes"); - // writer.WriteStartObject(); - //} + if (props.Count() > 0) + { + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); + } foreach (var modelProperty in props) { @@ -508,8 +508,8 @@ private void WriteAttributes(ModelProperty[] props, JsonWriter writer, PropertyI } } - //if (props.Count() > 0) - // writer.WriteEndObject(); + if (props.Count() > 0) + writer.WriteEndObject(); } #endregion Serialization From 2ffdcda7b2eab99a4c249302254362ed5fab9db0 Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Fri, 12 Jun 2015 16:31:47 +0200 Subject: [PATCH 075/186] Update the test data to represent the JSON API v1 object structure --- .../Data/AttributeSerializationTest.json | 23 +- .../Data/ByteIdSerializationTest.json | 38 +-- .../Data/DeserializeAttributeRequest.json | 23 +- .../Data/DeserializeCollectionRequest.json | 10 +- JSONAPI.Tests/Data/LinkTemplateTest.json | 20 +- .../Data/MalformedRawJsonString.json | 24 +- ...adataManagerPropertyWasPresentRequest.json | 4 +- JSONAPI.Tests/Data/NonStandardIdTest.json | 16 +- ...eformatsRawJsonStringWithUnquotedKeys.json | 2 +- .../Data/SerializerIntegrationTest.json | 251 ++++++++++++------ .../Json/JsonApiMediaFormatterTests.cs | 4 +- 11 files changed, 263 insertions(+), 152 deletions(-) diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json index e00c19a0..193f3bb6 100644 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ b/JSONAPI.Tests/Data/AttributeSerializationTest.json @@ -1,8 +1,9 @@ { - "data": [ - { - "type": "samples", - "id": "1", + "data": [ + { + "type": "samples", + "id": "1", + "attributes": { "booleanField": false, "nullableBooleanField": false, "sByteField": 0, @@ -36,9 +37,12 @@ "stringField": null, "enumField": 0, "nullableEnumField": null - }, { - "type": "samples", - "id": "2", + } + }, + { + "type": "samples", + "id": "2", + "attributes": { "booleanField": true, "nullableBooleanField": true, "sByteField": 123, @@ -72,6 +76,7 @@ "stringField": "Some string 156", "enumField": 1, "nullableEnumField": 2 - } - ] + } + } + ] } diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json index ed041273..53202d9c 100644 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ b/JSONAPI.Tests/Data/ByteIdSerializationTest.json @@ -1,17 +1,25 @@ { - "data": [ - { - "type": "tags", - "id": "1", - "text": "Ember" - }, { - "type": "tags", - "id": "2", - "text": "React" - }, { - "type": "tags", - "id": "3", - "text": "Angular" - } - ] + "data": [ + { + "type": "tags", + "id": "1", + "attributes": { + "text": "Ember" + } + }, + { + "type": "tags", + "id": "2", + "atributes": { + "text": "React" + } + }, + { + "type": "tags", + "id": "3", + "attributes": { + "text": "Angular" + } + } + ] } diff --git a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json index 25942259..b8867551 100644 --- a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json +++ b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json @@ -1,9 +1,10 @@ { - "data": [ - { - "id": "1", + "data": [ + { + "id": "1", + "attributes": { "booleanField": false, - "nullableBooleanField": false, + "nullableBooleanField": false, "sByteField": 0, "nullableSByteField": null, "byteField": 0, @@ -35,8 +36,11 @@ "stringField": null, "enumField": 0, "nullableEnumField": null - }, { - "id": "2", + } + }, + { + "id": "2", + "attributes": { "booleanField": true, "nullableBooleanField": true, "sByteField": 123, @@ -70,6 +74,7 @@ "stringField": "Some string 156", "enumField": 1, "nullableEnumField": 2 - } - ] -} + } + } + ] + } diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json index 6ba4c6ab..e2c32b30 100644 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -3,24 +3,24 @@ { "id": "1", "title": "Linkbait!", - "links": { + "relationships": { "author": { - "linkage": { + "data": { "type": "authors", "id": "1" } }, "comments": { - "linkage": [{ "type": "comments", "id": "400" }, { "type": "comments", "id": "401" }] + "data": [{ "type": "comments", "id": "400" }, { "type": "comments", "id": "401" }] } } }, { "id": "2", "title": "Rant #1023", - "links": { + "relationships": { "author": { - "linkage": { + "data": { "type": "authors", "id": "1" } diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json index 90ebee37..19232d29 100644 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ b/JSONAPI.Tests/Data/LinkTemplateTest.json @@ -1,12 +1,14 @@ { - "data": { - "type": "posts", - "id": "2", - "title": "How to fry an egg", - "links": { - "author": { - "related": "/users/5" + "data": { + "type": "posts", + "id": "2", + "title": "How to fry an egg", + "relationships": { + "author": { + "links": { + "related": "/users/5" } - } - } + } + } + } } \ No newline at end of file diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json index fba94c29..52a40556 100644 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ b/JSONAPI.Tests/Data/MalformedRawJsonString.json @@ -1,13 +1,15 @@ { - "data": [ - { - "type": "comments", - "id": "5", - "body": null, - "customData": { }, - "links": { - "post": null - } - } - ] + "data": [ + { + "type": "comments", + "id": "5", + "attributes": { + "body": null + }, + "customData": { }, + "relationships": { + "post": null + } + } + ] } diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json index 97f46dbe..3b61299d 100644 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -2,9 +2,9 @@ "data": { "type": "posts", "id": "42", - "links": { + "relationships": { "author": { - "linkage": { + "data": { "id": "18", "type": "authors" } diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json index ec4b2a94..5e9f9226 100644 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ b/JSONAPI.Tests/Data/NonStandardIdTest.json @@ -1,9 +1,11 @@ { - "data": [ - { - "type": "non-standard-id-things", - "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", - "data": "Swap" - } - ] + "data": [ + { + "type": "non-standard-id-things", + "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", + "attributes": { + "data": "Swap" + } + } + ] } \ No newline at end of file diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json index c7f8feff..c670c031 100644 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json @@ -7,7 +7,7 @@ "customData": { "unquotedKey": 5 }, - "links": { + "relationships": { "post": null } } diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json index 9d68949b..5b6c90e9 100644 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ b/JSONAPI.Tests/Data/SerializerIntegrationTest.json @@ -1,90 +1,177 @@ { - "data": [ - { - "type": "posts", - "id": "1", - "title": "Linkbait!", - "links": { - "comments": [ "2", "3", "4" ], - "author": "1" + "data": [ + { + "type": "posts", + "id": "1", + "attributes": { + "title": "Linkbait!" + }, + "relationships": { + "comments": { + "data": [ + { + "type": "comments", + "id": "2" + }, + { + "type": "comments", + "id": "3" + }, + { + "type": "comments", + "id": "4" + } + ] + }, + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "2", - "title": "Rant #1023", - "links": { - "comments": [ "5" ], - "author": "1" + } + }, + { + "type": "posts", + "id": "2", + "attributes": { + "title": "Rant #1023" + }, + "relationships": { + "comments": [ + { + "type": "comments", + "id": "5" + } + ], + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "3", - "title": "Polemic in E-flat minor #824", - "links": { - "comments": [ ], - "author": "1" + } + }, + { + "type": "posts", + "id": "3", + "attributes": { + "title": "Polemic in E-flat minor #824" + }, + "relationships": { + "comments": [ ], + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "4", - "title": "This post has no author.", - "links": { - "comments": [ ], - "author": null + } + }, + { + "type": "posts", + "id": "4", + "attributes": { + "title": "This post has no author." + }, + "relationships": { + "comments": [ ], + "author": null + } + } + ], + "included": [ + { + "type": "comments", + "id": "2", + "attributes": { + "body": "Nuh uh!" + }, + "customData": null, + "relationships": { + "post": { + "data": { + "type": "posts", + "id": "1" + } } - } - ], - "linked": { - "comments": [ - { - "type": "comments", - "id": "2", - "body": "Nuh uh!", - "customData": null, - "links": { "post": "1" } - }, - { - "type": "comments", - "id": "3", - "body": "Yeah huh!", - "customData": null, - "links": { - "post": "1" - } - }, - { - "type": "comments", - "id": "4", - "body": "Third Reich.", - "customData": { - "foo": "bar" - }, - "links": { - "post": "1" - } - }, - { - "type": "comments", - "id": "5", - "body": "I laughed, I cried!", - "customData": null, - "links": { - "post": "2" - } + } + }, + { + "type": "comments", + "id": "3", + "attributes": { + "body": "Yeah huh!" + }, + "customData": null, + "relationships": { + "post": { + "data": { + "type": "posts", + "id": "1" + } + } + } + }, + { + "type": "comments", + "id": "4", + "attributes": { + "body": "Third Reich." + }, + "customData": { + "foo": "bar" + }, + "relationships": { + "post": { + "data": { + "type": "posts", + "id": "1" + } + } + } + }, + { + "type": "comments", + "id": "5", + "attributes": { + "body": "I laughed, I cried!" + }, + "customData": null, + "relationships": { + "post": { + "data": { + "type": "posts", + "id": "2" + } } - ], - "authors": [ - { - "type": "authors", - "id": "1", - "name": "Jason Hater", - "links": { - "posts": [ "1", "2", "3" ] - } + } + }, + { + "type": "authors", + "id": "1", + "attributes": { + "name": "Jason Hater" + }, + "relationships": { + "posts": { + "data": [ + { + "type": "posts", + "id": "2" + }, + { + "type": "posts", + "id": "3" + }, + { + "type": "posts", + "id": "4" + } + ] } - ] - } + + } + } + ] } diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index 6185e258..95632b66 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -525,7 +525,7 @@ public void DeserializeExtraPropertyTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""bogus"":""PANIC!"",""links"":{""posts"":{""linkage"":[]}}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""type"":""authors"",""attributes"":{""name"":""Jason Hater"",""bogus"":""PANIC!""},""relationships"":{""posts"":{""data"":[]}}}}")); // Act Author a; @@ -545,7 +545,7 @@ public void DeserializeExtraRelationshipTest() var formatter = new JsonApiFormatter(modelManager); MemoryStream stream = new MemoryStream(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""name"":""Jason Hater"",""links"":{""posts"":{""linkage"":[]},""bogus"":{""linkage"":[]}}}}")); + stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""type"":""authors"",""attributes"":{""name"":""Jason Hater""},""relationships"":{""posts"":{""data"":[]},""bogus"":{""data"":[]}}}}")); // Act Author a; From ce5511a61410084d03f828f343ce80515a748ec1 Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Fri, 12 Jun 2015 16:34:16 +0200 Subject: [PATCH 076/186] Clean up some commented and not used code --- JSONAPI/Attributes/SerializeAs.cs | 2 +- JSONAPI/Json/JsonApiFormatter.cs | 111 +++++++----------------------- 2 files changed, 27 insertions(+), 86 deletions(-) diff --git a/JSONAPI/Attributes/SerializeAs.cs b/JSONAPI/Attributes/SerializeAs.cs index 53eac010..c83616d4 100644 --- a/JSONAPI/Attributes/SerializeAs.cs +++ b/JSONAPI/Attributes/SerializeAs.cs @@ -2,7 +2,7 @@ namespace JSONAPI.Attributes { - public enum SerializeAsOptions { Ids, Link, Embedded } + public enum SerializeAsOptions { Ids, Link } [System.AttributeUsage(System.AttributeTargets.Property)] public class SerializeAs diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 841d4dc8..7a4c33c7 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -91,6 +91,11 @@ public override bool CanWriteType(Type type) } private const string PrimaryDataKeyName = "data"; + private const string AttributesKeyName = "attributes"; + private const string RelationshipsKeyName = "relationships"; + private const string IncludedKeyName = "included"; + private const string LinksKeyName = "links"; + private const string RelatedKeyName = "related"; #region Serialization @@ -217,7 +222,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js // Now do other stuff if (relationshipModelProperties.Count() > 0) { - writer.WritePropertyName("relationships"); + writer.WritePropertyName(RelationshipsKeyName); writer.WriteStartObject(); } foreach (var relationshipModelProperty in relationshipModelProperties) @@ -296,15 +301,12 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); //TODO: Support ids and type properties in "link" object writer.WriteStartObject(); - writer.WritePropertyName("links"); + writer.WritePropertyName(LinksKeyName); + writer.WriteStartObject(); + writer.WritePropertyName(RelatedKeyName); writer.WriteValue(href); writer.WriteEndObject(); - break; - case SerializeAsOptions.Embedded: - // Not really supported by Ember Data yet, incidentally...but easy to implement here. - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); - //serializer.Serialize(writer, prop.GetValue(value, null)); - this.Serialize(prop.GetValue(value, null), writeStream, writer, serializer, aggregator); + writer.WriteEndObject(); break; } } @@ -345,17 +347,13 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js var relatedObjectId = lt.Contains("{0}") ? objId.Value : null; string link = String.Format(lt, relatedObjectId, GetIdFor(value)); - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); writer.WriteStartObject(); - writer.WritePropertyName("links"); + writer.WritePropertyName(LinksKeyName); + writer.WriteStartObject(); + writer.WritePropertyName(RelatedKeyName); writer.WriteValue(link); writer.WriteEndObject(); - break; - case SerializeAsOptions.Embedded: - // Not really supported by Ember Data yet, incidentally...but easy to implement here. - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); - //serializer.Serialize(writer, prop.GetValue(value, null)); - this.Serialize(propertyValue, writeStream, writer, serializer, aggregator); + writer.WriteEndObject(); break; } } @@ -394,18 +392,11 @@ protected void SerializeLinkedResources(Stream writeStream, JsonWriter writer, J if (aggregator.Appendices.Count > 0) { - writer.WritePropertyName("included"); + writer.WritePropertyName(IncludedKeyName); writer.WriteStartArray(); do { - //// Okay, we should have captured everything now. Now combine the type writers into the main writer... - //foreach (KeyValuePair> apair in writers) - //{ - // apair.Value.Key.WriteEnd(); // close off the array - // writer.WritePropertyName(_modelManager.GetResourceTypeNameForType(apair.Key)); - // writer.WriteRawValue(apair.Value.Value.ToString()); // write the contents of the type JsonWriter's StringWriter to the main JsonWriter - //} numAdditions = 0; Dictionary> appxs = new Dictionary>(aggregator.Appendices); // shallow clone, in case we add a new type during enumeration! foreach (KeyValuePair> apair in appxs) @@ -446,14 +437,14 @@ private void WriteAttributes(ModelProperty[] props, JsonWriter writer, PropertyI { if (props.Count() > 0) { - writer.WritePropertyName("attributes"); + writer.WritePropertyName(AttributesKeyName); writer.WriteStartObject(); } foreach (var modelProperty in props) { var prop = modelProperty.Property; - if (prop == idProp) continue; // Don't write the "id" property twice, see above! + if (prop == idProp) continue; // Don't write the "id" property twice! if (modelProperty is FieldModelProperty) { @@ -561,15 +552,19 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, reader.Read(); // burn the PropertyName token switch (value) { - case "included": + case IncludedKeyName: //TODO: If we want to capture linked/related objects in a compound document when deserializing, do it here...do we? reader.Skip(); break; - case "relationships": + case RelationshipsKeyName: // ignore this, is it even meaningful in a PUT/POST body? reader.Skip(); break; case PrimaryDataKeyName: + // Could be a single resource or multiple, according to spec! + foundPrimaryData = true; + break; + case AttributesKeyName: // Could be a single resource or multiple, according to spec! foundPrimaryData = true; retval = DeserializePrimaryData(singleType, reader); @@ -644,27 +639,6 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, return GetDefaultValueForType(type); } - /* - try - { - using (var reader = (new StreamReader(readStream, effectiveEncoding))) - { - var json = reader.ReadToEnd(); - var jo = JObject.Parse(json); - return jo.SelectToken(root, false).ToObject(type); - } - } - catch (Exception e) - { - if (formatterLogger == null) - { - throw; - } - formatterLogger.LogError(String.Empty, e); - return GetDefaultValueForType(type); - } - */ - return GetDefaultValueForType(type); } @@ -715,8 +689,9 @@ private object Deserialize(Type objectType, JsonReader reader) string value = (string)reader.Value; var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value); - if (value == "related") + if (value == RelatedKeyName) { + // This can only happen within a links object reader.Read(); // burn the PropertyName token //TODO: linked resources (Done??) DeserializeLinkedResources(retval, reader); @@ -795,40 +770,6 @@ private object Deserialize(Type objectType, JsonReader reader) } while (reader.TokenType != JsonToken.EndObject); reader.Read(); // burn the EndObject token before returning back up the call stack - /* - // Suss out all the relationship members, and which ones have what cardinality... - IEnumerable relations = ( - from prop in objectType.GetProperties() - where !CanWriteTypeAsJsonApiAttribute(prop.PropertyType) - && prop.GetCustomAttributes(true).Any(attribute => attribute is System.Runtime.Serialization.DataMemberAttribute) - select prop - ); - IEnumerable hasManys = relations.Where(prop => typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)); - IEnumerable belongsTos = relations.Where(prop => !typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)); - - JObject links = (JObject)jo["links"]; - - // Lets deal with belongsTos first, that should be simpler... - foreach (PropertyInfo prop in belongsTos) - { - if (links == null) break; // Well, apparently we don't have any data for the relationships! - - string btId = (string)links[_modelManager.GetJsonKeyForProperty(prop)]; - if (btId == null) - { - prop.SetValue(retval, null, null); // Important that we set--the value may have been cleared! - continue; // breaking early! - } - Type relType = prop.PropertyType; - //if (typeof(EntityObject).IsAssignableFrom(relType)) - if (resolver.CanIncludeTypeAsObject(relType)) - { - prop.SetValue(retval, resolver.GetById(relType, btId), null); - //throw new ApplicationException(String.Format("Could not assign BelongsTo property \"{0}\" on object of type {1} by ID {2} because no object of type {3} could be retrieved by that ID.", prop.Name, objectType, btId, prop.PropertyType)); - } - } - */ - return retval; } @@ -863,7 +804,7 @@ private void DeserializeLinkedResources(object obj, JsonReader reader) throw new BadRequestException("Each relationship key on a links object must have an object value."); var relationshipObject = (JObject) relationshipToken; - var linkageToken = relationshipObject["linkage"]; + var linkageToken = relationshipObject[PrimaryDataKeyName]; var linkageObjects = new List>(); From 52e5d4ff555eae5b37cd5b210f7c45c85ec5d7b9 Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Sat, 13 Jun 2015 12:18:39 +0200 Subject: [PATCH 077/186] Fix some more test regarding attributes The deserialization of attributes is now working properly --- .../Data/AttributeSerializationTest.json | 6 +- .../Data/ByteIdSerializationTest.json | 2 +- .../Data/DeserializeAttributeRequest.json | 150 +++++++------- .../Data/DeserializeCollectionRequest.json | 21 +- .../Data/SerializerIntegrationTest.json | 183 +++++++++--------- .../Json/JsonApiMediaFormatterTests.cs | 2 - JSONAPI/Json/JsonApiFormatter.cs | 10 +- 7 files changed, 199 insertions(+), 175 deletions(-) diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json index 193f3bb6..2d13231b 100644 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ b/JSONAPI.Tests/Data/AttributeSerializationTest.json @@ -26,7 +26,7 @@ "nullableDoubleField": null, "singleField": 0.0, "nullableSingleField": null, - "decimalField": "0", + "decimalField": 0.0, "nullableDecimalField": null, "dateTimeField": "0001-01-01T00:00:00", "nullableDateTimeField": null, @@ -65,8 +65,8 @@ "nullableDoubleField": 1056789.123, "singleField": 1056789.13, "nullableSingleField": 1056789.13, - "decimalField": "1056789.123", - "nullableDecimalField": "1056789.123", + "decimalField": 1056789.123, + "nullableDecimalField": 1056789.123, "dateTimeField": "1776-07-04T00:00:00", "nullableDateTimeField": "1776-07-04T00:00:00", "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json index 53202d9c..d4e9d1a4 100644 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ b/JSONAPI.Tests/Data/ByteIdSerializationTest.json @@ -10,7 +10,7 @@ { "type": "tags", "id": "2", - "atributes": { + "attributes": { "text": "React" } }, diff --git a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json index b8867551..5c0a093b 100644 --- a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json +++ b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json @@ -1,79 +1,81 @@ { "data": [ - { - "id": "1", - "attributes": { - "booleanField": false, - "nullableBooleanField": false, - "sByteField": 0, - "nullableSByteField": null, - "byteField": 0, - "nullableByteField": null, - "int16Field": 0, - "nullableInt16Field": null, - "uInt16Field": 0, - "nullableUInt16Field": null, - "int32Field": 0, - "nullableInt32Field": null, - "uInt32Field": 0, - "nullableUInt32Field": null, - "int64Field": 0, - "nullableInt64Field": null, - "uInt64Field": 0, - "nullableUInt64Field": null, - "doubleField": 0.0, - "nullableDoubleField": null, - "singleField": 0.0, - "nullableSingleField": null, - "decimalField": "0", - "nullableDecimalField": null, - "dateTimeField": "0001-01-01T00:00:00", - "nullableDateTimeField": null, - "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", - "nullableDateTimeOffsetField": null, - "guidField": "00000000-0000-0000-0000-000000000000", - "nullableGuidField": null, - "stringField": null, - "enumField": 0, - "nullableEnumField": null - } - }, - { - "id": "2", - "attributes": { - "booleanField": true, - "nullableBooleanField": true, - "sByteField": 123, - "nullableSByteField": 123, - "byteField": 253, - "nullableByteField": 253, - "int16Field": 32000, - "nullableInt16Field": 32000, - "uInt16Field": 64000, - "nullableUInt16Field": 64000, - "int32Field": 2000000000, - "nullableInt32Field": 2000000000, - "uInt32Field": 3000000000, - "nullableUInt32Field": 3000000000, - "int64Field": 9223372036854775807, - "nullableInt64Field": 9223372036854775807, - "uInt64Field": 9223372036854775808, - "nullableUInt64Field": 9223372036854775808, - "doubleField": 1056789.123, - "nullableDoubleField": 1056789.123, - "singleField": 1056789.13, - "nullableSingleField": 1056789.13, - "decimalField": "1056789.123", - "nullableDecimalField": "1056789.123", - "dateTimeField": "1776-07-04T00:00:00", - "nullableDateTimeField": "1776-07-04T00:00:00", - "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", - "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", - "stringField": "Some string 156", - "enumField": 1, - "nullableEnumField": 2 + { + "type": "samples", + "id": "1", + "attributes": { + "booleanField": false, + "nullableBooleanField": false, + "sByteField": 0, + "nullableSByteField": null, + "byteField": 0, + "nullableByteField": null, + "int16Field": 0, + "nullableInt16Field": null, + "uInt16Field": 0, + "nullableUInt16Field": null, + "int32Field": 0, + "nullableInt32Field": null, + "uInt32Field": 0, + "nullableUInt32Field": null, + "int64Field": 0, + "nullableInt64Field": null, + "uInt64Field": 0, + "nullableUInt64Field": null, + "doubleField": 0.0, + "nullableDoubleField": null, + "singleField": 0.0, + "nullableSingleField": null, + "decimalField": "0", + "nullableDecimalField": null, + "dateTimeField": "0001-01-01T00:00:00", + "nullableDateTimeField": null, + "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", + "nullableDateTimeOffsetField": null, + "guidField": "00000000-0000-0000-0000-000000000000", + "nullableGuidField": null, + "stringField": null, + "enumField": 0, + "nullableEnumField": null + } + }, + { + "type": "samples", + "id": "2", + "attributes": { + "booleanField": true, + "nullableBooleanField": true, + "sByteField": 123, + "nullableSByteField": 123, + "byteField": 253, + "nullableByteField": 253, + "int16Field": 32000, + "nullableInt16Field": 32000, + "uInt16Field": 64000, + "nullableUInt16Field": 64000, + "int32Field": 2000000000, + "nullableInt32Field": 2000000000, + "uInt32Field": 3000000000, + "nullableUInt32Field": 3000000000, + "int64Field": 9223372036854775807, + "nullableInt64Field": 9223372036854775807, + "uInt64Field": 9223372036854775808, + "nullableUInt64Field": 9223372036854775808, + "doubleField": 1056789.123, + "nullableDoubleField": 1056789.123, + "singleField": 1056789.13, + "nullableSingleField": 1056789.13, + "decimalField": "1056789.123", + "nullableDecimalField": "1056789.123", + "dateTimeField": "1776-07-04T00:00:00", + "nullableDateTimeField": "1776-07-04T00:00:00", + "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", + "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", + "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", + "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", + "stringField": "Some string 156", + "enumField": 1, + "nullableEnumField": 2 } } ] diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json index e2c32b30..52b3f7b3 100644 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json @@ -1,8 +1,11 @@ { "data": [ { + "type": "posts", "id": "1", - "title": "Linkbait!", + "attributes": { + "title": "Linkbait!" + }, "relationships": { "author": { "data": { @@ -11,13 +14,25 @@ } }, "comments": { - "data": [{ "type": "comments", "id": "400" }, { "type": "comments", "id": "401" }] + "data": [ + { + "type": "comments", + "id": "400" + }, + { + "type": "comments", + "id": "401" + } + ] } } }, { + "type": "posts", "id": "2", - "title": "Rant #1023", + "attributes": { + "title": "Rant #1023" + }, "relationships": { "author": { "data": { diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json index 5b6c90e9..74c2fd6b 100644 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ b/JSONAPI.Tests/Data/SerializerIntegrationTest.json @@ -1,93 +1,95 @@ { - "data": [ - { - "type": "posts", - "id": "1", - "attributes": { - "title": "Linkbait!" - }, - "relationships": { - "comments": { - "data": [ - { - "type": "comments", - "id": "2" - }, - { - "type": "comments", - "id": "3" - }, - { - "type": "comments", - "id": "4" - } - ] + "data": [ + { + "type": "posts", + "id": "1", + "attributes": { + "title": "Linkbait!" }, - "author": { - "data": { - "type": "authors", - "id": "1" - } + "relationships": { + "comments": { + "data": [ + { + "type": "comments", + "id": "2" + }, + { + "type": "comments", + "id": "3" + }, + { + "type": "comments", + "id": "4" + } + ] + }, + "author": { + "data": { + "type": "authors", + "id": "1" + } + } } - } - }, - { - "type": "posts", - "id": "2", - "attributes": { - "title": "Rant #1023" - }, - "relationships": { - "comments": [ - { - "type": "comments", - "id": "5" - } - ], - "author": { - "data": { - "type": "authors", - "id": "1" - } + }, + { + "type": "posts", + "id": "2", + "attributes": { + "title": "Rant #1023" + }, + "relationships": { + "comments": { + "data": [ + { + "type": "comments", + "id": "5" + } + ] + }, + "author": { + "data": { + "type": "authors", + "id": "1" + } + } } - } - }, - { - "type": "posts", - "id": "3", - "attributes": { - "title": "Polemic in E-flat minor #824" - }, - "relationships": { - "comments": [ ], - "author": { - "data": { - "type": "authors", - "id": "1" - } + }, + { + "type": "posts", + "id": "3", + "attributes": { + "title": "Polemic in E-flat minor #824" + }, + "relationships": { + "comments": [ ], + "author": { + "data": { + "type": "authors", + "id": "1" + } + } } - } - }, - { - "type": "posts", - "id": "4", - "attributes": { - "title": "This post has no author." - }, - "relationships": { - "comments": [ ], - "author": null - } - } - ], + }, + { + "type": "posts", + "id": "4", + "attributes": { + "title": "This post has no author." + }, + "relationships": { + "comments": [ ], + "author": null + } + } + ], "included": [ { "type": "comments", "id": "2", "attributes": { - "body": "Nuh uh!" + "body": "Nuh uh!", + "customData": null }, - "customData": null, "relationships": { "post": { "data": { @@ -101,9 +103,9 @@ "type": "comments", "id": "3", "attributes": { - "body": "Yeah huh!" + "body": "Yeah huh!", + "customData": null }, - "customData": null, "relationships": { "post": { "data": { @@ -117,11 +119,12 @@ "type": "comments", "id": "4", "attributes": { - "body": "Third Reich." - }, - "customData": { - "foo": "bar" + "body": "Third Reich.", + "customData": { + "foo": "bar" + } }, + "relationships": { "post": { "data": { @@ -135,9 +138,9 @@ "type": "comments", "id": "5", "attributes": { - "body": "I laughed, I cried!" + "body": "I laughed, I cried!", + "customData": null }, - "customData": null, "relationships": { "post": { "data": { @@ -158,15 +161,15 @@ "data": [ { "type": "posts", - "id": "2" + "id": "1" }, { "type": "posts", - "id": "3" + "id": "2" }, { "type": "posts", - "id": "4" + "id": "3" } ] } diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs index 95632b66..39a5c5d8 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs @@ -220,7 +220,6 @@ public void SerializerIntegrationTest() Trace.WriteLine(output); var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); Assert.AreEqual(expected, output.Trim()); - //Assert.AreEqual("[2,3,4]", sw.ToString()); } [TestMethod] @@ -243,7 +242,6 @@ public void SerializeArrayIntegrationTest() Trace.WriteLine(output); var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); Assert.AreEqual(expected, output.Trim()); - //Assert.AreEqual("[2,3,4]", sw.ToString()); } [TestMethod] diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 7a4c33c7..3d286523 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -563,6 +563,7 @@ private object ReadFromStream(Type type, Stream readStream, HttpContent content, case PrimaryDataKeyName: // Could be a single resource or multiple, according to spec! foundPrimaryData = true; + retval = DeserializePrimaryData(singleType, reader); break; case AttributesKeyName: // Could be a single resource or multiple, according to spec! @@ -676,9 +677,9 @@ private object DeserializePrimaryData(Type singleType, JsonReader reader) return retval; } - private object Deserialize(Type objectType, JsonReader reader) + private object Deserialize(Type objectType, JsonReader reader, object obj = null) { - object retval = Activator.CreateInstance(objectType); + object retval = obj == null ? Activator.CreateInstance(objectType) : obj; if (reader.TokenType != JsonToken.StartObject) throw new JsonReaderException(String.Format("Expected JsonToken.StartObject, got {0}", reader.TokenType.ToString())); reader.Read(); // Burn the StartObject token @@ -696,6 +697,11 @@ private object Deserialize(Type objectType, JsonReader reader) //TODO: linked resources (Done??) DeserializeLinkedResources(retval, reader); } + else if (value == AttributesKeyName) { + reader.Read(); + + retval = Deserialize(objectType, reader, retval); + } else if (modelProperty != null) { if (!(modelProperty is FieldModelProperty)) From bf220f6082d7ac94940e400a23b3fa692df886d1 Mon Sep 17 00:00:00 2001 From: GlennGeelen Date: Mon, 15 Jun 2015 17:26:34 +0200 Subject: [PATCH 078/186] Fix the serialization for links Fix all unit tests except for the filter tests --- JSONAPI.Tests/Data/LinkTemplateTest.json | 8 +- .../Data/MalformedRawJsonString.json | 10 +- ...adataManagerPropertyWasPresentRequest.json | 22 +-- ...eformatsRawJsonStringWithUnquotedKeys.json | 24 +-- .../Data/SerializerIntegrationTest.json | 156 +++++++++--------- JSONAPI.Tests/Json/LinkTemplateTests.cs | 2 +- JSONAPI/Attributes/SerializeAs.cs | 2 +- JSONAPI/Json/JsonApiFormatter.cs | 151 +++++++++-------- 8 files changed, 196 insertions(+), 179 deletions(-) diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json index 19232d29..95f9b61d 100644 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ b/JSONAPI.Tests/Data/LinkTemplateTest.json @@ -2,9 +2,15 @@ "data": { "type": "posts", "id": "2", - "title": "How to fry an egg", + "attributes": { + "title": "How to fry an egg" + }, "relationships": { "author": { + "data": { + "type": "users", + "id": "5" + }, "links": { "related": "/users/5" } diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json index 52a40556..51a07b7d 100644 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ b/JSONAPI.Tests/Data/MalformedRawJsonString.json @@ -3,12 +3,14 @@ { "type": "comments", "id": "5", - "attributes": { - "body": null + "attributes": { + "body": null, + "customData": { } }, - "customData": { }, "relationships": { - "post": null + "post": { + "data": null + } } } ] diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json index 3b61299d..35a76237 100644 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json @@ -1,14 +1,14 @@ { - "data": { - "type": "posts", - "id": "42", - "relationships": { - "author": { - "data": { - "id": "18", - "type": "authors" - } + "data": { + "type": "posts", + "id": "42", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "18" } - } - } + } + } + } } \ No newline at end of file diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json index c670c031..efc860ab 100644 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json @@ -1,15 +1,19 @@ { - "data": [ - { - "type": "comments", - "id": "5", + "data": [ + { + "type": "comments", + "id": "5", + "attributes": { "body": null, "customData": { - "unquotedKey": 5 - }, - "relationships": { - "post": null + "unquotedKey": 5 } - } - ] + }, + "relationships": { + "post": { + "data": null + } + } + } + ] } \ No newline at end of file diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json index 74c2fd6b..a5bd99e8 100644 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ b/JSONAPI.Tests/Data/SerializerIntegrationTest.json @@ -1,87 +1,93 @@ { - "data": [ - { - "type": "posts", - "id": "1", - "attributes": { - "title": "Linkbait!" + "data": [ + { + "type": "posts", + "id": "1", + "attributes": { + "title": "Linkbait!" + }, + "relationships": { + "comments": { + "data": [ + { + "type": "comments", + "id": "2" + }, + { + "type": "comments", + "id": "3" + }, + { + "type": "comments", + "id": "4" + } + ] }, - "relationships": { - "comments": { - "data": [ - { - "type": "comments", - "id": "2" - }, - { - "type": "comments", - "id": "3" - }, - { - "type": "comments", - "id": "4" - } - ] - }, - "author": { - "data": { - "type": "authors", - "id": "1" - } - } + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "2", - "attributes": { - "title": "Rant #1023" + } + }, + { + "type": "posts", + "id": "2", + "attributes": { + "title": "Rant #1023" + }, + "relationships": { + "comments": { + "data": [ + { + "type": "comments", + "id": "5" + } + ] }, - "relationships": { - "comments": { - "data": [ - { - "type": "comments", - "id": "5" - } - ] - }, - "author": { - "data": { - "type": "authors", - "id": "1" - } - } + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "3", - "attributes": { - "title": "Polemic in E-flat minor #824" + } + }, + { + "type": "posts", + "id": "3", + "attributes": { + "title": "Polemic in E-flat minor #824" + }, + "relationships": { + "comments": { + "data": [ ] }, - "relationships": { - "comments": [ ], - "author": { - "data": { - "type": "authors", - "id": "1" - } - } + "author": { + "data": { + "type": "authors", + "id": "1" + } } - }, - { - "type": "posts", - "id": "4", - "attributes": { - "title": "This post has no author." + } + }, + { + "type": "posts", + "id": "4", + "attributes": { + "title": "This post has no author." + }, + "relationships": { + "comments": { + "data": [ ] }, - "relationships": { - "comments": [ ], - "author": null + "author": { + "data": null } - } - ], + } + } + ], "included": [ { "type": "comments", diff --git a/JSONAPI.Tests/Json/LinkTemplateTests.cs b/JSONAPI.Tests/Json/LinkTemplateTests.cs index 2daeea62..f46fa9ce 100644 --- a/JSONAPI.Tests/Json/LinkTemplateTests.cs +++ b/JSONAPI.Tests/Json/LinkTemplateTests.cs @@ -17,7 +17,7 @@ private class Post public string Title { get; set; } - [SerializeAs(SerializeAsOptions.Link)] + [SerializeAs(SerializeAsOptions.RelatedLink)] [LinkTemplate("/users/{0}")] public virtual User Author { get; set; } } diff --git a/JSONAPI/Attributes/SerializeAs.cs b/JSONAPI/Attributes/SerializeAs.cs index c83616d4..c4cfcb5c 100644 --- a/JSONAPI/Attributes/SerializeAs.cs +++ b/JSONAPI/Attributes/SerializeAs.cs @@ -2,7 +2,7 @@ namespace JSONAPI.Attributes { - public enum SerializeAsOptions { Ids, Link } + public enum SerializeAsOptions { RelatedLink, SelfLink, BothLinks, NoLinks } [System.AttributeUsage(System.AttributeTargets.Property)] public class SerializeAs diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 3d286523..4c9e69ef 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -230,7 +230,7 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js bool skip = false, iip = false; string lt = null; - SerializeAsOptions sa = SerializeAsOptions.Ids; + SerializeAsOptions sa = SerializeAsOptions.NoLinks; var prop = relationshipModelProperty.Property; @@ -253,111 +253,115 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js } if (skip) continue; + // Write the relationship's type writer.WritePropertyName(relationshipModelProperty.JsonKey); + // Now look for enumerable-ness: if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) { + writer.WriteStartObject(); + // Write the data element + writer.WritePropertyName(PrimaryDataKeyName); // Look out! If we want to SerializeAs a link, computing the property is probably // expensive...so don't force it just to check for null early! - if (sa != SerializeAsOptions.Link && prop.GetValue(value, null) == null) + if (sa == SerializeAsOptions.NoLinks && prop.GetValue(value, null) == null) { writer.WriteStartArray(); writer.WriteEndArray(); + writer.WriteEndObject(); continue; } - switch (sa) + // Always write the data attribute of a relationship + IEnumerable items = (IEnumerable)prop.GetValue(value, null); + if (items == null) { - case SerializeAsOptions.Ids: - IEnumerable items = (IEnumerable)prop.GetValue(value, null); - if (items == null) + // Return an empty array when there are no items + writer.WriteStartArray(); + writer.WriteEndArray(); + } + else + { + // Write the array with data + this.WriteIdsArrayJson(writer, items, serializer); + if (iip) + { + Type itemType; + if (prop.PropertyType.IsGenericType) { - // Return an empty array when there are no items - writer.WriteStartArray(); - writer.WriteEndArray(); - break; // LOOK OUT! Ending this case block early here!!! + itemType = prop.PropertyType.GetGenericArguments()[0]; } - this.WriteIdsArrayJson(writer, items, serializer); - if (iip) + else { - Type itemType; - if (prop.PropertyType.IsGenericType) - { - itemType = prop.PropertyType.GetGenericArguments()[0]; - } - else - { - // Must be an array at this point, right?? - itemType = prop.PropertyType.GetElementType(); - } - if (aggregator != null) aggregator.Add(itemType, items); // should call the IEnumerable one...right? + // Must be an array at this point, right?? + itemType = prop.PropertyType.GetElementType(); } - break; - case SerializeAsOptions.Link: - if (lt == null) throw new JsonSerializationException("A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); - //TODO: Check for "{0}" in linkTemplate and (only) if it's there, get the Ids of all objects and "implode" them. - string href = String.Format(lt, null, GetIdFor(value)); - //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); - //TODO: Support ids and type properties in "link" object - writer.WriteStartObject(); - writer.WritePropertyName(LinksKeyName); - writer.WriteStartObject(); - writer.WritePropertyName(RelatedKeyName); - writer.WriteValue(href); - writer.WriteEndObject(); - writer.WriteEndObject(); - break; + if (aggregator != null) aggregator.Add(itemType, items); // should call the IEnumerable one...right? + } + } + + // in case there is also a link defined, add it to the relationship + if (sa == SerializeAsOptions.RelatedLink) + { + if (lt == null) throw new JsonSerializationException("A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); + //TODO: Check for "{0}" in linkTemplate and (only) if it's there, get the Ids of all objects and "implode" them. + string href = String.Format(lt, null, GetIdFor(value)); + //writer.WritePropertyName(ContractResolver._modelManager.GetJsonKeyForProperty(prop)); + //TODO: Support ids and type properties in "link" object + + writer.WritePropertyName(LinksKeyName); + writer.WriteStartObject(); + writer.WritePropertyName(RelatedKeyName); + writer.WriteValue(href); + writer.WriteEndObject(); } + writer.WriteEndObject(); } else { var propertyValue = prop.GetValue(value, null); + writer.WriteStartObject(); + // Write the data element + writer.WritePropertyName(PrimaryDataKeyName); // Look out! If we want to SerializeAs a link, computing the property is probably // expensive...so don't force it just to check for null early! - if (sa != SerializeAsOptions.Link && propertyValue == null) + if (sa == SerializeAsOptions.NoLinks && propertyValue == null) { writer.WriteNull(); + writer.WriteEndObject(); continue; } Lazy objId = new Lazy(() => GetIdFor(propertyValue)); - switch (sa) + // Write the data object + writer.WriteStartObject(); + WriteTypeAndId(writer, prop.PropertyType, propertyValue); + writer.WriteEndObject(); + + if (iip) + if (aggregator != null) + aggregator.Add(prop.PropertyType, propertyValue); + + // If there are links write them next to the data object + if (sa == SerializeAsOptions.RelatedLink) { - case SerializeAsOptions.Ids: - // Write the data element - writer.WriteStartObject(); - writer.WritePropertyName(PrimaryDataKeyName); - writer.WriteStartObject(); - // Write the type and id - WriteTypeAndId(writer, prop.PropertyType, propertyValue); - - writer.WriteEndObject(); - writer.WriteEndObject(); - if (iip) - if (aggregator != null) - aggregator.Add(prop.PropertyType, propertyValue); - break; - case SerializeAsOptions.Link: - if (lt == null) - throw new JsonSerializationException( - "A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); - var relatedObjectId = lt.Contains("{0}") ? objId.Value : null; - string link = String.Format(lt, relatedObjectId, GetIdFor(value)); - - writer.WriteStartObject(); - writer.WritePropertyName(LinksKeyName); - writer.WriteStartObject(); - writer.WritePropertyName(RelatedKeyName); - writer.WriteValue(link); - writer.WriteEndObject(); - writer.WriteEndObject(); - break; + if (lt == null) + throw new JsonSerializationException( + "A property was decorated with SerializeAs(SerializeAsOptions.Link) but no LinkTemplate attribute was provided."); + var relatedObjectId = lt.Contains("{0}") ? objId.Value : null; + string link = String.Format(lt, relatedObjectId, GetIdFor(value)); + + writer.WritePropertyName(LinksKeyName); + writer.WriteStartObject(); + writer.WritePropertyName(RelatedKeyName); + writer.WriteValue(link); + writer.WriteEndObject(); } + writer.WriteEndObject(); } - } if (relationshipModelProperties.Count() > 0) { @@ -690,7 +694,7 @@ private object Deserialize(Type objectType, JsonReader reader, object obj = null string value = (string)reader.Value; var modelProperty = _modelManager.GetPropertyForJsonKey(objectType, value); - if (value == RelatedKeyName) + if (value == RelationshipsKeyName) { // This can only happen within a links object reader.Read(); // burn the PropertyName token @@ -969,10 +973,6 @@ private void WriteTypeAndId(JsonWriter writer, Type propertyType, object propert private void WriteIdsArrayJson(Newtonsoft.Json.JsonWriter writer, IEnumerable value, Newtonsoft.Json.JsonSerializer serializer) { - // Write the data element - writer.WriteStartObject(); - writer.WritePropertyName(PrimaryDataKeyName); - IEnumerator collectionEnumerator = (value as IEnumerable).GetEnumerator(); writer.WriteStartArray(); while (collectionEnumerator.MoveNext()) @@ -982,7 +982,6 @@ private void WriteIdsArrayJson(Newtonsoft.Json.JsonWriter writer, IEnumerable Date: Fri, 19 Jun 2015 14:01:45 -0400 Subject: [PATCH 079/186] fix remaining tests --- .../Responses/GetSearchResultsResponse.json | 54 ++++-- .../Responses/GetReturnsIPayloadResponse.json | 28 +-- .../PatchWithArrayForToOneLinkageRequest.json | 4 +- ...atchWithArrayRelationshipValueRequest.json | 2 +- .../PatchWithAttributeUpdateRequest.json | 4 +- .../PatchWithMissingToManyLinkageRequest.json | 2 +- .../PatchWithNullForToManyLinkageRequest.json | 4 +- .../PatchWithNullToOneUpdateRequest.json | 6 +- ...atchWithObjectForToManyLinkageRequest.json | 4 +- ...atchWithStringForToManyLinkageRequest.json | 4 +- ...PatchWithStringForToOneLinkageRequest.json | 4 +- ...tchWithStringRelationshipValueRequest.json | 2 +- ...chWithToManyEmptyLinkageUpdateRequest.json | 4 +- ...ithToManyHomogeneousDataUpdateRequest.json | 4 +- ...thToManyLinkageObjectMissingIdRequest.json | 4 +- ...ToManyLinkageObjectMissingTypeRequest.json | 4 +- .../PatchWithToManyUpdateRequest.json | 6 +- ...ithToOneLinkageObjectMissingIdRequest.json | 4 +- ...hToOneLinkageObjectMissingTypeRequest.json | 4 +- .../Requests/PatchWithToOneUpdateRequest.json | 4 +- .../Fixtures/Posts/Requests/PostRequest.json | 13 +- .../Posts/Responses/GetAllResponse.json | 112 +++++++++--- .../Posts/Responses/GetByIdResponse.json | 30 +++- .../Responses/GetWithFilterResponse.json | 21 ++- .../PatchWithAttributeUpdateResponse.json | 30 +++- .../PatchWithNullToOneUpdateResponse.json | 27 ++- ...hWithToManyEmptyLinkageUpdateResponse.json | 28 ++- ...thToManyHomogeneousDataUpdateResponse.json | 30 +++- .../PatchWithToManyUpdateResponse.json | 29 +++- .../PatchWithToOneUpdateResponse.json | 30 +++- .../Posts/Responses/PostResponse.json | 21 ++- .../Responses/GetSortedAscendingResponse.json | 164 +++++++++++------- .../GetSortedByMixedDirectionResponse.json | 164 +++++++++++------- .../GetSortedByMultipleAscendingResponse.json | 164 +++++++++++------- ...GetSortedByMultipleDescendingResponse.json | 164 +++++++++++------- .../GetSortedDescendingResponse.json | 164 +++++++++++------- .../UserGroups/Responses/GetAllResponse.json | 8 +- 37 files changed, 894 insertions(+), 457 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json index c4c35cdc..367ebae2 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json @@ -3,23 +3,53 @@ { "type": "posts", "id": "201", - "title": "Post 1", - "content": "Post 1 content", - "created": "2015-01-31T14:00:00+00:00", - "links": { - "author": "401", - "comments": [ "101", "102", "103" ], - "tags": [ "301", "302" ] + "attributes": { + "title": "Post 1", + "content": "Post 1 content", + "created": "2015-01-31T14:00:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "102" }, + { "type": "comments", "id": "103" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "301" }, + { "type": "tags", "id": "302" } + ] + } } }, { "type": "comments", "id": "101", - "text": "Comment 1", - "created": "2015-01-31T14:30:00+00:00", - "links": { - "post": "201", - "author": "403" + "attributes": { + "text": "Comment 1", + "created": "2015-01-31T14:30:00+00:00" + }, + "relationships": { + "post": { + "data": { + "type": "posts", + "id": "201" + } + }, + "author": { + "data": { + "type": "users", + "id": "403" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json index 734c670a..e2f476a8 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json @@ -3,23 +3,27 @@ { "type": "users", "id": "6500", - "firstName": "George", - "lastName": "Washington", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "George", + "lastName": "Washington" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "6501", - "firstName": "Abraham", - "lastName": "Lincoln", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Abraham", + "lastName": "Lincoln" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } } ], diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json index b259486d..9c120cd3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": { - "linkage": [ { "type": "users", "id": "403" } ] + "data": [ { "type": "users", "id": "403" } ] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json index 04b74242..b0dfe5c9 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json @@ -3,7 +3,7 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": ["301"] } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json index d6c319c2..f9330854 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json @@ -3,7 +3,9 @@ { "type": "posts", "id": "202", - "title": "New post title" + "attributes": { + "title": "New post title" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json index b8e4a2af..0edb9945 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json @@ -3,7 +3,7 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json index c360c795..09f4b503 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": null + "data": null } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json index 75bc2796..26beb667 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { - "author": { - "linkage": null + "relationships": { + "author": { + "data": null } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json index 692472ca..e53eb574 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": { "type": "tags", "id": "301" } + "data": { "type": "tags", "id": "301" } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json index 2537e5db..25b3ac78 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": "301" + "data": "301" } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json index 3a6b508f..0c5fd684 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": { - "linkage": "403" + "data": "403" } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json index 06a48cf9..e1cb0a33 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json @@ -3,7 +3,7 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": "301" } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json index b2643e19..8fed2e6d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": [] + "data": [] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json index 527ff671..262b053a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": [ + "data": [ { "id": "301", "type": "tags" diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json index 688e21d2..9b867750 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": [ { "type": "tags" } ] + "data": [ { "type": "tags" } ] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json index f89a4f48..24754731 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": [ { "id": "301" } ] + "data": [ { "id": "301" } ] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json index c87cc2dc..3b418f4d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json @@ -3,9 +3,11 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "tags": { - "linkage": [ { "type": "tags", "id": "301" } ] + "data": [ + { "type": "tags", "id": "301" } + ] } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json index 3687916e..52957507 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": { - "linkage": { "type": "users" } + "data": { "type": "users" } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json index f59e54f4..bec1d11b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": { - "linkage": { "id": "403" } + "data": { "id": "403" } } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json index c2b50f31..0d69a337 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json @@ -3,9 +3,9 @@ { "type": "posts", "id": "202", - "links": { + "relationships": { "author": { - "linkage": { + "data": { "type": "users", "id": "403" } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json index aed51861..7e1426ca 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json @@ -3,15 +3,16 @@ { "type": "posts", "id": "205", - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00", - "links": { + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + }, + "relationships": { "author": { - "linkage": { + "data": { "type": "users", "id": "401" - } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json index 5592bfbc..0a9aa7ea 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json @@ -3,49 +3,105 @@ { "type": "posts", "id": "201", - "title": "Post 1", - "content": "Post 1 content", - "created": "2015-01-31T14:00:00+00:00", - "links": { - "author": "401", - "comments": [ "101", "102", "103" ], - "tags": [ "301", "302" ] + "attributes": { + "title": "Post 1", + "content": "Post 1 content", + "created": "2015-01-31T14:00:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "102" }, + { "type": "comments", "id": "103" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "301" }, + { "type": "tags", "id": "302" } + ] + } } }, { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "302" }, + { "type": "tags", "id": "303" } + ] + } } }, { "type": "posts", "id": "203", - "title": "Post 3", - "content": "Post 3 content", - "created": "2015-02-07T11:11:00+00:00", - "links": { - "author": "401", - "comments": [ "105" ], - "tags": [ "303" ] + "attributes": { + "title": "Post 3", + "content": "Post 3 content", + "created": "2015-02-07T11:11:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "303" } + ] + } } }, { "type": "posts", "id": "204", - "title": "Post 4", - "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00", - "links": { - "author": "402", - "comments": [], - "tags": [] + "attributes": { + "title": "Post 4", + "content": "Post 4 content", + "created": "2015-02-08T06:59:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "402" + } + }, + "comments": { "data": [ ] }, + "tags": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json index faa72760..89de980d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json @@ -3,13 +3,29 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "302" }, + { "type": "tags", "id": "303" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json index 95d81f18..9cd5df41 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json @@ -3,13 +3,20 @@ { "type": "posts", "id": "204", - "title": "Post 4", - "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00", - "links": { - "author": "402", - "comments": [], - "tags": [] + "attributes": { + "title": "Post 4", + "content": "Post 4 content", + "created": "2015-02-08T06:59:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "402" + } + }, + "comments": { "data": [ ] }, + "tags": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json index 0ee29b97..d7af8cbb 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json @@ -3,13 +3,29 @@ { "type": "posts", "id": "202", - "title": "New post title", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] + "attributes": { + "title": "New post title", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "302" }, + { "type": "tags", "id": "303" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json index 079784a1..fcd9960a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json @@ -3,13 +3,26 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": null, - "comments": [ "104" ], - "tags": [ "302", "303" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": null + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "302" }, + { "type": "tags", "id": "303" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json index 62ca3775..ca33fa36 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json @@ -3,13 +3,27 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { + "type": "comments", + "id": "104" + } + ] + }, + "tags": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json index 36f44989..c1144df0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json @@ -3,13 +3,29 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "303", "301" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "303" }, + { "type": "tags", "id": "301" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json index f92f7c33..31123b36 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json @@ -3,13 +3,28 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "301" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "301" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json index b843a36d..b8c6340b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json @@ -3,13 +3,29 @@ { "type": "posts", "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "403", - "comments": [ "104" ], - "tags": [ "302", "303" ] + "attributes": { + "title": "Post 2", + "content": "Post 2 content", + "created": "2015-02-05T08:10:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "403" + } + }, + "comments": { + "data": [ + { "type": "comments", "id": "104" } + ] + }, + "tags": { + "data": [ + { "type": "tags", "id": "302" }, + { "type": "tags", "id": "303" } + ] + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json index 9da869ef..b59c5be6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json @@ -3,13 +3,20 @@ { "type": "posts", "id": "205", - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00", - "links": { - "author": "401", - "comments": [], - "tags": [] + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { "data": [ ] }, + "tags": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json index 905ad31e..61a5f729 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json @@ -3,111 +3,155 @@ { "type": "users", "id": "401", - "firstName": "Alice", - "lastName": "Smith", - "links": { - "comments": [ "105" ], - "posts": [ "201", "202", "203" ], - "userGroups": [ ] + "attributes": { + "firstName": "Alice", + "lastName": "Smith" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "201" }, + { "type": "posts", "id": "202" }, + { "type": "posts", "id": "203" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "402", - "firstName": "Bob", - "lastName": "Jones", - "links": { - "comments": [ "102" ], - "posts": [ "204" ], - "userGroups": [ ] + "attributes": { + "firstName": "Bob", + "lastName": "Jones" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "102" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "204" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "403", - "firstName": "Charlie", - "lastName": "Michaels", - "links": { - "comments": [ "101", "103", "104" ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Michaels" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "103" }, + { "type": "comments", "id": "104" } + ] + }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "409", - "firstName": "Charlie", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "406", - "firstName": "Ed", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Ed", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "405", - "firstName": "Michelle", - "lastName": "Johnson", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Michelle", + "lastName": "Johnson" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "408", - "firstName": "Pat", - "lastName": "Morgan", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Pat", + "lastName": "Morgan" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "404", - "firstName": "Richard", - "lastName": "Smith", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Richard", + "lastName": "Smith" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "410", - "firstName": "Sally", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Sally", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "407", - "firstName": "Thomas", - "lastName": "Potter", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Thomas", + "lastName": "Potter" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json index e2f8072e..afd2bdec 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json @@ -3,111 +3,155 @@ { "type": "users", "id": "410", - "firstName": "Sally", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Sally", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "406", - "firstName": "Ed", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Ed", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "409", - "firstName": "Charlie", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "405", - "firstName": "Michelle", - "lastName": "Johnson", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Michelle", + "lastName": "Johnson" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "402", - "firstName": "Bob", - "lastName": "Jones", - "links": { - "comments": [ "102" ], - "posts": [ "204" ], - "userGroups": [ ] + "attributes": { + "firstName": "Bob", + "lastName": "Jones" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "102" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "204" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "403", - "firstName": "Charlie", - "lastName": "Michaels", - "links": { - "comments": [ "101", "103", "104" ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Michaels" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "103" }, + { "type": "comments", "id": "104" } + ] + }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "408", - "firstName": "Pat", - "lastName": "Morgan", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Pat", + "lastName": "Morgan" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "407", - "firstName": "Thomas", - "lastName": "Potter", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Thomas", + "lastName": "Potter" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "404", - "firstName": "Richard", - "lastName": "Smith", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Richard", + "lastName": "Smith" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "401", - "firstName": "Alice", - "lastName": "Smith", - "links": { - "comments": [ "105" ], - "posts": [ "201", "202", "203" ], - "userGroups": [ ] + "attributes": { + "firstName": "Alice", + "lastName": "Smith" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "201" }, + { "type": "posts", "id": "202" }, + { "type": "posts", "id": "203" } + ] + }, + "userGroups": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json index 387256fa..d42356be 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json @@ -3,111 +3,155 @@ { "type": "users", "id": "409", - "firstName": "Charlie", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "406", - "firstName": "Ed", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Ed", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "410", - "firstName": "Sally", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Sally", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "405", - "firstName": "Michelle", - "lastName": "Johnson", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Michelle", + "lastName": "Johnson" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "402", - "firstName": "Bob", - "lastName": "Jones", - "links": { - "comments": [ "102" ], - "posts": [ "204" ], - "userGroups": [ ] + "attributes": { + "firstName": "Bob", + "lastName": "Jones" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "102" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "204" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "403", - "firstName": "Charlie", - "lastName": "Michaels", - "links": { - "comments": [ "101", "103", "104" ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Michaels" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "103" }, + { "type": "comments", "id": "104" } + ] + }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "408", - "firstName": "Pat", - "lastName": "Morgan", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Pat", + "lastName": "Morgan" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "407", - "firstName": "Thomas", - "lastName": "Potter", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Thomas", + "lastName": "Potter" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "401", - "firstName": "Alice", - "lastName": "Smith", - "links": { - "comments": [ "105" ], - "posts": [ "201", "202", "203" ], - "userGroups": [ ] + "attributes": { + "firstName": "Alice", + "lastName": "Smith" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "201" }, + { "type": "posts", "id": "202" }, + { "type": "posts", "id": "203" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "404", - "firstName": "Richard", - "lastName": "Smith", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Richard", + "lastName": "Smith" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json index e0f988b3..1684c854 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json @@ -3,111 +3,155 @@ { "type": "users", "id": "404", - "firstName": "Richard", - "lastName": "Smith", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Richard", + "lastName": "Smith" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "401", - "firstName": "Alice", - "lastName": "Smith", - "links": { - "comments": [ "105" ], - "posts": [ "201", "202", "203" ], - "userGroups": [ ] + "attributes": { + "firstName": "Alice", + "lastName": "Smith" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "201" }, + { "type": "posts", "id": "202" }, + { "type": "posts", "id": "203" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "407", - "firstName": "Thomas", - "lastName": "Potter", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Thomas", + "lastName": "Potter" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "408", - "firstName": "Pat", - "lastName": "Morgan", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Pat", + "lastName": "Morgan" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "403", - "firstName": "Charlie", - "lastName": "Michaels", - "links": { - "comments": [ "101", "103", "104" ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Michaels" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "103" }, + { "type": "comments", "id": "104" } + ] + }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "402", - "firstName": "Bob", - "lastName": "Jones", - "links": { - "comments": [ "102" ], - "posts": [ "204" ], - "userGroups": [ ] + "attributes": { + "firstName": "Bob", + "lastName": "Jones" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "102" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "204" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "405", - "firstName": "Michelle", - "lastName": "Johnson", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Michelle", + "lastName": "Johnson" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "410", - "firstName": "Sally", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Sally", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "406", - "firstName": "Ed", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Ed", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "409", - "firstName": "Charlie", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json index 6a80b703..0c5a42f8 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json @@ -3,111 +3,155 @@ { "type": "users", "id": "407", - "firstName": "Thomas", - "lastName": "Potter", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Thomas", + "lastName": "Potter" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "410", - "firstName": "Sally", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Sally", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "404", - "firstName": "Richard", - "lastName": "Smith", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Richard", + "lastName": "Smith" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "408", - "firstName": "Pat", - "lastName": "Morgan", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Pat", + "lastName": "Morgan" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "405", - "firstName": "Michelle", - "lastName": "Johnson", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Michelle", + "lastName": "Johnson" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "406", - "firstName": "Ed", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Ed", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "403", - "firstName": "Charlie", - "lastName": "Michaels", - "links": { - "comments": [ "101", "103", "104" ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Michaels" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "101" }, + { "type": "comments", "id": "103" }, + { "type": "comments", "id": "104" } + ] + }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "409", - "firstName": "Charlie", - "lastName": "Burns", - "links": { - "comments": [ ], - "posts": [ ], - "userGroups": [ ] + "attributes": { + "firstName": "Charlie", + "lastName": "Burns" + }, + "relationships": { + "comments": { "data": [ ] }, + "posts": { "data": [ ] }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "402", - "firstName": "Bob", - "lastName": "Jones", - "links": { - "comments": [ "102" ], - "posts": [ "204" ], - "userGroups": [ ] + "attributes": { + "firstName": "Bob", + "lastName": "Jones" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "102" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "204" } + ] + }, + "userGroups": { "data": [ ] } } }, { "type": "users", "id": "401", - "firstName": "Alice", - "lastName": "Smith", - "links": { - "comments": [ "105" ], - "posts": [ "201", "202", "203" ], - "userGroups": [ ] + "attributes": { + "firstName": "Alice", + "lastName": "Smith" + }, + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "105" } + ] + }, + "posts": { + "data": [ + { "type": "posts", "id": "201" }, + { "type": "posts", "id": "202" }, + { "type": "posts", "id": "203" } + ] + }, + "userGroups": { "data": [ ] } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json index e707faa8..239674df 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json @@ -3,9 +3,11 @@ { "type": "user-groups", "id": "501", - "name": "Admin users", - "links": { - "users": [ ] + "attributes": { + "name": "Admin users" + }, + "relationships": { + "users": { "data": [ ] } } } ] From dc5cc3c682f5f774bb723487a8c7d29dbe690873 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 19 Jun 2015 14:57:59 -0400 Subject: [PATCH 080/186] refactor writing resource identifier objects --- JSONAPI/Json/JsonApiFormatter.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 4c9e69ef..2b0a7186 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -336,10 +336,8 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js Lazy objId = new Lazy(() => GetIdFor(propertyValue)); - // Write the data object - writer.WriteStartObject(); - WriteTypeAndId(writer, prop.PropertyType, propertyValue); - writer.WriteEndObject(); + // Write the resource identifier object + WriteResourceIdentifierObject(writer, prop.PropertyType, propertyValue); if (iip) if (aggregator != null) @@ -962,6 +960,13 @@ protected string GetIdFor(object obj) return GetValueForIdProperty(idprop, obj); } + private void WriteResourceIdentifierObject(JsonWriter writer, Type propertyType, object propertyValue) + { + writer.WriteStartObject(); + WriteTypeAndId(writer, propertyType, propertyValue); + writer.WriteEndObject(); + } + private void WriteTypeAndId(JsonWriter writer, Type propertyType, object propertyValue) { writer.WritePropertyName("type"); @@ -977,9 +982,7 @@ private void WriteIdsArrayJson(Newtonsoft.Json.JsonWriter writer, IEnumerable Date: Sun, 28 Jun 2015 18:46:52 -0400 Subject: [PATCH 081/186] rewrite JSONAPI.NET to be compatible with JSON API 1.0 --- JSONAPI.Autofac/JSONAPI.Autofac.csproj | 90 ++ .../JsonApiAutofacConfiguration.cs | 106 ++ JSONAPI.Autofac/Properties/AssemblyInfo.cs | 36 + JSONAPI.Autofac/app.config | 15 + JSONAPI.Autofac/packages.config | 8 + .../Controllers/CitiesController.cs | 42 + .../Controllers/CommentsController.cs | 17 +- .../Controllers/PostsController.cs | 17 +- .../Controllers/PresidentsController.cs | 29 +- .../Controllers/SamplesController.cs | 91 ++ .../Controllers/TagsController.cs | 17 +- .../Controllers/TreesController.cs | 13 + .../Controllers/UserGroupsController.cs | 17 +- .../Controllers/UsersController.cs | 19 +- ...PI.EntityFramework.Tests.TestWebApp.csproj | 10 + .../Models/City.cs | 16 + .../Models/Sample.cs | 52 + .../Models/State.cs | 15 + .../Startup.cs | 66 +- .../Acceptance/AcceptanceTestsBase.cs | 26 +- .../Acceptance/AttributeSerializationTests.cs | 22 + .../Acceptance/ErrorsTests.cs | 34 + ..._of_various_types_serialize_correctly.json | 89 ++ .../Controller_action_throws_exception.json | 14 + .../Errors/Controller_does_not_exist.json | 10 + .../Responses/GetSearchResultsResponse.json | 47 +- .../Responses/GetReturnsIPayloadResponse.json | 33 - ...et_returns_IResourceCollectionPayload.json | 12 + .../PatchWithArrayForToOneLinkageRequest.json | 16 +- ...atchWithArrayRelationshipValueRequest.json | 14 +- .../PatchWithAttributeUpdateRequest.json | 14 +- .../PatchWithMissingToManyLinkageRequest.json | 14 +- .../PatchWithMissingToOneLinkageRequest.json | 10 + .../PatchWithNullForToManyLinkageRequest.json | 16 +- .../PatchWithNullToOneUpdateRequest.json | 16 +- ...atchWithObjectForToManyLinkageRequest.json | 16 +- ...atchWithStringForToManyLinkageRequest.json | 16 +- ...PatchWithStringForToOneLinkageRequest.json | 16 +- ...tchWithStringRelationshipValueRequest.json | 14 +- ...chWithToManyEmptyLinkageUpdateRequest.json | 16 +- ...ithToManyHomogeneousDataUpdateRequest.json | 34 +- ...thToManyLinkageObjectMissingIdRequest.json | 16 +- ...ToManyLinkageObjectMissingTypeRequest.json | 16 +- .../PatchWithToManyUpdateRequest.json | 20 +- ...ithToOneLinkageObjectMissingIdRequest.json | 16 +- ...hToOneLinkageObjectMissingTypeRequest.json | 16 +- .../Requests/PatchWithToOneUpdateRequest.json | 20 +- .../Fixtures/Posts/Requests/PostRequest.json | 30 +- .../Posts/Responses/GetAllResponse.json | 100 +- .../Posts/Responses/GetByIdResponse.json | 51 +- .../Responses/GetWithFilterResponse.json | 24 +- ...PatchWithArrayForToOneLinkageResponse.json | 11 +- ...tchWithArrayRelationshipValueResponse.json | 11 +- .../PatchWithAttributeUpdateResponse.json | 51 +- ...PatchWithMissingToManyLinkageResponse.json | 11 +- .../PatchWithMissingToOneLinkageResponse.json | 13 + ...PatchWithNullForToManyLinkageResponse.json | 11 +- .../PatchWithNullToOneUpdateResponse.json | 48 +- ...tchWithObjectForToManyLinkageResponse.json | 11 +- ...tchWithStringForToManyLinkageResponse.json | 11 +- ...atchWithStringForToOneLinkageResponse.json | 11 +- ...chWithStringRelationshipValueResponse.json | 11 +- ...hWithToManyEmptyLinkageUpdateResponse.json | 51 +- ...thToManyHomogeneousDataUpdateResponse.json | 51 +- ...hToManyLinkageObjectMissingIdResponse.json | 11 +- ...oManyLinkageObjectMissingTypeResponse.json | 11 +- .../PatchWithToManyUpdateResponse.json | 50 +- ...thToOneLinkageObjectMissingIdResponse.json | 11 +- ...ToOneLinkageObjectMissingTypeResponse.json | 11 +- .../PatchWithToOneUpdateResponse.json | 51 +- .../Posts/Responses/PostResponse.json | 44 +- .../Responses/GetSortedAscendingResponse.json | 256 +++-- ...ortedByColumnMissingDirectionResponse.json | 11 +- .../GetSortedByMixedDirectionResponse.json | 256 +++-- .../GetSortedByMultipleAscendingResponse.json | 256 +++-- ...GetSortedByMultipleDescendingResponse.json | 256 +++-- .../GetSortedBySameColumnTwiceResponse.json | 11 +- .../GetSortedByUnknownColumnResponse.json | 11 +- .../GetSortedDescendingResponse.json | 256 +++-- .../UserGroups/Responses/GetAllResponse.json | 7 +- .../Acceptance/PayloadTests.cs | 4 +- .../Acceptance/PostsTests.cs | 57 +- .../Acceptance/SortingTests.cs | 20 +- ...EntityFrameworkPayloadMaterializerTests.cs | 59 ++ .../EntityFrameworkMaterializerTests.cs | 2 +- .../JSONAPI.EntityFramework.Tests.csproj | 14 +- ...lePayloadBuilderConfigurationExtensions.cs | 23 - ...=> EntityFrameworkPluralizationService.cs} | 47 +- ...tityFrameworkResourceObjectMaterializer.cs | 199 ++++ JSONAPI.EntityFramework/Http/ApiController.cs | 101 -- .../EntityFrameworkPayloadMaterializer.cs | 95 ++ .../JSONAPI.EntityFramework.csproj | 6 +- .../DefaultFilteringTransformerTests.cs | 182 ++-- .../DefaultPaginationTransformerTests.cs | 36 +- .../DefaultSortingTransformerTests.cs | 25 +- .../FallbackPayloadBuilderAttributeTests.cs | 173 +++ .../Core/DefaultNamingConventionsTests.cs | 68 ++ JSONAPI.Tests/Core/MetadataManagerTests.cs | 39 +- JSONAPI.Tests/Core/ModelManagerTests.cs | 426 -------- .../Core/ResourceTypeRegistryTests.cs | 902 ++++++++++++++++ .../Data/AttributeSerializationTest.json | 82 -- JSONAPI.Tests/JSONAPI.Tests.csproj | 82 +- .../Json/ErrorPayloadSerializerTests.cs | 48 + JSONAPI.Tests/Json/ErrorSerializerTests.cs | 87 +- .../Serialize_ErrorPayload.json | 4 + ...alize_error_with_all_possible_members.json | 17 + .../Serialize_error_with_only_id.json | 3 + .../Serialize_ErrorPayload.json | 1 + .../JsonApiFormatter/Serialize_HttpError.json | 1 + .../Serialize_ResourceCollectionPayload.json | 1 + .../Serialize_SingleResourcePayload.json | 1 + .../Writes_error_for_anything_else.json | 7 + .../Serialize_link_with_metadata.json | 4 + .../Serialize_link_without_metadata.json | 1 + .../Deserialize_metadata.json | 6 + .../Deserialize_null_metadata.json | 1 + .../Serialize_metadata.json | 7 + .../Serialize_null_metadata.json | 1 + .../Deserialize_relationship_object.json | 4 + ...elationship_with_all_possible_members.json | 8 + ...ialize_relationship_with_linkage_only.json | 3 + ...Serialize_relationship_with_meta_only.json | 3 + ...e_relationship_with_related_link_only.json | 5 + ...nship_with_self_link_and_related_link.json | 6 + ...lize_relationship_with_self_link_only.json | 5 + .../Deserialize_empty_payload.json | 3 + .../Deserialize_payload_with_metadata.json | 3 + ...Deserialize_payload_with_primary_data.json | 3 + ...rimary_data_and_unknown_top_level_key.json | 6 + ...ctionPayload_for_all_possible_members.json | 12 + ...llectionPayload_for_primary_data_only.json | 6 + ...ad_for_primary_data_only_and_metadata.json | 7 + .../Deserialize_fails_on_integer.json | 1 + .../Deserialize_fails_on_string.json | 1 + .../Deserialize_null_to_one_linkage.json | 1 + .../Deserialize_to_many_linkage.json | 10 + .../Deserialize_to_one_linkage.json | 4 + .../Serialize_ToOneResourceLinkage.json | 4 + .../Serialize_linkage.json | 1 + .../Serialize_null_linkage.json | 1 + .../Deserialize_resource_object.json | 16 + ...or_resource_with_all_possible_members.json | 17 + ...ceObject_for_resource_with_attributes.json | 9 + ...esourceObject_for_resource_with_links.json | 7 + ...urceObject_for_resource_with_metadata.json | 5 + ...resource_with_only_null_relationships.json | 4 + ...bject_for_resource_with_relationships.json | 8 + ...g_integer_greater_than_int64_maxvalue.json | 8 + ...bject_for_resource_without_attributes.json | 4 + .../Deserialize_null_payload.json | 3 + .../Deserialize_payload_with_resource.json | 3 + ...ourcePayload_for_all_possible_members.json | 9 + ...Payload_for_primary_data_and_metadata.json | 4 + ...ResourcePayload_for_primary_data_only.json | 3 + JSONAPI.Tests/Json/JsonApiFormatterTests.cs | 736 +++++++++++++ .../Json/JsonApiMediaFormatterTests.cs | 624 ----------- .../Json/JsonApiSerializerTestsBase.cs | 61 ++ JSONAPI.Tests/Json/LinkSerializerTests.cs | 42 + JSONAPI.Tests/Json/LinkTemplateTests.cs | 68 -- JSONAPI.Tests/Json/MetadataSerializerTests.cs | 95 ++ .../Json/RelationshipObjectSerializerTests.cs | 231 ++++ ...esourceCollectionPayloadSerializerTests.cs | 252 +++++ .../Json/ResourceLinkageSerializerTests.cs | 126 +++ .../Json/ResourceObjectSerializerTests.cs | 275 +++++ .../SingleResourcePayloadSerializerTests.cs | 164 +++ .../Models/{Sample.cs => AttributeGrabBag.cs} | 24 +- JSONAPI.Tests/Models/Author.cs | 8 +- JSONAPI.Tests/Models/Comment.cs | 9 +- JSONAPI.Tests/Models/Post.cs | 14 +- .../Builders/ErrorPayloadBuilderTests.cs | 162 +++ .../Builders/FallbackPayloadBuilderTests.cs | 128 +++ .../RegistryDrivenPayloadBuilderTests.cs | 122 +++ ...DrivenSingleResourcePayloadBuilderTests.cs | 266 +++++ .../Payload/DefaultLinkConventionsTests.cs | 190 ++++ .../Payload/ToManyResourceLinkageTests.cs | 58 + .../Payload/ToOneResourceLinkageTests.cs | 42 + JSONAPI.Tests/TestHelpers.cs | 40 + .../Controllers/TodosController.cs | 7 +- .../JSONAPI.TodoMVC.API.csproj | 7 + JSONAPI.TodoMVC.API/Startup.cs | 22 +- JSONAPI.TodoMVC.API/packages.config | 1 + JSONAPI.sln | 20 +- .../DefaultFilteringTransformer.cs | 39 +- .../DefaultPaginationTransformer.cs | 48 +- .../DefaultSortingTransformer.cs | 46 +- .../FallbackPayloadBuilderAttribute.cs | 93 ++ .../JsonApiExceptionFilterAttribute.cs | 43 + .../JsonApiQueryableAttribute.cs | 85 -- .../QueryableTransformException.cs | 13 - JSONAPI/Attributes/IncludeInPayload.cs | 15 - JSONAPI/Attributes/LinkTemplate.cs | 15 - .../Attributes/RelatedResourceLinkTemplate.cs | 24 + .../Attributes/RelationshipLinkTemplate.cs | 24 + JSONAPI/Attributes/SerializeAs.cs | 18 - .../Attributes/SerializeAsComplexAttribute.cs | 12 + .../SerializeStringAsRawJsonAttribute.cs | 9 - ...ultQueryablePayloadBuilderConfiguration.cs | 121 --- JSONAPI/Core/IAttributeValueConverter.cs | 317 ++++++ JSONAPI/Core/IModelManager.cs | 83 -- JSONAPI/Core/IPluralizationService.cs | 3 - JSONAPI/Core/IResourceTypeRegistration.cs | 59 ++ JSONAPI/Core/IResourceTypeRegistry.cs | 33 + JSONAPI/Core/JsonApiConfiguration.cs | 84 +- JSONAPI/Core/JsonApiHttpConfiguration.cs | 51 + JSONAPI/Core/ModelManager.cs | 340 ------ JSONAPI/Core/ModelProperty.cs | 68 -- JSONAPI/Core/ResourceTypeField.cs | 126 +++ JSONAPI/Core/ResourceTypeRegistry.cs | 360 +++++++ .../Core/TypeRegistrationNotFoundException.cs | 28 + JSONAPI/Http/ApiController.cs | 126 --- JSONAPI/Http/IPayloadMaterializer.cs | 50 + JSONAPI/Http/JsonApiController.cs | 83 ++ JSONAPI/JSONAPI.csproj | 85 +- JSONAPI/Json/BasicMetadata.cs | 22 + JSONAPI/Json/DeserializationException.cs | 34 + JSONAPI/Json/ErrorPayloadSerializer.cs | 55 + JSONAPI/Json/ErrorSerializer.cs | 130 ++- JSONAPI/Json/GuidErrorIdProvider.cs | 13 - JSONAPI/Json/IErrorIdProvider.cs | 9 - JSONAPI/Json/IErrorPayloadSerializer.cs | 11 + JSONAPI/Json/IErrorSerializer.cs | 13 +- JSONAPI/Json/IJsonApiSerializer.cs | 29 + JSONAPI/Json/ILinkSerializer.cs | 11 + JSONAPI/Json/IMetadataSerializer.cs | 11 + JSONAPI/Json/IRelationshipObjectSerializer.cs | 11 + .../IResourceCollectionPayloadSerializer.cs | 11 + JSONAPI/Json/IResourceLinkageSerializer.cs | 11 + JSONAPI/Json/IResourceObjectSerializer.cs | 11 + .../Json/ISingleResourcePayloadSerializer.cs | 11 + JSONAPI/Json/JsonApiFormatter.cs | 994 ++---------------- JSONAPI/Json/LinkSerializer.cs | 50 + JSONAPI/Json/MetadataSerializer.cs | 48 + JSONAPI/Json/RelationshipObjectSerializer.cs | 108 ++ .../ResourceCollectionPayloadSerializer.cs | 127 +++ JSONAPI/Json/ResourceLinkageSerializer.cs | 84 ++ JSONAPI/Json/ResourceObjectSerializer.cs | 208 ++++ .../Json/SingleResourcePayloadSerializer.cs | 105 ++ ...ryableResourceCollectionPayloadBuilder.cs} | 26 +- .../Payload/Builders/ErrorPayloadBuilder.cs | 153 +++ .../Builders/FallbackPayloadBuilder.cs | 108 ++ .../Payload/Builders/IErrorPayloadBuilder.cs | 27 + .../Builders/IFallbackPayloadBuilder.cs | 22 + ...ryableResourceCollectionPayloadBuilder.cs} | 10 +- .../IResourceCollectionPayloadBuilder.cs | 21 + .../Builders/ISingleResourcePayloadBuilder.cs | 18 + JSONAPI/Payload/Builders/JsonApiException.cs | 47 + .../Builders/RegistryDrivenPayloadBuilder.cs | 154 +++ ...yDrivenResourceCollectionPayloadBuilder.cs | 34 + ...istryDrivenSingleResourcePayloadBuilder.cs | 32 + JSONAPI/Payload/DefaultLinkConventions.cs | 98 ++ JSONAPI/Payload/Error.cs | 20 + JSONAPI/Payload/ErrorPayload.cs | 22 + JSONAPI/Payload/ExceptionErrorMetadata.cs | 39 + JSONAPI/Payload/IError.cs | 55 + JSONAPI/Payload/IErrorPayload.cs | 13 + JSONAPI/Payload/IJsonApiPayload.cs | 13 + JSONAPI/Payload/ILink.cs | 39 + JSONAPI/Payload/ILinkConventions.cs | 31 + JSONAPI/Payload/IMetadata.cs | 15 + JSONAPI/Payload/IPayload.cs | 21 - JSONAPI/Payload/IRelationshipObject.cs | 28 + JSONAPI/Payload/IResourceCollectionPayload.cs | 18 + JSONAPI/Payload/IResourceIdentifier.cs | 38 + JSONAPI/Payload/IResourceLinkage.cs | 15 + JSONAPI/Payload/IResourceObject.cs | 41 + JSONAPI/Payload/ISingleResourcePayload.cs | 18 + JSONAPI/Payload/Payload.cs | 13 - JSONAPI/Payload/PayloadReaderException.cs | 20 + JSONAPI/Payload/RelationshipObject.cs | 43 + JSONAPI/Payload/ResourceCollectionPayload.cs | 25 + JSONAPI/Payload/ResourceObject.cs | 32 + JSONAPI/Payload/SingleResourcePayload.cs | 30 + JSONAPI/Payload/ToManyResourceLinkage.cs | 34 + JSONAPI/Payload/ToOneResourceLinkage.cs | 27 + 274 files changed, 11436 insertions(+), 4700 deletions(-) create mode 100644 JSONAPI.Autofac/JSONAPI.Autofac.csproj create mode 100644 JSONAPI.Autofac/JsonApiAutofacConfiguration.cs create mode 100644 JSONAPI.Autofac/Properties/AssemblyInfo.cs create mode 100644 JSONAPI.Autofac/app.config create mode 100644 JSONAPI.Autofac/packages.config create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json create mode 100644 JSONAPI.EntityFramework.Tests/EntityFrameworkPayloadMaterializerTests.cs rename JSONAPI.EntityFramework.Tests/{ => Http}/EntityFrameworkMaterializerTests.cs (97%) delete mode 100644 JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs rename JSONAPI.EntityFramework/{PluralizationService.cs => EntityFrameworkPluralizationService.cs} (57%) create mode 100644 JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs delete mode 100644 JSONAPI.EntityFramework/Http/ApiController.cs create mode 100644 JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs create mode 100644 JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs create mode 100644 JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs delete mode 100644 JSONAPI.Tests/Core/ModelManagerTests.cs create mode 100644 JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs delete mode 100644 JSONAPI.Tests/Data/AttributeSerializationTest.json create mode 100644 JSONAPI.Tests/Json/ErrorPayloadSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/Fixtures/ErrorPayloadSerializer/Serialize_ErrorPayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_all_possible_members.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_only_id.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Writes_error_for_anything_else.json create mode 100644 JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_with_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_without_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_null_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_null_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Deserialize_relationship_object.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_all_possible_members.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_linkage_only.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_meta_only.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_related_link_only.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_and_related_link.json create mode 100644 JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_only.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_empty_payload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data_and_unknown_top_level_key.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_all_possible_members.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_integer.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_string.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_null_to_one_linkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_many_linkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_one_linkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_ToOneResourceLinkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_linkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_null_linkage.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Deserialize_resource_object.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_all_possible_members.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_attributes.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_links.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_only_null_relationships.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_relationships.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_without_attributes.json create mode 100644 JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_null_payload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_payload_with_resource.json create mode 100644 JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_all_possible_members.json create mode 100644 JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_and_metadata.json create mode 100644 JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_only.json create mode 100644 JSONAPI.Tests/Json/JsonApiFormatterTests.cs delete mode 100644 JSONAPI.Tests/Json/JsonApiMediaFormatterTests.cs create mode 100644 JSONAPI.Tests/Json/JsonApiSerializerTestsBase.cs create mode 100644 JSONAPI.Tests/Json/LinkSerializerTests.cs delete mode 100644 JSONAPI.Tests/Json/LinkTemplateTests.cs create mode 100644 JSONAPI.Tests/Json/MetadataSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/RelationshipObjectSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/ResourceCollectionPayloadSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/ResourceLinkageSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/ResourceObjectSerializerTests.cs create mode 100644 JSONAPI.Tests/Json/SingleResourcePayloadSerializerTests.cs rename JSONAPI.Tests/Models/{Sample.cs => AttributeGrabBag.cs} (71%) create mode 100644 JSONAPI.Tests/Payload/Builders/ErrorPayloadBuilderTests.cs create mode 100644 JSONAPI.Tests/Payload/Builders/FallbackPayloadBuilderTests.cs create mode 100644 JSONAPI.Tests/Payload/Builders/RegistryDrivenPayloadBuilderTests.cs create mode 100644 JSONAPI.Tests/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilderTests.cs create mode 100644 JSONAPI.Tests/Payload/DefaultLinkConventionsTests.cs create mode 100644 JSONAPI.Tests/Payload/ToManyResourceLinkageTests.cs create mode 100644 JSONAPI.Tests/Payload/ToOneResourceLinkageTests.cs create mode 100644 JSONAPI.Tests/TestHelpers.cs create mode 100644 JSONAPI/ActionFilters/FallbackPayloadBuilderAttribute.cs create mode 100644 JSONAPI/ActionFilters/JsonApiExceptionFilterAttribute.cs delete mode 100644 JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs delete mode 100644 JSONAPI/ActionFilters/QueryableTransformException.cs delete mode 100644 JSONAPI/Attributes/IncludeInPayload.cs delete mode 100644 JSONAPI/Attributes/LinkTemplate.cs create mode 100644 JSONAPI/Attributes/RelatedResourceLinkTemplate.cs create mode 100644 JSONAPI/Attributes/RelationshipLinkTemplate.cs delete mode 100644 JSONAPI/Attributes/SerializeAs.cs create mode 100644 JSONAPI/Attributes/SerializeAsComplexAttribute.cs delete mode 100644 JSONAPI/Attributes/SerializeStringAsRawJsonAttribute.cs delete mode 100644 JSONAPI/Core/DefaultQueryablePayloadBuilderConfiguration.cs create mode 100644 JSONAPI/Core/IAttributeValueConverter.cs delete mode 100644 JSONAPI/Core/IModelManager.cs create mode 100644 JSONAPI/Core/IResourceTypeRegistration.cs create mode 100644 JSONAPI/Core/IResourceTypeRegistry.cs create mode 100644 JSONAPI/Core/JsonApiHttpConfiguration.cs delete mode 100644 JSONAPI/Core/ModelManager.cs delete mode 100644 JSONAPI/Core/ModelProperty.cs create mode 100644 JSONAPI/Core/ResourceTypeField.cs create mode 100644 JSONAPI/Core/ResourceTypeRegistry.cs create mode 100644 JSONAPI/Core/TypeRegistrationNotFoundException.cs delete mode 100644 JSONAPI/Http/ApiController.cs create mode 100644 JSONAPI/Http/IPayloadMaterializer.cs create mode 100644 JSONAPI/Http/JsonApiController.cs create mode 100644 JSONAPI/Json/BasicMetadata.cs create mode 100644 JSONAPI/Json/DeserializationException.cs create mode 100644 JSONAPI/Json/ErrorPayloadSerializer.cs delete mode 100644 JSONAPI/Json/GuidErrorIdProvider.cs delete mode 100644 JSONAPI/Json/IErrorIdProvider.cs create mode 100644 JSONAPI/Json/IErrorPayloadSerializer.cs create mode 100644 JSONAPI/Json/IJsonApiSerializer.cs create mode 100644 JSONAPI/Json/ILinkSerializer.cs create mode 100644 JSONAPI/Json/IMetadataSerializer.cs create mode 100644 JSONAPI/Json/IRelationshipObjectSerializer.cs create mode 100644 JSONAPI/Json/IResourceCollectionPayloadSerializer.cs create mode 100644 JSONAPI/Json/IResourceLinkageSerializer.cs create mode 100644 JSONAPI/Json/IResourceObjectSerializer.cs create mode 100644 JSONAPI/Json/ISingleResourcePayloadSerializer.cs create mode 100644 JSONAPI/Json/LinkSerializer.cs create mode 100644 JSONAPI/Json/MetadataSerializer.cs create mode 100644 JSONAPI/Json/RelationshipObjectSerializer.cs create mode 100644 JSONAPI/Json/ResourceCollectionPayloadSerializer.cs create mode 100644 JSONAPI/Json/ResourceLinkageSerializer.cs create mode 100644 JSONAPI/Json/ResourceObjectSerializer.cs create mode 100644 JSONAPI/Json/SingleResourcePayloadSerializer.cs rename JSONAPI/Payload/{DefaultQueryablePayloadBuilder.cs => Builders/DefaultQueryableResourceCollectionPayloadBuilder.cs} (63%) create mode 100644 JSONAPI/Payload/Builders/ErrorPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/FallbackPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/IErrorPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/IFallbackPayloadBuilder.cs rename JSONAPI/Payload/{IQueryablePayloadBuilder.cs => Builders/IQueryableResourceCollectionPayloadBuilder.cs} (51%) create mode 100644 JSONAPI/Payload/Builders/IResourceCollectionPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/ISingleResourcePayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/JsonApiException.cs create mode 100644 JSONAPI/Payload/Builders/RegistryDrivenPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/RegistryDrivenResourceCollectionPayloadBuilder.cs create mode 100644 JSONAPI/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilder.cs create mode 100644 JSONAPI/Payload/DefaultLinkConventions.cs create mode 100644 JSONAPI/Payload/Error.cs create mode 100644 JSONAPI/Payload/ErrorPayload.cs create mode 100644 JSONAPI/Payload/ExceptionErrorMetadata.cs create mode 100644 JSONAPI/Payload/IError.cs create mode 100644 JSONAPI/Payload/IErrorPayload.cs create mode 100644 JSONAPI/Payload/IJsonApiPayload.cs create mode 100644 JSONAPI/Payload/ILink.cs create mode 100644 JSONAPI/Payload/ILinkConventions.cs create mode 100644 JSONAPI/Payload/IMetadata.cs delete mode 100644 JSONAPI/Payload/IPayload.cs create mode 100644 JSONAPI/Payload/IRelationshipObject.cs create mode 100644 JSONAPI/Payload/IResourceCollectionPayload.cs create mode 100644 JSONAPI/Payload/IResourceIdentifier.cs create mode 100644 JSONAPI/Payload/IResourceLinkage.cs create mode 100644 JSONAPI/Payload/IResourceObject.cs create mode 100644 JSONAPI/Payload/ISingleResourcePayload.cs delete mode 100644 JSONAPI/Payload/Payload.cs create mode 100644 JSONAPI/Payload/PayloadReaderException.cs create mode 100644 JSONAPI/Payload/RelationshipObject.cs create mode 100644 JSONAPI/Payload/ResourceCollectionPayload.cs create mode 100644 JSONAPI/Payload/ResourceObject.cs create mode 100644 JSONAPI/Payload/SingleResourcePayload.cs create mode 100644 JSONAPI/Payload/ToManyResourceLinkage.cs create mode 100644 JSONAPI/Payload/ToOneResourceLinkage.cs diff --git a/JSONAPI.Autofac/JSONAPI.Autofac.csproj b/JSONAPI.Autofac/JSONAPI.Autofac.csproj new file mode 100644 index 00000000..2f0b3d78 --- /dev/null +++ b/JSONAPI.Autofac/JSONAPI.Autofac.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {AF7861F3-550B-4F70-A33E-1E5F48D39333} + Library + Properties + JSONAPI.Autofac + JSONAPI.Autofac + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + + + ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll + + + ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + + + + + False + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + + + False + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + + + + + + + + + + + + + + + + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs b/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs new file mode 100644 index 00000000..78e6e515 --- /dev/null +++ b/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Web.Http; +using Autofac; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using JSONAPI.Json; +using JSONAPI.Payload; +using JSONAPI.Payload.Builders; + +namespace JSONAPI.Autofac +{ + public class JsonApiAutofacConfiguration + { + private readonly INamingConventions _namingConventions; + private readonly List _typesToRegister; + private readonly List> _containerBuildingActions; + private ILinkConventions _linkConventions; + + public JsonApiAutofacConfiguration(INamingConventions namingConventions) + { + if (namingConventions == null) throw new ArgumentNullException("namingConventions"); + + _namingConventions = namingConventions; + _typesToRegister = new List(); + _containerBuildingActions = new List>(); + } + + public void RegisterResourceType(Type resourceType) + { + _typesToRegister.Add(resourceType); + } + + public void OverrideLinkConventions(ILinkConventions linkConventions) + { + _linkConventions = linkConventions; + } + + public void OnContainerBuilding(Action action) + { + _containerBuildingActions.Add(action); + } + + public IContainer Apply(HttpConfiguration httpConfig) + { + var container = GetContainer(); + var jsonApiConfiguration = container.Resolve(); + jsonApiConfiguration.Apply(httpConfig); + return container; + } + + private IContainer GetContainer() + { + var builder = new ContainerBuilder(); + + // Registry + builder.Register(c => _namingConventions).As().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.Register(c => + { + var registry = c.Resolve(); + foreach (var type in _typesToRegister) + registry.RegisterResourceType(type); + return registry; + }).As().SingleInstance(); + + // Serialization + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + // Queryable transforms + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + // Payload building + var linkConventions = _linkConventions ?? new DefaultLinkConventions(); + builder.Register(c => linkConventions).As().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().As(); + + builder.RegisterType(); + + foreach (var containerBuildingAction in _containerBuildingActions) + { + containerBuildingAction(builder); + } + + return builder.Build(); + } + } +} diff --git a/JSONAPI.Autofac/Properties/AssemblyInfo.cs b/JSONAPI.Autofac/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..9804046c --- /dev/null +++ b/JSONAPI.Autofac/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("868ac502-6972-4889-bcbd-9d6160258667")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac/app.config b/JSONAPI.Autofac/app.config new file mode 100644 index 00000000..b678ca2c --- /dev/null +++ b/JSONAPI.Autofac/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac/packages.config b/JSONAPI.Autofac/packages.config new file mode 100644 index 00000000..dc5afbb9 --- /dev/null +++ b/JSONAPI.Autofac/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs new file mode 100644 index 00000000..d94a3232 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs @@ -0,0 +1,42 @@ +using System.Web.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class CitiesController : ApiController + { + public IHttpActionResult Get(string id) + { + City city; + if (id == "9000") + { + city = + new City + { + Id = "9000", + Name = "Seattle", + State = new State + { + Id = "4000", + Name = "Washington" + } + }; + } + else if (id == "9001") + { + city = + new City + { + Id = "9001", + Name = "Tacoma" + }; + } + else + { + return NotFound(); + } + + return Ok(city); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs index 3aa108ef..679ac940 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs @@ -1,21 +1,12 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { - public class CommentsController : ApiController + public class CommentsController : JsonApiController { - protected readonly TestDbContext DbContext; - - public CommentsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() + public CommentsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) { - return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs index 728266ad..a45bbc8a 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs @@ -1,21 +1,12 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { - public class PostsController : ApiController + public class PostsController : JsonApiController { - protected readonly TestDbContext DbContext; - - public PostsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() + public PostsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) { - return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs index 483d2cb1..1d6a7348 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Web; using System.Web.Http; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; @@ -11,29 +12,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class PresidentsController : ApiController { - public class MyArrayPayload : IPayload - { - private readonly T[] _array; - - public MyArrayPayload(T[] array) - { - _array = array; - } - - public object PrimaryData { get { return _array; } } - - public JObject Metadata - { - get - { - var obj = new JObject(); - obj["count"] = _array.Length; - return obj; - } - } - } - - // This endpoint exists to demonstrate returning IPayload + // This endpoint exists to demonstrate returning IResourceCollectionPayload [Route("presidents")] public IHttpActionResult GetPresidents() { @@ -52,8 +31,10 @@ public IHttpActionResult GetPresidents() LastName = "Lincoln" } }; + + var userResources = users.Select(u => (IResourceObject)new ResourceObject("users", u.Id)).ToArray(); - var payload = new MyArrayPayload(users); + var payload = new ResourceCollectionPayload(userResources, null, null); return Ok(payload); } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs new file mode 100644 index 00000000..5e2d8448 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs @@ -0,0 +1,91 @@ +using System; +using System.Web.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class SamplesController : ApiController + { + public IHttpActionResult GetSamples() + { + var s1 = new Sample + { + Id = "1", + BooleanField = false, + NullableBooleanField = false, + SbyteField = default(SByte), + NullableSbyteField = null, + ByteField = default(Byte), + NullableByteField = null, + Int16Field = default(Int16), + NullableInt16Field = null, + Uint16Field = default(UInt16), + NullableUint16Field = null, + Int32Field = default(Int32), + NullableInt32Field = null, + Uint32Field = default(Int32), + NullableUint32Field = null, + Int64Field = default(Int64), + NullableInt64Field = null, + Uint64Field = default(UInt64), + NullableUint64Field = null, + DoubleField = default(Double), + NullableDoubleField = null, + SingleField = default(Single), + NullableSingleField = null, + DecimalField = default(Decimal), + NullableDecimalField = null, + DateTimeField = default(DateTime), + NullableDateTimeField = null, + DateTimeOffsetField = default(DateTimeOffset), + NullableDateTimeOffsetField = null, + GuidField = default(Guid), + NullableGuidField = null, + StringField = null, + EnumField = default(SampleEnum), + NullableEnumField = null, + ComplexAttributeField = null + }; + var s2 = new Sample + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SbyteField = 123, + NullableSbyteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + Uint16Field = 64000, + NullableUint16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + Uint32Field = 3000000000, + NullableUint32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + Uint64Field = 9223372036854775808, + NullableUint64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" + }; + + return Ok(new[] { s1, s2 }); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs index e17978d8..12388d87 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs @@ -1,21 +1,12 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { - public class TagsController : ApiController + public class TagsController : JsonApiController { - protected readonly TestDbContext DbContext; - - public TagsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() + public TagsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) { - return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs new file mode 100644 index 00000000..53e1d564 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs @@ -0,0 +1,13 @@ +using System; +using System.Web.Http; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class TreesController : ApiController + { + public IHttpActionResult Get() + { + throw new Exception("Something bad happened!"); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs index d4a31b72..c2847197 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs @@ -1,21 +1,12 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { - public class UserGroupsController : ApiController + public class UserGroupsController : JsonApiController { - protected readonly TestDbContext DbContext; - - public UserGroupsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() + public UserGroupsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) { - return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs index 2bed59d8..2c314dd6 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs @@ -1,23 +1,12 @@ -using System.Threading.Tasks; -using System.Web.Http; -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { - public class UsersController : ApiController + public class UsersController : JsonApiController { - protected readonly TestDbContext DbContext; - - public UsersController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() + public UsersController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) { - return new EntityFrameworkMaterializer(DbContext, MetadataManager.Instance); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index 59a71cdf..05e4b8a0 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -115,16 +115,22 @@ + + + + + + @@ -132,6 +138,10 @@ + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + {e906356c-93f6-41f6-9a0d-73b8a99aa53c} JSONAPI.EntityFramework diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs new file mode 100644 index 00000000..e1ab9d36 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using JSONAPI.Attributes; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public class City + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + [RelatedResourceLinkTemplate("/cities/{1}/state")] + public virtual State State { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs new file mode 100644 index 00000000..33b91019 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs @@ -0,0 +1,52 @@ +using System; +using JSONAPI.Attributes; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public enum SampleEnum + { + Value1 = 1, + Value2 = 2 + } + + public class Sample + { + public string Id { get; set; } + public Boolean BooleanField { get; set; } + public Boolean? NullableBooleanField { get; set; } + public SByte SbyteField { get; set; } + public SByte? NullableSbyteField { get; set; } + public Byte ByteField { get; set; } + public Byte? NullableByteField { get; set; } + public Int16 Int16Field { get; set; } + public Int16? NullableInt16Field { get; set; } + public UInt16 Uint16Field { get; set; } + public UInt16? NullableUint16Field { get; set; } + public Int32 Int32Field { get; set; } + public Int32? NullableInt32Field { get; set; } + public UInt32 Uint32Field { get; set; } + public UInt32? NullableUint32Field { get; set; } + public Int64 Int64Field { get; set; } + public Int64? NullableInt64Field { get; set; } + public UInt64 Uint64Field { get; set; } + public UInt64? NullableUint64Field { get; set; } + public Double DoubleField { get; set; } + public Double? NullableDoubleField { get; set; } + public Single SingleField { get; set; } + public Single? NullableSingleField { get; set; } + public Decimal DecimalField { get; set; } + public Decimal? NullableDecimalField { get; set; } + public DateTime DateTimeField { get; set; } + public DateTime? NullableDateTimeField { get; set; } + public DateTimeOffset DateTimeOffsetField { get; set; } + public DateTimeOffset? NullableDateTimeOffsetField { get; set; } + public Guid GuidField { get; set; } + public Guid? NullableGuidField { get; set; } + public string StringField { get; set; } + public SampleEnum EnumField { get; set; } + public SampleEnum? NullableEnumField { get; set; } + + [SerializeAsComplex] + public string ComplexAttributeField { get; set; } + } +} diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs new file mode 100644 index 00000000..e0203ca4 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public class State + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public virtual ICollection Cities { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index e6e7f8e3..40fcd54e 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -1,11 +1,15 @@ using System; +using System.Data.Entity; using System.Reflection; using System.Web; using System.Web.Http; using Autofac; using Autofac.Integration.WebApi; +using JSONAPI.Autofac; using JSONAPI.Core; +using JSONAPI.EntityFramework.Http; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; using Microsoft.Owin; using Owin; @@ -41,42 +45,44 @@ public void Configuration(IAppBuilder app) dbContext.Dispose(); }); - var appContainerBuilder = new ContainerBuilder(); - appContainerBuilder.Register(ctx => HttpContext.Current.GetOwinContext()).As(); - appContainerBuilder.Register(c => c.Resolve().Get(DbContextKey)).As(); - appContainerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); - var appContainer = appContainerBuilder.Build(); - app.UseAutofacMiddleware(appContainer); + var pluralizationService = new EntityFrameworkPluralizationService(); + var namingConventions = new DefaultNamingConventions(pluralizationService); - var httpConfig = GetWebApiConfiguration(); - httpConfig.DependencyResolver = new AutofacWebApiDependencyResolver(appContainer); - app.UseWebApi(httpConfig); - app.UseAutofacWebApi(httpConfig); - } - - private static HttpConfiguration GetWebApiConfiguration() - { - var httpConfig = new HttpConfiguration(); - - // Configure the model manager - var pluralizationService = new PluralizationService(); - var modelManager = new ModelManager(pluralizationService) - .RegisterResourceType(typeof (Comment)) - .RegisterResourceType(typeof (Post)) - .RegisterResourceType(typeof (Tag)) - .RegisterResourceType(typeof (User)) - .RegisterResourceType(typeof (UserGroup)); - - // Configure JSON API - new JsonApiConfiguration(modelManager) - .UsingDefaultQueryablePayloadBuilder(c => c.EnumerateQueriesAsynchronously()) - .Apply(httpConfig); + var configuration = new JsonApiAutofacConfiguration(namingConventions); + configuration.OnContainerBuilding(builder => + { + builder.RegisterType() + .WithParameter("apiBaseUrl", "https://www.example.com") + .As(); + builder.Register(c => HttpContext.Current.GetOwinContext()).As(); + builder.Register(c => c.Resolve().Get(DbContextKey)).AsSelf().As(); + builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + }); + configuration.RegisterResourceType(typeof(City)); + configuration.RegisterResourceType(typeof(Comment)); + configuration.RegisterResourceType(typeof(Post)); + configuration.RegisterResourceType(typeof(Sample)); + configuration.RegisterResourceType(typeof(State)); + configuration.RegisterResourceType(typeof(Tag)); + configuration.RegisterResourceType(typeof(User)); + configuration.RegisterResourceType(typeof(UserGroup)); + var httpConfig = new HttpConfiguration + { + IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always + }; // Web API routes httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - return httpConfig; + var container = configuration.Apply(httpConfig); + + //var appContainerBuilder = new ContainerBuilder(); + app.UseAutofacMiddleware(container); + + httpConfig.DependencyResolver = new AutofacWebApiDependencyResolver(container); + app.UseWebApi(httpConfig); + app.UseAutofacWebApi(httpConfig); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index 1b590311..0488f29e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -17,27 +17,36 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance [TestClass] public abstract class AcceptanceTestsBase { + private const string JsonApiContentType = "application/vnd.api+json"; private static readonly Regex GuidRegex = new Regex(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase); //private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace"":[\s]*""[\w\:\\\.\s\,\-]*"""); private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace""[\s]*:[\s]*"".*?"""); - private static readonly Uri BaseUri = new Uri("http://localhost"); + private static readonly Uri BaseUri = new Uri("https://www.example.com"); protected static DbConnection GetEffortConnection() { return TestHelpers.GetEffortConnection(@"Acceptance\Data"); } - protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode) + protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) { var responseContent = await response.Content.ReadAsStringAsync(); var expectedResponse = JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); - redactedResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); + string actualResponse; + if (redactErrorData) + { + var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); + actualResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); + } + else + { + actualResponse = responseContent; + } - redactedResponse.Should().Be(expectedResponse); - response.Content.Headers.ContentType.MediaType.Should().Be("application/vnd.api+json"); + actualResponse.Should().Be(expectedResponse); + response.Content.Headers.ContentType.MediaType.Should().Be(JsonApiContentType); response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); response.StatusCode.Should().Be(expectedStatusCode); @@ -54,7 +63,7 @@ protected async Task SubmitGet(DbConnection effortConnectio })) { var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); + var response = await server.CreateRequest(uri.ToString()).AddHeader("Accept", JsonApiContentType).GetAsync(); return response; } } @@ -74,6 +83,7 @@ protected async Task SubmitPost(DbConnection effortConnecti var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); var response = await server .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) .And(request => { request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); @@ -98,6 +108,7 @@ protected async Task SubmitPatch(DbConnection effortConnect var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); var response = await server .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) .And(request => { request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); @@ -120,6 +131,7 @@ protected async Task SubmitDelete(DbConnection effortConnec var uri = new Uri(BaseUri, requestPath); var response = await server .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) .SendAsync("DELETE"); return response; } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs new file mode 100644 index 00000000..698dee96 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs @@ -0,0 +1,22 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class AttributeSerializationTests : AcceptanceTestsBase + { + [TestMethod] + public async Task Attributes_of_various_types_serialize_correctly() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "samples"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\AttributeSerialization\Attributes_of_various_types_serialize_correctly.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs new file mode 100644 index 00000000..164049b5 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class ErrorsTests : AcceptanceTestsBase + { + [TestMethod] + public async Task Controller_action_throws_exception() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "trees"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Errors\Controller_action_throws_exception.json", HttpStatusCode.InternalServerError, true); + } + } + + [TestMethod] + [Ignore] + public async Task Controller_does_not_exist() + { + // TODO: Currently ignoring this test because it doesn't seem possible to intercept 404s before they make it to the formatter + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "foo"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Errors\Controller_does_not_exist.json", HttpStatusCode.NotFound, true); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json new file mode 100644 index 00000000..ba0978cf --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json @@ -0,0 +1,89 @@ +{ + "data": [ + { + "type": "samples", + "id": "1", + "attributes": { + "boolean-field": false, + "byte-field": 0, + "complex-attribute-field": null, + "date-time-field": "0001-01-01T00:00:00", + "date-time-offset-field": "0001-01-01T00:00:00.0000000+00:00", + "decimal-field": "0", + "double-field": 0.0, + "enum-field": 0, + "guid-field": "00000000-0000-0000-0000-000000000000", + "int16-field": 0, + "int32-field": 0, + "int64-field": 0, + "nullable-boolean-field": false, + "nullable-byte-field": null, + "nullable-date-time-field": null, + "nullable-date-time-offset-field": null, + "nullable-decimal-field": null, + "nullable-double-field": null, + "nullable-enum-field": null, + "nullable-guid-field": null, + "nullable-int16-field": null, + "nullable-int32-field": null, + "nullable-int64-field": null, + "nullable-sbyte-field": null, + "nullable-single-field": null, + "nullable-uint16-field": null, + "nullable-uint32-field": null, + "nullable-uint64-field": null, + "sbyte-field": 0, + "single-field": 0.0, + "string-field": null, + "uint16-field": 0, + "uint32-field": 0, + "uint64-field": 0 + } + }, + { + "type": "samples", + "id": "2", + "attributes": { + "boolean-field": true, + "byte-field": 253, + "complex-attribute-field": { + "foo": { + "baz": [ 11 ] + }, + "bar": 5 + }, + "date-time-field": "1776-07-04T00:00:00", + "date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "decimal-field": "1056789.123", + "double-field": 1056789.123, + "enum-field": 1, + "guid-field": "6566f9b4-5245-40de-890d-98b40a4ad656", + "int16-field": 32000, + "int32-field": 2000000000, + "int64-field": 9223372036854775807, + "nullable-boolean-field": true, + "nullable-byte-field": 253, + "nullable-date-time-field": "1776-07-04T00:00:00", + "nullable-date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "nullable-decimal-field": "1056789.123", + "nullable-double-field": 1056789.123, + "nullable-enum-field": 2, + "nullable-guid-field": "3d1fb81e-43ee-4d04-af91-c8a326341293", + "nullable-int16-field": 32000, + "nullable-int32-field": 2000000000, + "nullable-int64-field": 9223372036854775807, + "nullable-sbyte-field": 123, + "nullable-single-field": 1056789.13, + "nullable-uint16-field": 64000, + "nullable-uint32-field": 3000000000, + "nullable-uint64-field": 9223372036854775808, + "sbyte-field": 123, + "single-field": 1056789.13, + "string-field": "Some string 156", + "uint16-field": 64000, + "uint32-field": 3000000000, + "uint64-field": 9223372036854775808 + } + } + ] +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json new file mode 100644 index 00000000..b5a6b45a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "Unhandled exception", + "detail": "An unhandled exception was thrown while processing the request.", + "meta": { + "exceptionMessage": "Something bad happened!", + "stackTrace": "{{STACK_TRACE}}" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json new file mode 100644 index 00000000..69c2a9c3 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "The resource you requested does not exist." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json index 367ebae2..5d153ee6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json @@ -4,29 +4,28 @@ "type": "posts", "id": "201", "attributes": { - "title": "Post 1", "content": "Post 1 content", - "created": "2015-01-31T14:00:00+00:00" + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "401" + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" } }, "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "102" }, - { "type": "comments", "id": "103" } - ] + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } }, "tags": { - "data": [ - { "type": "tags", "id": "301" }, - { "type": "tags", "id": "302" } - ] + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } } } }, @@ -34,20 +33,20 @@ "type": "comments", "id": "101", "attributes": { - "text": "Comment 1", - "created": "2015-01-31T14:30:00+00:00" + "created": "2015-01-31T14:30:00.0000000+00:00", + "text": "Comment 1" }, "relationships": { - "post": { - "data": { - "type": "posts", - "id": "201" + "author": { + "links": { + "self": "https://www.example.com/comments/101/relationships/author", + "related": "https://www.example.com/comments/101/author" } }, - "author": { - "data": { - "type": "users", - "id": "403" + "post": { + "links": { + "self": "https://www.example.com/comments/101/relationships/post", + "related": "https://www.example.com/comments/101/post" } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json deleted file mode 100644 index e2f476a8..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/GetReturnsIPayloadResponse.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "data": [ - { - "type": "users", - "id": "6500", - "attributes": { - "firstName": "George", - "lastName": "Washington" - }, - "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } - } - }, - { - "type": "users", - "id": "6501", - "attributes": { - "firstName": "Abraham", - "lastName": "Lincoln" - }, - "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } - } - } - ], - "meta": { - "count": 2 - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json new file mode 100644 index 00000000..23653849 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json @@ -0,0 +1,12 @@ +{ + "data": [ + { + "type": "users", + "id": "6500" + }, + { + "type": "users", + "id": "6501" + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json index 9c120cd3..d77552d2 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": [ { "type": "users", "id": "403" } ] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": [ { "type": "users", "id": "403" } ] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json index b0dfe5c9..7835ed54 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json @@ -1,11 +1,9 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": ["301"] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": ["301"] } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json index f9330854..4a0636e6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json @@ -1,11 +1,9 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "New post title" - } + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title" } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json index 0edb9945..417fe5a3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json @@ -1,12 +1,10 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json new file mode 100644 index 00000000..a664a866 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json index 09f4b503..58bf45a3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": null - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": null } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json index 26beb667..b73bceeb 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": null - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": null } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json index e53eb574..b1e90ab0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": { "type": "tags", "id": "301" } - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": { "type": "tags", "id": "301" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json index 25b3ac78..7aa288c5 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": "301" - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": "301" } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json index 0c5fd684..919ab261 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": "403" - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": "403" } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json index e1cb0a33..5886abfe 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json @@ -1,11 +1,9 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": "301" - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": "301" } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json index 8fed2e6d..19eb21a7 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": [] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json index 262b053a..a3adc581 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json @@ -1,22 +1,20 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": [ - { - "id": "301", - "type": "tags" - }, - { - "id": "303", - "type": "tags" - } - ] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ + { + "id": "301", + "type": "tags" + }, + { + "id": "303", + "type": "tags" + } + ] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json index 9b867750..5d36b4cb 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": [ { "type": "tags" } ] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ { "type": "tags" } ] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json index 24754731..6b4c11ae 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": [ { "id": "301" } ] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ { "id": "301" } ] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json index 3b418f4d..db0ef77d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json @@ -1,15 +1,13 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "tags": { - "data": [ - { "type": "tags", "id": "301" } - ] - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ + { "type": "tags", "id": "301" } + ] } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json index 52957507..ebdfb7d6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": { "type": "users" } - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { "type": "users" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json index bec1d11b..0ef65f96 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json @@ -1,13 +1,11 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": { "id": "403" } - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { "id": "403" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json index 0d69a337..296c122a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json @@ -1,16 +1,14 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "relationships": { - "author": { - "data": { - "type": "users", - "id": "403" - } + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { + "type": "users", + "id": "403" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json index 7e1426ca..c49fe508 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json @@ -1,21 +1,19 @@ { - "data": [ - { - "type": "posts", - "id": "205", - "attributes": { - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00" - }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } + "data": { + "type": "posts", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json index 0a9aa7ea..38ba54bc 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json @@ -4,29 +4,28 @@ "type": "posts", "id": "201", "attributes": { - "title": "Post 1", "content": "Post 1 content", - "created": "2015-01-31T14:00:00+00:00" + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "401" + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" } }, "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "102" }, - { "type": "comments", "id": "103" } - ] + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } }, "tags": { - "data": [ - { "type": "tags", "id": "301" }, - { "type": "tags", "id": "302" } - ] + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } } } }, @@ -34,27 +33,28 @@ "type": "posts", "id": "202", "attributes": { - "title": "Post 2", "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "401" + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" } }, "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, "tags": { - "data": [ - { "type": "tags", "id": "302" }, - { "type": "tags", "id": "303" } - ] + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } } } }, @@ -62,26 +62,28 @@ "type": "posts", "id": "203", "attributes": { - "title": "Post 3", "content": "Post 3 content", - "created": "2015-02-07T11:11:00+00:00" + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "401" + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" } }, "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } }, "tags": { - "data": [ - { "type": "tags", "id": "303" } - ] + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + } } } }, @@ -89,19 +91,29 @@ "type": "posts", "id": "204", "attributes": { - "title": "Post 4", "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00" + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "402" + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" } }, - "comments": { "data": [ ] }, - "tags": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json index 89de980d..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json @@ -1,32 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "302" }, - { "type": "tags", "id": "303" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json index 9cd5df41..7a337c39 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json @@ -4,19 +4,29 @@ "type": "posts", "id": "204", "attributes": { - "title": "Post 4", "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00" + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" }, "relationships": { "author": { - "data": { - "type": "users", - "id": "402" + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" } }, - "comments": { "data": [ ] }, - "tags": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json index 2083692b..30614ce8 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an object value for `linkage` but got Array.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid linkage for to-one relationship", + "detail": "Expected an object for to-one linkage, but got Array", + "source": { + "pointer": "/data/relationships/author/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json index c69467a9..56a27ad3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each relationship key on a links object must have an object value.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid relationship object", + "detail": "Expected an object, but found StartArray", + "source": { + "pointer": "/data/relationships/tags" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json index d7af8cbb..aba0d13d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json @@ -1,32 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "New post title", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "302" }, - { "type": "tags", "id": "303" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json index f01ca7cc..09cdf2e3 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an array value for `linkage` but no `linkage` key was found.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Missing linkage for to-many relationship", + "detail": "Expected an array for to-many linkage, but no linkage was specified.", + "source": { + "pointer": "/data/relationships/tags" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json new file mode 100644 index 00000000..f279160e --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Missing linkage for to-one relationship", + "detail": "Expected an object for to-one linkage, but no linkage was specified.", + "source": { + "pointer": "/data/relationships/author" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json index 2cc9a741..441ea0b1 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an array value for `linkage` but got Null.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Null linkage for to-many relationship", + "detail": "Expected an array for to-many linkage, but got Null.", + "source": { + "pointer": "/data/relationships/tags/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json index fcd9960a..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json @@ -1,29 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": null - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "302" }, - { "type": "tags", "id": "303" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json index 69566939..f1879bd7 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an array value for `linkage` but got Object.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid linkage for to-many relationship", + "detail": "Expected an array for to-many linkage, but got Object", + "source": { + "pointer": "/data/relationships/tags/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json index 48910763..3326563d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an array value for `linkage` but got String.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid linkage for relationship", + "detail": "Expected an array, object, or null for linkage, but got String", + "source": { + "pointer": "/data/relationships/tags/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json index d9775cdd..cfe637ad 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Expected an object value for `linkage` but got String.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid linkage for relationship", + "detail": "Expected an array, object, or null for linkage, but got String", + "source": { + "pointer": "/data/relationships/author/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json index c69467a9..0a717105 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each relationship key on a links object must have an object value.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Invalid relationship object", + "detail": "Expected an object, but found String", + "source": { + "pointer": "/data/relationships/author" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json index ca33fa36..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json @@ -1,30 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { - "data": [ - { - "type": "comments", - "id": "104" - } - ] - }, - "tags": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json index c1144df0..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json @@ -1,32 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "303" }, - { "type": "tags", "id": "301" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json index c7cf76f2..8b28cb3f 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each linkage object must have a string value for the key `id`.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Resource identifier missing id", + "detail": "The `id` key is missing.", + "source": { + "pointer": "/data/relationships/tags/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json index 1b599a87..125ef5db 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each linkage object must have a string value for the key `type`.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Resource identifier missing type", + "detail": "The `type` key is missing.", + "source": { + "pointer": "/data/relationships/tags/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json index 31123b36..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json @@ -1,31 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "301" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json index c7cf76f2..2beea4ec 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each linkage object must have a string value for the key `id`.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Resource identifier missing id", + "detail": "The `id` key is missing.", + "source": { + "pointer": "/data/relationships/author/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json index 1b599a87..fb14417c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "JSONAPI.Json.JsonApiFormatter+BadRequestException", - "detail": "Each linkage object must have a string value for the key `type`.", - "stackTrace": "{{STACK_TRACE}}", - "inner": null + "status": "400", + "title": "Resource identifier missing type", + "detail": "The `type` key is missing.", + "source": { + "pointer": "/data/relationships/author/data" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json index b8c6340b..f9067b51 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json @@ -1,32 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "202", - "attributes": { - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00" + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "403" - } - }, - "comments": { - "data": [ - { "type": "comments", "id": "104" } - ] - }, - "tags": { - "data": [ - { "type": "tags", "id": "302" }, - { "type": "tags", "id": "303" } - ] + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json index b59c5be6..a3050aaf 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json @@ -1,23 +1,31 @@ { - "data": [ - { - "type": "posts", - "id": "205", - "attributes": { - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00" + "data": { + "type": "posts", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/205/relationships/author", + "related": "https://www.example.com/posts/205/author" + } }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "401" - } - }, - "comments": { "data": [ ] }, - "tags": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/posts/205/relationships/comments", + "related": "https://www.example.com/posts/205/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/205/relationships/tags", + "related": "https://www.example.com/posts/205/tags" + } } } - ] + } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json index 61a5f729..9355b97e 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json @@ -4,154 +4,280 @@ "type": "users", "id": "401", "attributes": { - "firstName": "Alice", - "lastName": "Smith" + "first-name": "Alice", + "last-name": "Smith" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "201" }, - { "type": "posts", "id": "202" }, - { "type": "posts", "id": "203" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } } }, { "type": "users", "id": "402", "attributes": { - "firstName": "Bob", - "lastName": "Jones" + "first-name": "Bob", + "last-name": "Jones" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "102" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "204" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } } }, { "type": "users", "id": "403", "attributes": { - "firstName": "Charlie", - "lastName": "Michaels" + "first-name": "Charlie", + "last-name": "Michaels" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "103" }, - { "type": "comments", "id": "104" } - ] - }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } } }, { "type": "users", "id": "409", "attributes": { - "firstName": "Charlie", - "lastName": "Burns" + "first-name": "Charlie", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } } }, { "type": "users", "id": "406", "attributes": { - "firstName": "Ed", - "lastName": "Burns" + "first-name": "Ed", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } } }, { "type": "users", "id": "405", "attributes": { - "firstName": "Michelle", - "lastName": "Johnson" + "first-name": "Michelle", + "last-name": "Johnson" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } } }, { "type": "users", "id": "408", "attributes": { - "firstName": "Pat", - "lastName": "Morgan" + "first-name": "Pat", + "last-name": "Morgan" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } } }, { "type": "users", "id": "404", "attributes": { - "firstName": "Richard", - "lastName": "Smith" + "first-name": "Richard", + "last-name": "Smith" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } } }, { "type": "users", "id": "410", "attributes": { - "firstName": "Sally", - "lastName": "Burns" + "first-name": "Sally", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } } }, { "type": "users", "id": "407", "attributes": { - "firstName": "Thomas", - "lastName": "Potter" + "first-name": "Thomas", + "last-name": "Potter" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json index a620e915..ea9f5c96 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByColumnMissingDirectionResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "The sort expression \"firstName\" does not begin with a direction indicator (+ or -).", - "detail": null, - "stackTrace": null, - "inner": null + "status": "400", + "title": "Cannot determine sort direction", + "detail": "The sort expression \"first-name\" does not begin with a direction indicator (+ or -).", + "source": { + "parameter": "sort" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json index afd2bdec..34d2cff4 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json @@ -4,154 +4,280 @@ "type": "users", "id": "410", "attributes": { - "firstName": "Sally", - "lastName": "Burns" + "first-name": "Sally", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } } }, { "type": "users", "id": "406", "attributes": { - "firstName": "Ed", - "lastName": "Burns" + "first-name": "Ed", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } } }, { "type": "users", "id": "409", "attributes": { - "firstName": "Charlie", - "lastName": "Burns" + "first-name": "Charlie", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } } }, { "type": "users", "id": "405", "attributes": { - "firstName": "Michelle", - "lastName": "Johnson" + "first-name": "Michelle", + "last-name": "Johnson" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } } }, { "type": "users", "id": "402", "attributes": { - "firstName": "Bob", - "lastName": "Jones" + "first-name": "Bob", + "last-name": "Jones" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "102" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "204" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } } }, { "type": "users", "id": "403", "attributes": { - "firstName": "Charlie", - "lastName": "Michaels" + "first-name": "Charlie", + "last-name": "Michaels" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "103" }, - { "type": "comments", "id": "104" } - ] - }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } } }, { "type": "users", "id": "408", "attributes": { - "firstName": "Pat", - "lastName": "Morgan" + "first-name": "Pat", + "last-name": "Morgan" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } } }, { "type": "users", "id": "407", "attributes": { - "firstName": "Thomas", - "lastName": "Potter" + "first-name": "Thomas", + "last-name": "Potter" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } } }, { "type": "users", "id": "404", "attributes": { - "firstName": "Richard", - "lastName": "Smith" + "first-name": "Richard", + "last-name": "Smith" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } } }, { "type": "users", "id": "401", "attributes": { - "firstName": "Alice", - "lastName": "Smith" + "first-name": "Alice", + "last-name": "Smith" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "201" }, - { "type": "posts", "id": "202" }, - { "type": "posts", "id": "203" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json index d42356be..bb1e4fbe 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json @@ -4,154 +4,280 @@ "type": "users", "id": "409", "attributes": { - "firstName": "Charlie", - "lastName": "Burns" + "first-name": "Charlie", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } } }, { "type": "users", "id": "406", "attributes": { - "firstName": "Ed", - "lastName": "Burns" + "first-name": "Ed", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } } }, { "type": "users", "id": "410", "attributes": { - "firstName": "Sally", - "lastName": "Burns" + "first-name": "Sally", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } } }, { "type": "users", "id": "405", "attributes": { - "firstName": "Michelle", - "lastName": "Johnson" + "first-name": "Michelle", + "last-name": "Johnson" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } } }, { "type": "users", "id": "402", "attributes": { - "firstName": "Bob", - "lastName": "Jones" + "first-name": "Bob", + "last-name": "Jones" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "102" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "204" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } } }, { "type": "users", "id": "403", "attributes": { - "firstName": "Charlie", - "lastName": "Michaels" + "first-name": "Charlie", + "last-name": "Michaels" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "103" }, - { "type": "comments", "id": "104" } - ] - }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } } }, { "type": "users", "id": "408", "attributes": { - "firstName": "Pat", - "lastName": "Morgan" + "first-name": "Pat", + "last-name": "Morgan" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } } }, { "type": "users", "id": "407", "attributes": { - "firstName": "Thomas", - "lastName": "Potter" + "first-name": "Thomas", + "last-name": "Potter" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } } }, { "type": "users", "id": "401", "attributes": { - "firstName": "Alice", - "lastName": "Smith" + "first-name": "Alice", + "last-name": "Smith" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "201" }, - { "type": "posts", "id": "202" }, - { "type": "posts", "id": "203" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } } }, { "type": "users", "id": "404", "attributes": { - "firstName": "Richard", - "lastName": "Smith" + "first-name": "Richard", + "last-name": "Smith" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json index 1684c854..c56db237 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json @@ -4,154 +4,280 @@ "type": "users", "id": "404", "attributes": { - "firstName": "Richard", - "lastName": "Smith" + "first-name": "Richard", + "last-name": "Smith" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } } }, { "type": "users", "id": "401", "attributes": { - "firstName": "Alice", - "lastName": "Smith" + "first-name": "Alice", + "last-name": "Smith" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "201" }, - { "type": "posts", "id": "202" }, - { "type": "posts", "id": "203" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } } }, { "type": "users", "id": "407", "attributes": { - "firstName": "Thomas", - "lastName": "Potter" + "first-name": "Thomas", + "last-name": "Potter" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } } }, { "type": "users", "id": "408", "attributes": { - "firstName": "Pat", - "lastName": "Morgan" + "first-name": "Pat", + "last-name": "Morgan" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } } }, { "type": "users", "id": "403", "attributes": { - "firstName": "Charlie", - "lastName": "Michaels" + "first-name": "Charlie", + "last-name": "Michaels" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "103" }, - { "type": "comments", "id": "104" } - ] - }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } } }, { "type": "users", "id": "402", "attributes": { - "firstName": "Bob", - "lastName": "Jones" + "first-name": "Bob", + "last-name": "Jones" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "102" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "204" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } } }, { "type": "users", "id": "405", "attributes": { - "firstName": "Michelle", - "lastName": "Johnson" + "first-name": "Michelle", + "last-name": "Johnson" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } } }, { "type": "users", "id": "410", "attributes": { - "firstName": "Sally", - "lastName": "Burns" + "first-name": "Sally", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } } }, { "type": "users", "id": "406", "attributes": { - "firstName": "Ed", - "lastName": "Burns" + "first-name": "Ed", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } } }, { "type": "users", "id": "409", "attributes": { - "firstName": "Charlie", - "lastName": "Burns" + "first-name": "Charlie", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json index 972ba06e..a3541ef0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "The attribute \"firstName\" was specified more than once.", - "detail": null, - "stackTrace": null, - "inner": null + "status": "400", + "title": "Attribute specified more than once", + "detail": "The attribute \"first-name\" was specified more than once.", + "source": { + "parameter": "sort" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json index 34a93fbb..5884cc7a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json @@ -2,11 +2,12 @@ "errors": [ { "id": "{{SOME_GUID}}", - "status": "500", - "title": "The attribute \"foobar\" does not exist on type \"users\".", - "detail": null, - "stackTrace": null, - "inner": null + "status": "400", + "title": "Attribute not found", + "detail": "The attribute \"foobar\" does not exist on type \"users\".", + "source": { + "parameter": "sort" + } } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json index 0c5a42f8..aa6cd74b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json @@ -4,154 +4,280 @@ "type": "users", "id": "407", "attributes": { - "firstName": "Thomas", - "lastName": "Potter" + "first-name": "Thomas", + "last-name": "Potter" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } } }, { "type": "users", "id": "410", "attributes": { - "firstName": "Sally", - "lastName": "Burns" + "first-name": "Sally", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } } }, { "type": "users", "id": "404", "attributes": { - "firstName": "Richard", - "lastName": "Smith" + "first-name": "Richard", + "last-name": "Smith" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } } }, { "type": "users", "id": "408", "attributes": { - "firstName": "Pat", - "lastName": "Morgan" + "first-name": "Pat", + "last-name": "Morgan" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } } }, { "type": "users", "id": "405", "attributes": { - "firstName": "Michelle", - "lastName": "Johnson" + "first-name": "Michelle", + "last-name": "Johnson" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } } }, { "type": "users", "id": "406", "attributes": { - "firstName": "Ed", - "lastName": "Burns" + "first-name": "Ed", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } } }, { "type": "users", "id": "403", "attributes": { - "firstName": "Charlie", - "lastName": "Michaels" + "first-name": "Charlie", + "last-name": "Michaels" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "101" }, - { "type": "comments", "id": "103" }, - { "type": "comments", "id": "104" } - ] - }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } } }, { "type": "users", "id": "409", "attributes": { - "firstName": "Charlie", - "lastName": "Burns" + "first-name": "Charlie", + "last-name": "Burns" }, "relationships": { - "comments": { "data": [ ] }, - "posts": { "data": [ ] }, - "userGroups": { "data": [ ] } + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } } }, { "type": "users", "id": "402", "attributes": { - "firstName": "Bob", - "lastName": "Jones" + "first-name": "Bob", + "last-name": "Jones" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "102" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "204" } - ] + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } } }, { "type": "users", "id": "401", "attributes": { - "firstName": "Alice", - "lastName": "Smith" + "first-name": "Alice", + "last-name": "Smith" }, "relationships": { "comments": { - "data": [ - { "type": "comments", "id": "105" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } }, "posts": { - "data": [ - { "type": "posts", "id": "201" }, - { "type": "posts", "id": "202" }, - { "type": "posts", "id": "203" } - ] + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } }, - "userGroups": { "data": [ ] } + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json index 239674df..23c46576 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json @@ -7,7 +7,12 @@ "name": "Admin users" }, "relationships": { - "users": { "data": [ ] } + "users": { + "links": { + "self": "https://www.example.com/user-groups/501/relationships/users", + "related": "https://www.example.com/user-groups/501/users" + } + } } } ] diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs index 321fe182..854afd31 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs @@ -8,13 +8,13 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance public class PayloadTests : AcceptanceTestsBase { [TestMethod] - public async Task Get_returns_IPayload() + public async Task Get_returns_IResourceCollectionPayload() { using (var effortConnection = GetEffortConnection()) { var response = await SubmitGet(effortConnection, "presidents"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Payload\Responses\GetReturnsIPayloadResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Payload\Responses\Get_returns_IResourceCollectionPayload.json", HttpStatusCode.OK); } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs index 7085b692..8986215d 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs @@ -247,6 +247,35 @@ public async Task PatchWithNullToOneUpdate() await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Author).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.Author.Should().BeNull(); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task PatchWithMissingToOneLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneLinkageRequest.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + using (var dbContext = new TestDbContext(effortConnection, false)) { var allPosts = dbContext.Posts.ToArray(); @@ -256,7 +285,7 @@ public async Task PatchWithNullToOneUpdate() actualPost.Title.Should().Be("Post 2"); actualPost.Content.Should().Be("Post 2 content"); actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); - actualPost.AuthorId.Should().BeNull(); + actualPost.AuthorId.Should().Be("401"); actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); } } @@ -274,7 +303,7 @@ public async Task PatchWithToOneLinkageObjectMissingId() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -303,7 +332,7 @@ public async Task PatchWithToOneLinkageObjectMissingType() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -332,7 +361,7 @@ public async Task PatchWithArrayForToOneLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -361,7 +390,7 @@ public async Task PatchWithStringForToOneLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -390,7 +419,7 @@ public async Task PatchWithMissingToManyLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -419,7 +448,7 @@ public async Task PatchWithToManyLinkageObjectMissingId() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -448,7 +477,7 @@ public async Task PatchWithToManyLinkageObjectMissingType() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -477,7 +506,7 @@ public async Task PatchWithObjectForToManyLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithObjectForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -506,7 +535,7 @@ public async Task PatchWithStringForToManyLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) @@ -536,7 +565,7 @@ public async Task PatchWithNullForToManyLinkage() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithNullForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -565,7 +594,7 @@ public async Task PatchWithArrayRelationshipValue() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -594,7 +623,7 @@ public async Task PatchWithStringRelationshipValue() { var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -623,6 +652,8 @@ public async Task Delete() { var response = await SubmitDelete(effortConnection, "posts/203"); + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); response.StatusCode.Should().Be(HttpStatusCode.NoContent); using (var dbContext = new TestDbContext(effortConnection, false)) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs index 9bc23161..21d84d9c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs @@ -18,7 +18,7 @@ public async Task GetSortedAscending() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName"); + var response = await SubmitGet(effortConnection, "users?sort=%2Bfirst-name"); await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json", HttpStatusCode.OK); } @@ -34,7 +34,7 @@ public async Task GetSortedDesending() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=-firstName"); + var response = await SubmitGet(effortConnection, "users?sort=-first-name"); await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json", HttpStatusCode.OK); } @@ -50,7 +50,7 @@ public async Task GetSortedByMultipleAscending() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,%2BfirstName"); + var response = await SubmitGet(effortConnection, "users?sort=%2Blast-name,%2Bfirst-name"); await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json", HttpStatusCode.OK); } @@ -66,7 +66,7 @@ public async Task GetSortedByMultipleDescending() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=-lastName,-firstName"); + var response = await SubmitGet(effortConnection, "users?sort=-last-name,-first-name"); await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json", HttpStatusCode.OK); } @@ -82,7 +82,7 @@ public async Task GetSortedByMixedDirection() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=%2BlastName,-firstName"); + var response = await SubmitGet(effortConnection, "users?sort=%2Blast-name,-first-name"); await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json", HttpStatusCode.OK); } @@ -100,7 +100,7 @@ public async Task GetSortedByUnknownColumn() { var response = await SubmitGet(effortConnection, "users?sort=%2Bfoobar"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest, true); } } @@ -114,9 +114,9 @@ public async Task GetSortedBySameColumnTwice() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=%2BfirstName,%2BfirstName"); + var response = await SubmitGet(effortConnection, "users?sort=%2Bfirst-name,%2Bfirst-name"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest, true); } } @@ -130,9 +130,9 @@ public async Task GetSortedByColumnMissingDirection() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitGet(effortConnection, "users?sort=firstName"); + var response = await SubmitGet(effortConnection, "users?sort=first-name"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json", HttpStatusCode.BadRequest); + await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByColumnMissingDirectionResponse.json", HttpStatusCode.BadRequest, true); } } } diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkPayloadMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkPayloadMaterializerTests.cs new file mode 100644 index 00000000..ca4cfe13 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/EntityFrameworkPayloadMaterializerTests.cs @@ -0,0 +1,59 @@ +using System.Data.Common; +using System.Data.Entity; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.EntityFramework.Http; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Payload; +using JSONAPI.Payload.Builders; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.EntityFramework.Tests +{ + [TestClass] + public class EntityFrameworkPayloadMaterializerTests + { + private static DbConnection GetEffortConnection() + { + return TestHelpers.GetEffortConnection(@"Acceptance\Data"); + } + + [Ignore] + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public void GetRecords_returns_all_records_when_there_is_no_filtering() + { + //using (var effortConnection = GetEffortConnection()) + //{ + // using (var dbContext = new TestDbContext(effortConnection, false)) + // { + // // Arrange + // var mockResourceObjectReader = new Mock(MockBehavior.Strict); + // var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/posts"); + // var cts = new CancellationTokenSource(); + + // var mockPayload = new Mock(MockBehavior.Strict); + + // var mockQueryableBuilder = new Mock(MockBehavior.Strict); + // var mockSingleResourcePayloadBuilder = new Mock(MockBehavior.Strict); + + // // Act + // var materializer = new EntityFrameworkPayloadMaterializer(dbContext, mockResourceObjectReader.Object, + // mockQueryableBuilder.Object, mockSingleResourcePayloadBuilder.Object); + // var payload = materializer.GetRecords(request, cts.Token).Result; + + // // Assert + // payload.Should().BeSameAs(mockPayload.Object); + // } + //} + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/Http/EntityFrameworkMaterializerTests.cs similarity index 97% rename from JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs rename to JSONAPI.EntityFramework.Tests/Http/EntityFrameworkMaterializerTests.cs index 4f959a32..1c48189b 100644 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ b/JSONAPI.EntityFramework.Tests/Http/EntityFrameworkMaterializerTests.cs @@ -5,7 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace JSONAPI.EntityFramework.Tests +namespace JSONAPI.EntityFramework.Tests.Http { [TestClass] public class EntityFrameworkMaterializerTests diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 620623ba..9b9b24f5 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -110,6 +110,8 @@ + + @@ -118,7 +120,8 @@ - + + @@ -176,7 +179,14 @@ - + + + Always + + + + + Designer diff --git a/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs b/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs deleted file mode 100644 index eba0476e..00000000 --- a/JSONAPI.EntityFramework/DefaultQueryablePayloadBuilderConfigurationExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; - -namespace JSONAPI.EntityFramework -{ - /// - /// Extension Methods for JSONAPI.Core.DefaultQueryablePayloadBuilderConfiguration - /// - public static class DefaultQueryablePayloadBuilderConfigurationExtensions - { - /// - /// Add Entity Framework specific handling to the configuration - /// - /// The configuration object to modify - /// The same configuration object that was passed in - public static DefaultQueryablePayloadBuilderConfiguration EnumerateQueriesAsynchronously(this DefaultQueryablePayloadBuilderConfiguration config) - { - config.EnumerateQueriesWith(new AsynchronousEnumerationTransformer()); - - return config; - } - } -} diff --git a/JSONAPI.EntityFramework/PluralizationService.cs b/JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs similarity index 57% rename from JSONAPI.EntityFramework/PluralizationService.cs rename to JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs index 14b914e0..05739362 100644 --- a/JSONAPI.EntityFramework/PluralizationService.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs @@ -1,23 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace JSONAPI.EntityFramework -{ - public class PluralizationService : JSONAPI.Core.IPluralizationService - { - private static Lazy _pls - = new Lazy( - () => new System.Data.Entity.Infrastructure.Pluralization.EnglishPluralizationService() - ); - public string Pluralize(string s) - { - return _pls.Value.Pluralize(s); - } - public string Singularize(string s) - { - return _pls.Value.Singularize(s); - } - } -} +using System; +using JSONAPI.Core; + +namespace JSONAPI.EntityFramework +{ + /// + /// Implementation of IPluralizationService that uses EntityFramework's built-in EnglishPluralizationService + /// + public class EntityFrameworkPluralizationService : IPluralizationService + { + private static readonly Lazy _pls + = new Lazy( + () => new System.Data.Entity.Infrastructure.Pluralization.EnglishPluralizationService() + ); + public string Pluralize(string s) + { + return _pls.Value.Pluralize(s); + } + public string Singularize(string s) + { + return _pls.Value.Singularize(s); + } + } +} diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..35c768cc --- /dev/null +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Json; +using JSONAPI.Payload; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.EntityFramework +{ + /// + /// This class manages converting IResourceObject instances from a request into records managed + /// by Entity Framework. + /// + public class EntityFrameworkEntityFrameworkResourceObjectMaterializer + { + private readonly DbContext _dbContext; + private readonly IResourceTypeRegistry _registry; + private readonly MethodInfo _openSetToManyRelationshipValueMethod; + + /// + /// Creates a new EntityFrameworkEntityFrameworkResourceObjectMaterializer + /// + /// + /// + public EntityFrameworkEntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceTypeRegistry registry) + { + _dbContext = dbContext; + _registry = registry; + _openSetToManyRelationshipValueMethod = GetType() + .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); + } + + /// + /// Gets a record managed by Entity Framework that has merged in the data from + /// the supplied resource object. + /// + /// + /// + /// + /// + public async Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken) + { + var registration = _registry.GetRegistrationForResourceTypeName(resourceObject.Type); + + var material = await GetExistingRecord(registration, resourceObject.Id, cancellationToken); + if (material == null) + { + material = Activator.CreateInstance(registration.Type); + registration.IdProperty.SetValue(material, resourceObject.Id); + _dbContext.Set(registration.Type).Add(material); + } + + await MergeFieldsIntoProperties(resourceObject, material, registration, cancellationToken); + + return material; + } + + /// + /// Gets an existing record from the store by ID, if it exists + /// + /// + /// + /// + /// + protected virtual Task GetExistingRecord(IResourceTypeRegistration registration, string id, CancellationToken cancellationToken) + { + return _dbContext.Set(registration.Type).FindAsync(cancellationToken, id); + } + + /// + /// Merges the field values of the given resource object into the materialized object + /// + /// + /// + /// + /// + /// + /// Thrown when a semantically incorrect part of the payload is encountered + protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceObject, object material, + IResourceTypeRegistration registration, CancellationToken cancellationToken) + { + foreach (var attributeValue in resourceObject.Attributes) + { + var attribute = (ResourceTypeAttribute) registration.GetFieldByName(attributeValue.Key); + attribute.SetValue(material, attributeValue.Value); + } + + foreach (var relationshipValue in resourceObject.Relationships) + { + var linkage = relationshipValue.Value.Linkage; + + var typeRelationship = (ResourceTypeRelationship) registration.GetFieldByName(relationshipValue.Key); + if (typeRelationship.IsToMany) + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-many relationship", + "Expected an array for to-many linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); + + if (linkage.LinkageToken == null) + throw new DeserializationException("Null linkage for to-many relationship", + "Expected an array for to-many linkage, but got Null.", + "/data/relationships/" + relationshipValue.Key + "/data"); + + var linkageTokenType = linkage.LinkageToken.Type; + if (linkageTokenType != JTokenType.Array) + throw new DeserializationException("Invalid linkage for to-many relationship", + "Expected an array for to-many linkage, but got " + linkage.LinkageToken.Type, + "/data/relationships/" + relationshipValue.Key + "/data"); + + var linkageArray = (JArray) linkage.LinkageToken; + + // TODO: One query per related object is going to be slow. At the very least, we should be able to group the queries by type + var newCollection = new List(); + foreach (var resourceIdentifier in linkageArray) + { + var resourceIdentifierObject = (JObject) resourceIdentifier; + var relatedType = resourceIdentifierObject["type"].Value(); + var relatedId = resourceIdentifierObject["id"].Value(); + + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); + var relatedObject = await GetExistingRecord(relatedObjectRegistration, relatedId, cancellationToken); + newCollection.Add(relatedObject); + } + + var method = _openSetToManyRelationshipValueMethod.MakeGenericMethod(typeRelationship.RelatedType); + method.Invoke(this, new[] { material, newCollection, typeRelationship }); + } + else + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-one relationship", + "Expected an object for to-one linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); + + if (linkage.LinkageToken == null) + { + // For some reason we have to get the value first, or else setting it to null does nothing. + // TODO: This will cause a synchronous query. We can get rid of this line entirely by using Include when the object is first fetched. + typeRelationship.Property.GetValue(material); + typeRelationship.Property.SetValue(material, null); + } + else + { + var linkageTokenType = linkage.LinkageToken.Type; + if (linkageTokenType != JTokenType.Object) + throw new DeserializationException("Invalid linkage for to-one relationship", + "Expected an object for to-one linkage, but got " + linkage.LinkageToken.Type, + "/data/relationships/" + relationshipValue.Key + "/data"); + + var linkageObject = (JObject) linkage.LinkageToken; + var relatedType = linkageObject["type"].Value(); + var relatedId = linkageObject["id"].Value(); + + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); + var relatedObject = + await GetExistingRecord(relatedObjectRegistration, relatedId, cancellationToken); + + typeRelationship.Property.SetValue(material, relatedObject); + } + } + } + } + + // ReSharper disable once UnusedMember.Local + private void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) + { + // TODO: we need to fetch this property asynchronously first + var currentValue = relationship.Property.GetValue(material); + var typedArray = relatedObjects.Select(o => (TRelated) o).ToArray(); + if (relationship.Property.PropertyType.IsAssignableFrom(typeof (List))) + { + if (currentValue == null) + { + relationship.Property.SetValue(material, typedArray.ToList()); + } + else + { + var listCurrentValue = (ICollection) currentValue; + var itemsToAdd = typedArray.Except(listCurrentValue); + var itemsToRemove = listCurrentValue.Except(typedArray).ToList(); + + foreach (var related in itemsToAdd) + listCurrentValue.Add(related); + + foreach (var related in itemsToRemove) + listCurrentValue.Remove(related); + } + } + else + { + relationship.Property.SetValue(material, typedArray); + } + } + } +} diff --git a/JSONAPI.EntityFramework/Http/ApiController.cs b/JSONAPI.EntityFramework/Http/ApiController.cs deleted file mode 100644 index 93473040..00000000 --- a/JSONAPI.EntityFramework/Http/ApiController.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using JSONAPI.Core; - -namespace JSONAPI.EntityFramework.Http -{ - public class ApiController : JSONAPI.Http.ApiController - where T : class // hmm...see http://stackoverflow.com/a/6451237/489116 - where TC : DbContext - { - private EntityFrameworkMaterializer _materializer = null; - - protected override JSONAPI.Core.IMaterializer MaterializerFactory() - { - if (_materializer == null) - { - DbContext context = (DbContext)Activator.CreateInstance(typeof(TC)); - var metadataManager = MetadataManager.Instance; - _materializer = new JSONAPI.EntityFramework.EntityFrameworkMaterializer(context, metadataManager); - } - return _materializer; - } - - protected override TM MaterializerFactory() - { - return base.MaterializerFactory(); - } - - protected override IQueryable QueryableFactory(Core.IMaterializer materializer = null) - { - if (materializer == null) - { - materializer = MaterializerFactory(); - } - return ((EntityFrameworkMaterializer)materializer).DbContext.Set(); - } - - public override async Task> Post(IList postedObjs) - { - var materializer = this.MaterializerFactory(); - List materialList = new List(); - foreach (T postedObj in postedObjs) - { - DbContext context = materializer.DbContext; - var material = await materializer.MaterializeUpdateAsync(postedObj); - if (context.Entry(material).State == EntityState.Added) - { - await context.SaveChangesAsync(); - materialList.Add(material); - } - else - { - // POST should only create an object--if the EntityState is Unchanged or Modified, this is an illegal operation. - var e = new System.Web.Http.HttpResponseException(System.Net.HttpStatusCode.BadRequest); - //e.InnerException = new ArgumentException("The POSTed object already exists!"); // Can't do this, I guess... - throw e; - } - } - return materialList; - } - - public override async Task> Patch(string id, IList putObjs) - { - var materializer = this.MaterializerFactory(); - DbContext context = materializer.DbContext; - List materialList = new List(); - foreach (T putObj in putObjs) - { - var material = await materializer.MaterializeUpdateAsync(putObj); - materialList.Add(material); - } - await context.SaveChangesAsync(); - return materialList; - } - - public override async Task Delete(string id) - { - var materializer = this.MaterializerFactory(); - DbContext context = materializer.DbContext; - T target = await materializer.GetByIdAsync(id); - context.Set().Remove(target); - await context.SaveChangesAsync(); - await base.Delete(id); - } - - protected override void Dispose(bool disposing) - { - //FIXME: Unsure what to do with the "disposing" parameter here...what does it mean?? - if (_materializer != null) - { - _materializer.DbContext.Dispose(); - } - _materializer = null; - base.Dispose(disposing); - } - } -} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs new file mode 100644 index 00000000..74e75253 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs @@ -0,0 +1,95 @@ +using System.Data.Entity; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Http; +using JSONAPI.Payload; +using JSONAPI.Payload.Builders; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of for use with Entity Framework. + /// + public class EntityFrameworkPayloadMaterializer : IPayloadMaterializer + { + private readonly string _apiBaseUrl; + private readonly DbContext _dbContext; + private readonly IResourceTypeRegistry _resourceTypeRegistry; + private readonly IQueryableResourceCollectionPayloadBuilder _queryableResourceCollectionPayloadBuilder; + private readonly ISingleResourcePayloadBuilder _singleResourcePayloadBuilder; + + /// + /// Creates a new EntityFrameworkPayloadMaterializer + /// + /// The base url of the API, e.g. https://www.example.com + /// + /// + /// + /// + public EntityFrameworkPayloadMaterializer( + string apiBaseUrl, + DbContext dbContext, + IResourceTypeRegistry resourceTypeRegistry, + IQueryableResourceCollectionPayloadBuilder queryableResourceCollectionPayloadBuilder, + ISingleResourcePayloadBuilder singleResourcePayloadBuilder) + { + _apiBaseUrl = apiBaseUrl; + _dbContext = dbContext; + _resourceTypeRegistry = resourceTypeRegistry; + _queryableResourceCollectionPayloadBuilder = queryableResourceCollectionPayloadBuilder; + _singleResourcePayloadBuilder = singleResourcePayloadBuilder; + } + + public Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) where T : class + { + var query = _dbContext.Set().AsQueryable(); + return _queryableResourceCollectionPayloadBuilder.BuildPayload(query, request, cancellationToken); + } + + public async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) where T : class + { + var singleResource = await _dbContext.Set().FindAsync(cancellationToken, id); + return _singleResourcePayloadBuilder.BuildPayload(singleResource, _apiBaseUrl, null); + } + + public async Task CreateRecord(ISingleResourcePayload requestPayload, HttpRequestMessage request, CancellationToken cancellationToken) where T : class + { + var newRecord = await MaterializeAsync(requestPayload.PrimaryData, cancellationToken); + var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, _apiBaseUrl, null); + await _dbContext.SaveChangesAsync(cancellationToken); + + return returnPayload; + } + + public async Task UpdateRecord(string id, ISingleResourcePayload requestPayload, HttpRequestMessage request, CancellationToken cancellationToken) where T : class + { + var newRecord = await MaterializeAsync(requestPayload.PrimaryData, cancellationToken); + var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, _apiBaseUrl, null); + await _dbContext.SaveChangesAsync(cancellationToken); + + return returnPayload; + } + + public async Task DeleteRecord(string id, CancellationToken cancellationToken) where T : class + { + var singleResource = await _dbContext.Set().FindAsync(cancellationToken, id); + _dbContext.Set().Remove(singleResource); + await _dbContext.SaveChangesAsync(cancellationToken); + + return null; + } + + /// + /// Convert a resource object into a material record managed by EntityFramework. + /// + /// + protected virtual async Task MaterializeAsync(IResourceObject resourceObject, CancellationToken cancellationToken) + { + var materializer = new EntityFrameworkEntityFrameworkResourceObjectMaterializer(_dbContext, _resourceTypeRegistry); + return await materializer.MaterializeResourceObject(resourceObject, cancellationToken); + } + } +} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 4dbd21ab..fedc74b2 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -74,10 +74,10 @@ - - - + + + diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 8b0626f8..46b2c3e2 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -46,22 +46,22 @@ private class Dummy public Decimal? NullableDecimalField { get; set; } public Boolean BooleanField { get; set; } public Boolean? NullableBooleanField { get; set; } - public SByte SByteField { get; set; } - public SByte? NullableSByteField { get; set; } + public SByte SbyteField { get; set; } + public SByte? NullableSbyteField { get; set; } public Byte ByteField { get; set; } public Byte? NullableByteField { get; set; } public Int16 Int16Field { get; set; } public Int16? NullableInt16Field { get; set; } - public UInt16 UInt16Field { get; set; } - public UInt16? NullableUInt16Field { get; set; } + public UInt16 Uint16Field { get; set; } + public UInt16? NullableUint16Field { get; set; } public Int32 Int32Field { get; set; } public Int32? NullableInt32Field { get; set; } - public UInt32 UInt32Field { get; set; } - public UInt32? NullableUInt32Field { get; set; } + public UInt32 Uint32Field { get; set; } + public UInt32? NullableUint32Field { get; set; } public Int64 Int64Field { get; set; } public Int64? NullableInt64Field { get; set; } - public UInt64 UInt64Field { get; set; } - public UInt64? NullableUInt64Field { get; set; } + public UInt64 Uint64Field { get; set; } + public UInt64? NullableUint64Field { get; set; } public Double DoubleField { get; set; } public Double? NullableDoubleField { get; set; } public Single SingleField { get; set; } @@ -230,12 +230,12 @@ public void SetupFixtures() new Dummy { Id = "210", - SByteField = 63 + SbyteField = 63 }, new Dummy { Id = "211", - SByteField = -89 + SbyteField = -89 }, #endregion @@ -245,7 +245,7 @@ public void SetupFixtures() new Dummy { Id = "220", - NullableSByteField = 91 + NullableSbyteField = 91 }, #endregion @@ -305,12 +305,12 @@ public void SetupFixtures() new Dummy { Id = "270", - UInt16Field = 12345 + Uint16Field = 12345 }, new Dummy { Id = "271", - UInt16Field = 45678 + Uint16Field = 45678 }, #endregion @@ -320,7 +320,7 @@ public void SetupFixtures() new Dummy { Id = "280", - NullableUInt16Field = 65000 + NullableUint16Field = 65000 }, #endregion @@ -355,12 +355,12 @@ public void SetupFixtures() new Dummy { Id = "310", - UInt32Field = 123456789 + Uint32Field = 123456789 }, new Dummy { Id = "311", - UInt32Field = 234567890 + Uint32Field = 234567890 }, #endregion @@ -370,7 +370,7 @@ public void SetupFixtures() new Dummy { Id = "320", - NullableUInt32Field = 345678901 + NullableUint32Field = 345678901 }, #endregion @@ -405,12 +405,12 @@ public void SetupFixtures() new Dummy { Id = "350", - UInt64Field = 123456789012 + Uint64Field = 123456789012 }, new Dummy { Id = "351", - UInt64Field = 234567890123 + Uint64Field = 234567890123 }, #endregion @@ -420,7 +420,7 @@ public void SetupFixtures() new Dummy { Id = "360", - NullableUInt64Field = 345678901234 + NullableUint64Field = 345678901234 }, #endregion @@ -541,10 +541,10 @@ private DefaultFilteringTransformer GetTransformer() { {"Dummy", "Dummies"} }); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Dummy)); - modelManager.RegisterResourceType(typeof(RelatedItemWithId)); - return new DefaultFilteringTransformer(modelManager); + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(pluralizationService)); + registry.RegisterResourceType(typeof(Dummy)); + registry.RegisterResourceType(typeof(RelatedItemWithId)); + return new DefaultFilteringTransformer(registry); } private Dummy[] GetArray(string uri) @@ -557,7 +557,7 @@ private Dummy[] GetArray(string uri) [TestMethod] public void Filters_by_matching_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]=String value 1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String value 1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("100"); } @@ -565,7 +565,7 @@ public void Filters_by_matching_string_property() [TestMethod] public void Filters_by_missing_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 3); returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); } @@ -577,7 +577,7 @@ public void Filters_by_missing_string_property() [TestMethod] public void Filters_by_matching_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeField]=1930-11-07"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-field]=1930-11-07"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("110"); } @@ -585,14 +585,14 @@ public void Filters_by_matching_datetime_property() [TestMethod] public void Filters_by_missing_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeField]=1961-02-18"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-02-18"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("120"); } @@ -600,7 +600,7 @@ public void Filters_by_matching_nullable_datetime_property() [TestMethod] public void Filters_by_missing_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "120").Should().BeFalse(); } @@ -612,7 +612,7 @@ public void Filters_by_missing_nullable_datetime_property() [TestMethod] public void Filters_by_matching_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeOffsetField]=1991-01-03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-offset-field]=1991-01-03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("130"); } @@ -620,14 +620,14 @@ public void Filters_by_matching_datetimeoffset_property() [TestMethod] public void Filters_by_missing_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[dateTimeOffsetField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-offset-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeOffsetField]=2014-05-05"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]=2014-05-05"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("140"); } @@ -635,7 +635,7 @@ public void Filters_by_matching_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDateTimeOffsetField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "140").Should().BeFalse(); } @@ -647,7 +647,7 @@ public void Filters_by_missing_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_matching_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[enumField]=1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]=1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("150"); } @@ -655,14 +655,14 @@ public void Filters_by_matching_enum_property() [TestMethod] public void Filters_by_missing_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[enumField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableEnumField]=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("160"); } @@ -670,7 +670,7 @@ public void Filters_by_matching_nullable_enum_property() [TestMethod] public void Filters_by_missing_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableEnumField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "160").Should().BeFalse(); } @@ -682,7 +682,7 @@ public void Filters_by_missing_nullable_enum_property() [TestMethod] public void Filters_by_matching_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[decimalField]=4.03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]=4.03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("170"); } @@ -690,14 +690,14 @@ public void Filters_by_matching_decimal_property() [TestMethod] public void Filters_by_missing_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[decimalField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDecimalField]=12.09"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-decimal-field]=12.09"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("180"); } @@ -705,7 +705,7 @@ public void Filters_by_matching_nullable_decimal_property() [TestMethod] public void Filters_by_missing_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDecimalField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-decimal-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "180").Should().BeFalse(); } @@ -717,7 +717,7 @@ public void Filters_by_missing_nullable_decimal_property() [TestMethod] public void Filters_by_matching_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[booleanField]=true"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[boolean-field]=true"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("190"); } @@ -725,14 +725,14 @@ public void Filters_by_matching_boolean_property() [TestMethod] public void Filters_by_missing_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[booleanField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[boolean-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableBooleanField]=false"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-boolean-field]=false"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("200"); } @@ -740,7 +740,7 @@ public void Filters_by_matching_nullable_boolean_property() [TestMethod] public void Filters_by_missing_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableBooleanField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-boolean-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "200").Should().BeFalse(); } @@ -752,7 +752,7 @@ public void Filters_by_missing_nullable_boolean_property() [TestMethod] public void Filters_by_matching_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[sByteField]=63"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sbyte-field]=63"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("210"); } @@ -760,14 +760,14 @@ public void Filters_by_matching_sbyte_property() [TestMethod] public void Filters_by_missing_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[sByteField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sbyte-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSByteField]=91"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-sbyte-field]=91"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("220"); } @@ -775,7 +775,7 @@ public void Filters_by_matching_nullable_sbyte_property() [TestMethod] public void Filters_by_missing_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSByteField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-sbyte-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "220").Should().BeFalse(); } @@ -787,7 +787,7 @@ public void Filters_by_missing_nullable_sbyte_property() [TestMethod] public void Filters_by_matching_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[byteField]=250"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byte-field]=250"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("230"); } @@ -795,14 +795,14 @@ public void Filters_by_matching_byte_property() [TestMethod] public void Filters_by_missing_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[byteField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byte-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableByteField]=44"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-byte-field]=44"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("240"); } @@ -810,7 +810,7 @@ public void Filters_by_matching_nullable_byte_property() [TestMethod] public void Filters_by_missing_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableByteField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-byte-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "240").Should().BeFalse(); } @@ -822,7 +822,7 @@ public void Filters_by_missing_nullable_byte_property() [TestMethod] public void Filters_by_matching_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int16Field]=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16-field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("250"); } @@ -830,14 +830,14 @@ public void Filters_by_matching_int16_property() [TestMethod] public void Filters_by_missing_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int16Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt16Field]=32764"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int16-field]=32764"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("260"); } @@ -845,7 +845,7 @@ public void Filters_by_matching_nullable_int16_property() [TestMethod] public void Filters_by_missing_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt16Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int16-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "260").Should().BeFalse(); } @@ -857,7 +857,7 @@ public void Filters_by_missing_nullable_int16_property() [TestMethod] public void Filters_by_matching_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt16Field]=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint16-field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("270"); } @@ -865,14 +865,14 @@ public void Filters_by_matching_uint16_property() [TestMethod] public void Filters_by_missing_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt16Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint16-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt16Field]=65000"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint16-field]=65000"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("280"); } @@ -880,7 +880,7 @@ public void Filters_by_matching_nullable_uint16_property() [TestMethod] public void Filters_by_missing_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt16Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint16-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "280").Should().BeFalse(); } @@ -892,7 +892,7 @@ public void Filters_by_missing_nullable_uint16_property() [TestMethod] public void Filters_by_matching_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int32Field]=100000006"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32-field]=100000006"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("290"); } @@ -900,14 +900,14 @@ public void Filters_by_matching_int32_property() [TestMethod] public void Filters_by_missing_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int32Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt32Field]=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int32-field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("300"); } @@ -915,7 +915,7 @@ public void Filters_by_matching_nullable_int32_property() [TestMethod] public void Filters_by_missing_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt32Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int32-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "300").Should().BeFalse(); } @@ -927,7 +927,7 @@ public void Filters_by_missing_nullable_int32_property() [TestMethod] public void Filters_by_matching_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt32Field]=123456789"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint32-field]=123456789"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("310"); } @@ -935,14 +935,14 @@ public void Filters_by_matching_uint32_property() [TestMethod] public void Filters_by_missing_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt32Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint32-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt32Field]=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint32-field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("320"); } @@ -950,7 +950,7 @@ public void Filters_by_matching_nullable_uint32_property() [TestMethod] public void Filters_by_missing_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt32Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint32-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "320").Should().BeFalse(); } @@ -962,7 +962,7 @@ public void Filters_by_missing_nullable_uint32_property() [TestMethod] public void Filters_by_matching_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int64Field]=123453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64-field]=123453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("330"); } @@ -970,14 +970,14 @@ public void Filters_by_matching_int64_property() [TestMethod] public void Filters_by_missing_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[int64Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt64Field]=345671901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int64-field]=345671901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("340"); } @@ -985,7 +985,7 @@ public void Filters_by_matching_nullable_int64_property() [TestMethod] public void Filters_by_missing_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableInt64Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int64-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "340").Should().BeFalse(); } @@ -997,7 +997,7 @@ public void Filters_by_missing_nullable_int64_property() [TestMethod] public void Filters_by_matching_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt64Field]=123456789012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint64-field]=123456789012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("350"); } @@ -1005,14 +1005,14 @@ public void Filters_by_matching_uint64_property() [TestMethod] public void Filters_by_missing_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[uInt64Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint64-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt64Field]=345678901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint64-field]=345678901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("360"); } @@ -1020,7 +1020,7 @@ public void Filters_by_matching_nullable_uint64_property() [TestMethod] public void Filters_by_missing_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableUInt64Field]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint64-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "360").Should().BeFalse(); } @@ -1032,7 +1032,7 @@ public void Filters_by_missing_nullable_uint64_property() [TestMethod] public void Filters_by_matching_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[singleField]=21.56901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]=21.56901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("370"); } @@ -1040,14 +1040,14 @@ public void Filters_by_matching_single_property() [TestMethod] public void Filters_by_missing_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[singleField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSingleField]=1.3456"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-single-field]=1.3456"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("380"); } @@ -1055,7 +1055,7 @@ public void Filters_by_matching_nullable_single_property() [TestMethod] public void Filters_by_missing_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableSingleField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-single-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "380").Should().BeFalse(); } @@ -1067,7 +1067,7 @@ public void Filters_by_missing_nullable_single_property() [TestMethod] public void Filters_by_matching_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[doubleField]=12.3453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]=12.3453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("390"); } @@ -1075,14 +1075,14 @@ public void Filters_by_matching_double_property() [TestMethod] public void Filters_by_missing_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[doubleField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDoubleField]=34567.1901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-double-field]=34567.1901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("400"); } @@ -1090,7 +1090,7 @@ public void Filters_by_matching_nullable_double_property() [TestMethod] public void Filters_by_missing_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullableDoubleField]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-double-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "400").Should().BeFalse(); } @@ -1113,7 +1113,7 @@ public void Does_not_filter_unknown_type() [TestMethod] public void Filters_by_matching_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[toOneRelatedItem]=1101"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-one-related-item]=1101"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1100"); } @@ -1121,7 +1121,7 @@ public void Filters_by_matching_to_one_relationship_id() [TestMethod] public void Filters_by_missing_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[toOneRelatedItem]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-one-related-item]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1100" || d.Id == "1102").Should().BeFalse(); } @@ -1133,7 +1133,7 @@ public void Filters_by_missing_to_one_relationship_id() [TestMethod] public void Filters_by_matching_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[toManyRelatedItems]=1111"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-many-related-items]=1111"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1110"); } @@ -1141,7 +1141,7 @@ public void Filters_by_matching_id_in_to_many_relationship() [TestMethod] public void Filters_by_missing_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[toManyRelatedItems]="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-many-related-items]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1110" || d.Id == "1120").Should().BeFalse(); } @@ -1153,7 +1153,7 @@ public void Filters_by_missing_id_in_to_many_relationship() [TestMethod] public void Ands_together_filters() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[stringField]=String value 2&filter[enumField]=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String value 2&filter[enum-field]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("102"); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs index 9cb9fc77..a1dfb68f 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -7,6 +7,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.Payload.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters @@ -48,7 +49,7 @@ public void SetupFixtures() private DefaultPaginationTransformer GetTransformer(int maxPageSize) { - return new DefaultPaginationTransformer("page.number", "page.size", maxPageSize); + return new DefaultPaginationTransformer(maxPageSize); } private Dummy[] GetArray(string uri, int maxPageSize = 50) @@ -71,13 +72,6 @@ public void ApplyPagination_returns_all_results_when_they_are_within_page() array.Length.Should().Be(9); } - [TestMethod] - public void ApplyPagination_returns_no_results_when_page_size_is_zero() - { - var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=0"); - array.Length.Should().Be(0); - } - [TestMethod] public void ApplyPagination_returns_first_page_of_data() { @@ -114,43 +108,53 @@ public void ApplyPagination_uses_max_page_size_when_requested_page_size_is_highe } [TestMethod] - public void ApplyPagination_returns_400_if_page_number_is_negative() + public void ApplyPagination_throws_exception_if_page_number_is_negative() { Action action = () => { GetArray("http://api.example.com/dummies?page[number]=-4&page[size]=4"); }; - action.ShouldThrow().And.Message.Should().Be("page.number must be not be negative."); + action.ShouldThrow().And.Error.Detail.Should().Be("Page number must not be negative."); } [TestMethod] - public void ApplyPagination_returns_400_if_page_size_is_negative() + public void ApplyPagination_throws_exception_if_page_size_is_negative() { Action action = () => { GetArray("http://api.example.com/dummies?page[number]=0&page[size]=-4"); }; - action.ShouldThrow().And.Message.Should().Be("page.size must be not be negative."); + action.ShouldThrow().And.Error.Detail.Should().Be("Page size must be greater than or equal to 1."); + } + + [TestMethod] + public void ApplyPagination_throws_exception_when_page_size_is_zero() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0&page[size]=0"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("Page size must be greater than or equal to 1."); } [TestMethod] - public void ApplyPagination_returns_400_if_page_number_specified_but_not_size() + public void ApplyPagination_throws_exception_if_page_number_specified_but_not_size() { Action action = () => { GetArray("http://api.example.com/dummies?page[number]=0"); }; - action.ShouldThrow().And.Message.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); + action.ShouldThrow().And.Error.Detail.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); } [TestMethod] - public void ApplyPagination_returns_400_if_page_size_specified_but_not_number() + public void ApplyPagination_throws_exception_if_page_size_specified_but_not_number() { Action action = () => { GetArray("http://api.example.com/dummies?page[size]=0"); }; - action.ShouldThrow().And.Message.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); + action.ShouldThrow().And.Error.Detail.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); } [TestMethod] diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 80ea20ed..e99dbf5f 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -5,6 +5,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.Payload.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters @@ -50,9 +51,9 @@ private DefaultSortingTransformer GetTransformer() { {"Dummy", "Dummies"} }); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Dummy)); - return new DefaultSortingTransformer(modelManager); + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(pluralizationService)); + registry.RegisterResourceType(typeof(Dummy)); + return new DefaultSortingTransformer(registry); } private Dummy[] GetArray(string uri) @@ -70,34 +71,34 @@ private void RunTransformAndExpectFailure(string uri, string expectedMessage) // ReSharper disable once UnusedVariable var result = GetTransformer().Sort(_fixturesQuery, request).ToArray(); }; - action.ShouldThrow().Which.Message.Should().Be(expectedMessage); + action.ShouldThrow().Which.Error.Detail.Should().Be(expectedMessage); } [TestMethod] public void Sorts_by_attribute_ascending() { - var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); + var array = GetArray("http://api.example.com/dummies?sort=%2Bfirst-name"); array.Should().BeInAscendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_attribute_descending() { - var array = GetArray("http://api.example.com/dummies?sort=-firstName"); + var array = GetArray("http://api.example.com/dummies?sort=-first-name"); array.Should().BeInDescendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_two_ascending_attributes() { - var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); + var array = GetArray("http://api.example.com/dummies?sort=%2Blast-name,%2Bfirst-name"); array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); } [TestMethod] public void Sorts_by_two_descending_attributes() { - var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); + var array = GetArray("http://api.example.com/dummies?sort=-last-name,-first-name"); array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); } @@ -116,13 +117,13 @@ public void Returns_400_if_sort_argument_is_whitespace() [TestMethod] public void Returns_400_if_property_name_is_missing() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B", "The property name is missing."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B", "One of the sort expressions is empty."); } [TestMethod] public void Returns_400_if_property_name_is_whitespace() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B ", "One of the sort expressions is empty."); } [TestMethod] @@ -134,13 +135,13 @@ public void Returns_400_if_no_property_exists() [TestMethod] public void Returns_400_if_the_same_property_is_specified_more_than_once() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2Blast-name,%2Blast-name", "The attribute \"last-name\" was specified more than once."); } [TestMethod] public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name", "The sort expression \"last-name\" does not begin with a direction indicator (+ or -)."); } } } diff --git a/JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs b/JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs new file mode 100644 index 00000000..e76e24bd --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Payload; +using JSONAPI.Payload.Builders; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class FallbackPayloadBuilderAttributeTests + { + private HttpActionExecutedContext GetActionExecutedContext(object objectContentValue, Exception exception = null) + { + var mockMediaTypeFormatter = new Mock(MockBehavior.Strict); + mockMediaTypeFormatter.Setup(f => f.CanWriteType(It.IsAny())).Returns(true); + mockMediaTypeFormatter.Setup(f => f.SetDefaultContentHeaders(It.IsAny(), It.IsAny(), It.IsAny())); + var response = new HttpResponseMessage + { + Content = new ObjectContent(objectContentValue.GetType(), objectContentValue, mockMediaTypeFormatter.Object) + }; + var actionContext = new HttpActionContext { Response = response }; + return new HttpActionExecutedContext(actionContext, exception); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_ISingleResourcePayload_alone() + { + // Arrange + var mockPayload = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_IResourceCollectionPayload_alone() + { + // Arrange + var mockPayload = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_IErrorPayload_alone_but_changes_request_status_to_match_error_status() + { + // Arrange + var mockError = new Mock(MockBehavior.Strict); + mockError.Setup(e => e.Status).Returns(HttpStatusCode.Conflict); + var mockPayload = new Mock(MockBehavior.Strict); + mockPayload.Setup(p => p.Errors).Returns(new[] {mockError.Object}); + var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [TestMethod] + public void OnActionExecutedAsync_does_nothing_if_there_is_an_exception() + { + // Arrange + var objectContent = new object(); + var theException = new Exception("This is an error."); + var actionExecutedContext = GetActionExecutedContext(objectContent, theException); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + var newObjectContent = ((ObjectContent) actionExecutedContext.Response.Content).Value; + newObjectContent.Should().BeSameAs(objectContent); + actionExecutedContext.Exception.Should().Be(theException); + } + + private class Fruit + { + } + + [TestMethod] + public void OnActionExecutedAsync_delegates_to_fallback_payload_builder_for_unknown_types() + { + // Arrange + var payload = new Fruit(); + var actionExecutedContext = GetActionExecutedContext(payload); + var cancellationTokenSource = new CancellationTokenSource(); + + var mockResult = new Mock(MockBehavior.Strict); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + mockFallbackPayloadBuilder.Setup(b => b.BuildPayload(payload, It.IsAny(), cancellationTokenSource.Token)) + .Returns(Task.FromResult(mockResult.Object)); + + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + // Assert + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockResult.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_creates_IErrorPayload_for_HttpError() + { + // Arrange + var httpError = new HttpError("Some error"); + var actionExecutedContext = GetActionExecutedContext(httpError); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + + var mockError = new Mock(MockBehavior.Strict); + mockError.Setup(e => e.Status).Returns(HttpStatusCode.OK); + var mockResult = new Mock(MockBehavior.Strict); + mockResult.Setup(r => r.Errors).Returns(new[] { mockError.Object }); + + var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + mockErrorPayloadBuilder.Setup(b => b.BuildFromHttpError(httpError, HttpStatusCode.OK)).Returns(mockResult.Object); + + // Act + var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + // Assert + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockResult.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + } +} diff --git a/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs b/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs new file mode 100644 index 00000000..5ce99dcc --- /dev/null +++ b/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Attributes; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class DefaultNamingConventionsTests + { + private class Band + { + [JsonProperty("THE-GENRE")] + public string Genre { get; set; } + } + + private class SomeClass + { + public string SomeKey { get; set; } + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_id() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(Author).GetProperty("Id")); + + // Assert + name.Should().Be("id"); + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_camel_cased_property() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(SomeClass).GetProperty("SomeKey")); + + // Assert + name.Should().Be("some-key"); + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_property_with_JsonProperty_attribute() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(Band).GetProperty("Genre")); + + // Assert + name.Should().Be("THE-GENRE"); + } + } +} diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs index 2a231144..6a9a9244 100644 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ b/JSONAPI.Tests/Core/MetadataManagerTests.cs @@ -9,32 +9,33 @@ namespace JSONAPI.Tests.Core [TestClass] public class MetadataManagerTests { + [Ignore] [TestMethod] [DeploymentItem(@"Data\MetadataManagerPropertyWasPresentRequest.json")] public void PropertyWasPresentTest() { - using (var inputStream = File.OpenRead("MetadataManagerPropertyWasPresentRequest.json")) - { - // Arrange - var modelManager = new ModelManager(new PluralizationService()); - modelManager.RegisterResourceType(typeof(Post)); - modelManager.RegisterResourceType(typeof(Author)); - JsonApiFormatter formatter = new JsonApiFormatter(modelManager); + //using (var inputStream = File.OpenRead("MetadataManagerPropertyWasPresentRequest.json")) + //{ + // // Arrange + // var modelManager = new ModelManager(new PluralizationService()); + // modelManager.RegisterResourceType(typeof(Post)); + // modelManager.RegisterResourceType(typeof(Author)); + // JsonApiFormatter formatter = new JsonApiFormatter(modelManager); - var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; + // var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - // Act - bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); - bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); - bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); - bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); + // // Act + // bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); + // bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); + // bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); + // bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); - // Assert - Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); - Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); - Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); - Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); - } + // // Assert + // Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); + // Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); + // Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); + // Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); + //} } } } diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs deleted file mode 100644 index 3168125a..00000000 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ /dev/null @@ -1,426 +0,0 @@ -using System; -using JSONAPI.Attributes; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.Core; -using JSONAPI.Tests.Models; -using System.Reflection; -using System.Collections.Generic; -using System.Collections; -using FluentAssertions; -using Newtonsoft.Json; - -namespace JSONAPI.Tests.Core -{ - [TestClass] - public class ModelManagerTests - { - private class InvalidModel // No Id discernable! - { - public string Data { get; set; } - } - - private class CustomIdModel - { - [UseAsId] - public Guid Uuid { get; set; } - - public string Data { get; set; } - } - - private class DerivedPost : Post - { - - } - - private class Band - { - [UseAsId] - public string BandName { get; set; } - - [JsonProperty("THE-GENRE")] - public string Genre { get; set; } - } - - private class Salad - { - public string Id { get; set; } - - [JsonProperty("salad-type")] - public string TheSaladType { get; set; } - - [JsonProperty("salad-type")] - public string AnotherSaladType { get; set; } - } - - [TestMethod] - public void FindsIdNamedId() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - mm.RegisterResourceType(typeof(Author)); - - // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(Author)); - - // Assert - Assert.AreSame(typeof(Author).GetProperty("Id"), idprop); - } - - [TestMethod] - public void Cant_register_model_with_missing_id() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - Action action = () => mm.RegisterResourceType(typeof(InvalidModel)); - - // Assert - action.ShouldThrow() - .Which.Message.Should() - .Be("Unable to determine Id property for type `invalid-models`."); - } - - [TestMethod] - public void FindsIdFromAttribute() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - mm.RegisterResourceType(typeof(CustomIdModel)); - - // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(CustomIdModel)); - - // Assert - Assert.AreSame(typeof(CustomIdModel).GetProperty("Uuid"), idprop); - } - - [TestMethod] - public void GetResourceTypeName_returns_correct_value_for_registered_types() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - mm.RegisterResourceType(typeof(Author)); - mm.RegisterResourceType(typeof(Comment)); - mm.RegisterResourceType(typeof(UserGroup)); - - // Act - var postKey = mm.GetResourceTypeNameForType(typeof(Post)); - var authorKey = mm.GetResourceTypeNameForType(typeof(Author)); - var commentKey = mm.GetResourceTypeNameForType(typeof(Comment)); - var manyCommentKey = mm.GetResourceTypeNameForType(typeof(Comment[])); - var userGroupsKey = mm.GetResourceTypeNameForType(typeof(UserGroup)); - - // Assert - Assert.AreEqual("posts", postKey); - Assert.AreEqual("authors", authorKey); - Assert.AreEqual("comments", commentKey); - Assert.AreEqual("comments", manyCommentKey); - Assert.AreEqual("user-groups", userGroupsKey); - } - - [TestMethod] - public void GetResourceTypeNameForType_gets_name_for_closest_registered_base_type_for_unregistered_type() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - - // Act - var resourceTypeName = mm.GetResourceTypeNameForType(typeof(DerivedPost)); - - // Assert - resourceTypeName.Should().Be("posts"); - } - - [TestMethod] - public void GetResourceTypeNameForType_fails_when_getting_unregistered_type() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - Action action = () => - { - mm.GetResourceTypeNameForType(typeof(Post)); - }; - - // Assert - action.ShouldThrow().WithMessage("The type `JSONAPI.Tests.Models.Post` was not registered."); - } - - [TestMethod] - public void GetTypeByResourceTypeName_returns_correct_value_for_registered_names() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - mm.RegisterResourceType(typeof(Author)); - mm.RegisterResourceType(typeof(Comment)); - mm.RegisterResourceType(typeof(UserGroup)); - - // Act - var postType = mm.GetTypeByResourceTypeName("posts"); - var authorType = mm.GetTypeByResourceTypeName("authors"); - var commentType = mm.GetTypeByResourceTypeName("comments"); - var userGroupType = mm.GetTypeByResourceTypeName("user-groups"); - - // Assert - postType.Should().Be(typeof (Post)); - authorType.Should().Be(typeof (Author)); - commentType.Should().Be(typeof (Comment)); - userGroupType.Should().Be(typeof (UserGroup)); - } - - [TestMethod] - public void GetTypeByResourceTypeName_fails_when_getting_unregistered_name() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - Action action = () => - { - mm.GetTypeByResourceTypeName("posts"); - }; - - // Assert - action.ShouldThrow().WithMessage("The resource type name `posts` was not registered."); - } - - [TestMethod] - public void TypeIsRegistered_returns_true_if_type_is_registered() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof (Post)); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof (Post)); - - // Assert - isRegistered.Should().BeTrue(); - } - - [TestMethod] - public void TypeIsRegistered_returns_true_if_parent_type_is_registered() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof(DerivedPost)); - - // Assert - isRegistered.Should().BeTrue(); - } - - [TestMethod] - public void TypeIsRegistered_returns_true_for_collection_of_registered_types() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); - - // Assert - isRegistered.Should().BeTrue(); - } - - [TestMethod] - public void TypeIsRegistered_returns_true_for_collection_of_children_of_registered_types() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - mm.RegisterResourceType(typeof(Post)); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); - - // Assert - isRegistered.Should().BeTrue(); - } - - [TestMethod] - public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof(Comment)); - - // Assert - isRegistered.Should().BeFalse(); - } - - [TestMethod] - public void TypeIsRegistered_returns_false_for_collection_of_unregistered_types() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - var isRegistered = mm.TypeIsRegistered(typeof(ICollection)); - - // Assert - isRegistered.Should().BeFalse(); - } - - [TestMethod] - public void GetJsonKeyForPropertyTest() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - var idKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Id")); - var nameKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Name")); - var postsKey = mm.CalculateJsonKeyForProperty(typeof(Author).GetProperty("Posts")); - - // Assert - Assert.AreEqual("id", idKey); - Assert.AreEqual("name", nameKey); - Assert.AreEqual("posts", postsKey); - - } - - [TestMethod] - public void GetPropertyForJsonKeyTest() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - Type authorType = typeof(Author); - mm.RegisterResourceType(authorType); - - // Act - var idProp = mm.GetPropertyForJsonKey(authorType, "id"); - var nameProp = mm.GetPropertyForJsonKey(authorType, "name"); - var postsProp = mm.GetPropertyForJsonKey(authorType, "posts"); - - // Assert - idProp.Property.Should().BeSameAs(authorType.GetProperty("Id")); - idProp.Should().BeOfType(); - - nameProp.Property.Should().BeSameAs(authorType.GetProperty("Name")); - nameProp.Should().BeOfType(); - - postsProp.Property.Should().BeSameAs(authorType.GetProperty("Posts")); - postsProp.Should().BeOfType(); - } - - [TestMethod] - public void GetPropertyForJsonKey_returns_correct_value_for_custom_id() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - Type bandType = typeof(Band); - mm.RegisterResourceType(bandType); - - // Act - var idProp = mm.GetPropertyForJsonKey(bandType, "id"); - - // Assert - idProp.Property.Should().BeSameAs(bandType.GetProperty("BandName")); - idProp.Should().BeOfType(); - } - - [TestMethod] - public void GetPropertyForJsonKey_returns_correct_value_for_JsonProperty_attribute() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - Type bandType = typeof(Band); - mm.RegisterResourceType(bandType); - - // Act - var prop = mm.GetPropertyForJsonKey(bandType, "THE-GENRE"); - - // Assert - prop.Property.Should().BeSameAs(bandType.GetProperty("Genre")); - prop.Should().BeOfType(); - } - - [TestMethod] - public void Cant_register_type_with_two_properties_with_the_same_name() - { - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - Type saladType = typeof(Salad); - - // Act - Action action = () => mm.RegisterResourceType(saladType); - - // Assert - action.ShouldThrow().Which.Message.Should().Be("The type `salads` already contains a property keyed at `salad-type`."); - } - - [TestMethod] - public void IsSerializedAsManyTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - bool isArray = mm.IsSerializedAsMany(typeof(Post[])); - bool isGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); - bool isString = mm.IsSerializedAsMany(typeof(string)); - bool isAuthor = mm.IsSerializedAsMany(typeof(Author)); - bool isNonGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); - - // Assert - Assert.IsTrue(isArray); - Assert.IsTrue(isGenericEnumerable); - Assert.IsFalse(isString); - Assert.IsFalse(isAuthor); - Assert.IsFalse(isNonGenericEnumerable); - } - - [TestMethod] - public void GetElementTypeTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - Type postTypeFromArray = mm.GetElementType(typeof(Post[])); - Type postTypeFromEnumerable = mm.GetElementType(typeof(IEnumerable)); - - // Assert - Assert.AreSame(typeof(Post), postTypeFromArray); - Assert.AreSame(typeof(Post), postTypeFromEnumerable); - } - - [TestMethod] - public void GetElementTypeInvalidArgumentTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - Type x = mm.GetElementType(typeof(Author)); - - // Assert - Assert.IsNull(x, "Return value of GetElementType should be null for a non-Many type argument!"); - } - } -} diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs new file mode 100644 index 00000000..773def7d --- /dev/null +++ b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs @@ -0,0 +1,902 @@ +using System; +using JSONAPI.Attributes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using System.Reflection; +using System.Collections.Generic; +using System.Collections; +using System.Diagnostics; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class ResourceTypeRegistryTests + { + private class InvalidModel // No Id discernable! + { + public string Data { get; set; } + } + + private class CustomIdModel + { + [UseAsId] + public Guid Uuid { get; set; } + + public string Data { get; set; } + } + + private class DerivedPost : Post + { + + } + + private class Salad + { + public string Id { get; set; } + + [JsonProperty("salad-type")] + public string TheSaladType { get; set; } + + [JsonProperty("salad-type")] + public string AnotherSaladType { get; set; } + } + + private class Continent + { + [UseAsId] + public string Name { get; set; } + + public string Id { get; set; } + } + + private class Boat + { + public string Id { get; set; } + + public string Type { get; set; } + } + + [TestMethod] + public void Cant_register_type_with_missing_id() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registry.RegisterResourceType(typeof(InvalidModel)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Unable to determine Id property for type `InvalidModel`."); + } + + [TestMethod] + public void Cant_register_type_with_non_id_property_called_id() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registry.RegisterResourceType(typeof(Continent)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Continent` because it contains a non-id property that would serialize as \"id\"."); + } + + [TestMethod] + public void Cant_register_type_with_property_called_type() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registry.RegisterResourceType(typeof(Boat)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Boat` because it contains a property that would serialize as \"type\"."); + } + + [TestMethod] + public void Cant_register_type_with_two_properties_with_the_same_name() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + Type saladType = typeof(Salad); + + // Act + Action action = () => registry.RegisterResourceType(saladType); + + // Assert + action.ShouldThrow().Which.Message.Should() + .Be("Failed to register type `Salad` because contains multiple properties that would serialize as `salad-type`."); + } + + [TestMethod] + public void RegisterResourceType_sets_up_registration_correctly() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(Post)); + var postReg = registry.GetRegistrationForType(typeof(Post)); + + // Assert + postReg.IdProperty.Should().BeSameAs(typeof(Post).GetProperty("Id")); + postReg.ResourceTypeName.Should().Be("posts"); + postReg.Attributes.Length.Should().Be(1); + postReg.Attributes.First().Property.Should().BeSameAs(typeof(Post).GetProperty("Title")); + postReg.Relationships.Length.Should().Be(2); + postReg.Relationships[0].IsToMany.Should().BeFalse(); + postReg.Relationships[0].Property.Should().BeSameAs(typeof(Post).GetProperty("Author")); + postReg.Relationships[0].SelfLinkTemplate.Should().BeNull(); + postReg.Relationships[0].RelatedResourceLinkTemplate.Should().BeNull(); + postReg.Relationships[1].IsToMany.Should().BeTrue(); + postReg.Relationships[1].Property.Should().BeSameAs(typeof(Post).GetProperty("Comments")); + postReg.Relationships[1].SelfLinkTemplate.Should().Be("/posts/{1}/relationships/comments"); + postReg.Relationships[1].RelatedResourceLinkTemplate.Should().Be("/posts/{1}/comments"); + } + + private AttributeGrabBag InitializeGrabBag() + { + return new AttributeGrabBag() + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SbyteField = 123, + NullableSbyteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + Uint16Field = 64000, + NullableUint16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + Uint32Field = 3000000000, + NullableUint32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + Uint64Field = 9223372036854775808, + NullableUint64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" + }; + } + + private void AssertAttribute(IResourceTypeRegistration reg, string attributeName, + JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func getPropertyFunc) + { + var grabBag = InitializeGrabBag(); + + var field = reg.GetFieldByName(attributeName); + var attribute = (ResourceTypeAttribute) field; + attribute.JsonKey.Should().Be(attributeName); + + attribute.SetValue(grabBag, tokenToSet); + var propertyValueAfterSet = getPropertyFunc(grabBag); + propertyValueAfterSet.Should().Be(expectedPropertyValue); + + var convertedToken = attribute.GetValue(grabBag); + if (expectedTokenAfterSet == null) + convertedToken.Should().BeNull(); + else + { + var convertedTokenValue = convertedToken.Value(); + convertedTokenValue.Should().Be(expectedTokenAfterSet); + } + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_boolean_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof (AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof (AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "boolean-field", false, false, false, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", true, true, true, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", null, false, false, g => g.BooleanField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_boolean_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof (AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof (AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-boolean-field", false, false, false, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", true, true, true, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", null, null, (Boolean?) null, g => g.NullableBooleanField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_SByte_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "sbyte-field", 0, 0, 0, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", 12, 12, 12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", -12, -12, -12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", null, 0, 0, g => g.SbyteField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_SByte_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-sbyte-field", 0, (SByte?)0, (SByte?)0, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", 12, (SByte?)12, (SByte?)12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", -12, (SByte?)-12, (SByte?)-12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", null, null, (SByte?)null, g => g.NullableSbyteField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Byte_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "byte-field", 0, 0, 0, g => g.ByteField); + AssertAttribute(reg, "byte-field", 12, 12, 12, g => g.ByteField); + AssertAttribute(reg, "byte-field", null, 0, 0, g => g.ByteField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Byte_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-byte-field", 0, (Byte?)0, (Byte?)0, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", 12, (Byte?)12, (Byte?)12, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", null, null, (Byte?)null, g => g.NullableByteField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Int16_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int16-field", 0, 0, 0, g => g.Int16Field); + AssertAttribute(reg, "int16-field", 4000, 4000, 4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", -4000, -4000, -4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", null, 0, 0, g => g.Int16Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int16_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int16-field", 0, (Int16?)0, (Int16?)0, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", 4000, (Int16?)4000, (Int16?)4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", -4000, (Int16?)-4000, (Int16?)-4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", null, null, (Int16?)null, g => g.NullableInt16Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_UInt16_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint16-field", 0, 0, 0, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", 4000, 4000, 4000, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", null, 0, 0, g => g.Uint16Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt16_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint16-field", 0, (UInt16?)0, (UInt16?)0, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", 4000, (UInt16?)4000, (UInt16?)4000, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", null, null, (UInt16?)null, g => g.NullableUint16Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Int32_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int32-field", 0, 0, 0, g => g.Int32Field); + AssertAttribute(reg, "int32-field", 2000000, 2000000, 2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", -2000000, -2000000, -2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", null, 0, 0, g => g.Int32Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int32_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int32-field", 0, 0, (Int32?)0, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", 2000000, 2000000, (Int32?)2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", -2000000, -2000000, (Int32?)-2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", null, null, (Int32?)null, g => g.NullableInt32Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_UInt32_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint32-field", 0, (UInt32)0, (UInt32)0, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", 2000000, (UInt32)2000000, (UInt32)2000000, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", null, (UInt32)0, (UInt32)0, g => g.Uint32Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt32_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint32-field", 0, (UInt32?)0, (UInt32?)0, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", 2000000, (UInt32?)2000000, (UInt32?)2000000, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", null, null, (UInt32?)null, g => g.NullableUint32Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Int64_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int64-field", 0, 0, 0, g => g.Int64Field); + AssertAttribute(reg, "int64-field", 20000000000, 20000000000, 20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", -20000000000, -20000000000, -20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", null, 0, 0, g => g.Int64Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int64_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int64-field", 0, 0, (Int64?)0, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", 20000000000, 20000000000, (Int64?)20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", -20000000000, -20000000000, (Int64?)-20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", null, null, (Int64?)null, g => g.NullableInt64Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_UInt64_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint64-field", 0, (UInt64)0, (UInt64)0, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", 20000000000, (UInt64)20000000000, (UInt64)20000000000, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", null, (UInt64)0, (UInt64)0, g => g.Uint64Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt64_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint64-field", 0, (UInt64?)0, (UInt64?)0, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", 20000000000, (UInt64?)20000000000, (UInt64?)20000000000, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", null, null, (UInt64?)null, g => g.NullableUint64Field); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Single_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "single-field", 0f, 0f, 0f, g => g.SingleField); + AssertAttribute(reg, "single-field", 20000000000.1234f, 20000000000.1234f, 20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", null, 0, 0, g => g.SingleField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Single_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-single-field", 0f, 0f, 0f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", 20000000000.1234f, 20000000000.1234f, (Int64?)20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", null, null, (Single?)null, g => g.NullableSingleField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Double_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "double-field", 0d, 0d, 0d, g => g.DoubleField); + AssertAttribute(reg, "double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.DoubleField); + AssertAttribute(reg, "double-field", null, 0d, 0d, g => g.DoubleField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Double_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-double-field", 0d, 0d, 0d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", null, null, (Double?)null, g => g.NullableDoubleField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_Decimal_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "decimal-field", "0", 0m, "0", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", null, 0m, "0", g => g.DecimalField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Decimal_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-decimal-field", "0", 0m, "0", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", null, null, (string)null, g => g.NullableDecimalField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_guid_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.GuidField); + AssertAttribute(reg, "guid-field", null, new Guid(), "00000000-0000-0000-0000-000000000000", g => g.GuidField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_guid_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "nullable-guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.NullableGuidField); + AssertAttribute(reg, "nullable-guid-field", null, null, (Guid?)null, g => g.NullableGuidField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_DateTime_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", null, new DateTime(), "0001-01-01T00:00:00", g => g.DateTimeField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_DateTime_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", null, null, (DateTime?)null, g => g.NullableDateTimeField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_DateTimeOffset_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset1 = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + var testDateTimeOffset2 = new DateTimeOffset(new DateTime(1776, 07, 04, 12, 30, 0), TimeSpan.FromHours(0)); + var testDateTimeOffset3 = new DateTimeOffset(new DateTime(2015, 03, 11, 04, 31, 0), TimeSpan.FromHours(0)); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset1, "1776-07-04T00:00:00.0000000-05:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T12:30:00+00:00", testDateTimeOffset2, "1776-07-04T12:30:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "2015-03-11T04:31:00.0000000+00:00", testDateTimeOffset3, "2015-03-11T04:31:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", null, new DateTimeOffset(), "0001-01-01T00:00:00.0000000+00:00", g => g.DateTimeOffsetField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_DateTimeOffset_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + AssertAttribute(reg, "nullable-date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset, "1776-07-04T00:00:00.0000000-05:00", + g => g.NullableDateTimeOffsetField); + AssertAttribute(reg, "nullable-date-time-offset-field", null, null, (DateTimeOffset?)null, + g => g.NullableDateTimeOffsetField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_string_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "string-field", "asdf", "asdf", "asdf", g => g.StringField); + AssertAttribute(reg, "string-field", null, null, (string)null, g => g.StringField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_enum_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.EnumField); + AssertAttribute(reg, "enum-field", null, (SampleEnum)0, 0, g => g.EnumField); + } + + [TestMethod] + public void RegisterResourceType_sets_up_correct_attribute_for_nullable_enum_field() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + registry.RegisterResourceType(typeof(AttributeGrabBag)); + var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField); + AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField); + } + + [TestMethod] + public void GetRegistrationForType_returns_correct_value_for_registered_types() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + registry.RegisterResourceType(typeof(Post)); + registry.RegisterResourceType(typeof(Author)); + registry.RegisterResourceType(typeof(Comment)); + registry.RegisterResourceType(typeof(UserGroup)); + + // Act + var postReg = registry.GetRegistrationForType(typeof(Post)); + var authorReg = registry.GetRegistrationForType(typeof(Author)); + var commentReg = registry.GetRegistrationForType(typeof(Comment)); + var userGroupReg = registry.GetRegistrationForType(typeof(UserGroup)); + + // Assert + postReg.ResourceTypeName.Should().Be("posts"); + authorReg.ResourceTypeName.Should().Be("authors"); + commentReg.ResourceTypeName.Should().Be("comments"); + userGroupReg.ResourceTypeName.Should().Be("user-groups"); + } + + [TestMethod] + public void GetRegistrationForType_gets_registration_for_closest_registered_base_type_for_unregistered_type() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + registry.RegisterResourceType(typeof(Post)); + + // Act + var registration = registry.GetRegistrationForType(typeof(DerivedPost)); + + // Assert + registration.Type.Should().Be(typeof(Post)); + } + + [TestMethod] + public void GetRegistrationForType_fails_when_getting_unregistered_type() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => + { + registry.GetRegistrationForType(typeof(Post)); + }; + + // Assert + action.ShouldThrow().WithMessage("No model registration was found for the type \"Post\"."); + } + + [TestMethod] + public void GetRegistrationForResourceTypeName_fails_when_getting_unregistered_type_name() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => + { + registry.GetRegistrationForResourceTypeName("posts"); + }; + + // Assert + action.ShouldThrow().WithMessage("No model registration was found for the type name \"posts\"."); + } + + [TestMethod] + public void GetModelRegistrationForResourceTypeName_returns_correct_value_for_registered_names() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + registry.RegisterResourceType(typeof(Post)); + registry.RegisterResourceType(typeof(Author)); + registry.RegisterResourceType(typeof(Comment)); + registry.RegisterResourceType(typeof(UserGroup)); + + // Act + var postReg = registry.GetRegistrationForResourceTypeName("posts"); + var authorReg = registry.GetRegistrationForResourceTypeName("authors"); + var commentReg = registry.GetRegistrationForResourceTypeName("comments"); + var userGroupReg = registry.GetRegistrationForResourceTypeName("user-groups"); + + // Assert + postReg.Type.Should().Be(typeof (Post)); + authorReg.Type.Should().Be(typeof (Author)); + commentReg.Type.Should().Be(typeof (Comment)); + userGroupReg.Type.Should().Be(typeof (UserGroup)); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_if_type_is_registered() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + registry.RegisterResourceType(typeof (Post)); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof (Post)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_if_parent_type_is_registered() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + registry.RegisterResourceType(typeof(Post)); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(DerivedPost)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(Comment)); + + // Assert + isRegistered.Should().BeFalse(); + } + + [TestMethod] + public void TypeIsRegistered_returns_false_for_collection_of_unregistered_types() + { + // Arrange + var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(ICollection)); + + // Assert + isRegistered.Should().BeFalse(); + } + } +} diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json deleted file mode 100644 index 2d13231b..00000000 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "data": [ - { - "type": "samples", - "id": "1", - "attributes": { - "booleanField": false, - "nullableBooleanField": false, - "sByteField": 0, - "nullableSByteField": null, - "byteField": 0, - "nullableByteField": null, - "int16Field": 0, - "nullableInt16Field": null, - "uInt16Field": 0, - "nullableUInt16Field": null, - "int32Field": 0, - "nullableInt32Field": null, - "uInt32Field": 0, - "nullableUInt32Field": null, - "int64Field": 0, - "nullableInt64Field": null, - "uInt64Field": 0, - "nullableUInt64Field": null, - "doubleField": 0.0, - "nullableDoubleField": null, - "singleField": 0.0, - "nullableSingleField": null, - "decimalField": 0.0, - "nullableDecimalField": null, - "dateTimeField": "0001-01-01T00:00:00", - "nullableDateTimeField": null, - "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", - "nullableDateTimeOffsetField": null, - "guidField": "00000000-0000-0000-0000-000000000000", - "nullableGuidField": null, - "stringField": null, - "enumField": 0, - "nullableEnumField": null - } - }, - { - "type": "samples", - "id": "2", - "attributes": { - "booleanField": true, - "nullableBooleanField": true, - "sByteField": 123, - "nullableSByteField": 123, - "byteField": 253, - "nullableByteField": 253, - "int16Field": 32000, - "nullableInt16Field": 32000, - "uInt16Field": 64000, - "nullableUInt16Field": 64000, - "int32Field": 2000000000, - "nullableInt32Field": 2000000000, - "uInt32Field": 3000000000, - "nullableUInt32Field": 3000000000, - "int64Field": 9223372036854775807, - "nullableInt64Field": 9223372036854775807, - "uInt64Field": 9223372036854775808, - "nullableUInt64Field": 9223372036854775808, - "doubleField": 1056789.123, - "nullableDoubleField": 1056789.123, - "singleField": 1056789.13, - "nullableSingleField": 1056789.13, - "decimalField": 1056789.123, - "nullableDecimalField": 1056789.123, - "dateTimeField": "1776-07-04T00:00:00", - "nullableDateTimeField": "1776-07-04T00:00:00", - "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", - "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", - "stringField": "Some string 156", - "enumField": 1, - "nullableEnumField": 2 - } - } - ] -} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 7b2d5664..3d9163a6 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -80,23 +80,41 @@ + + - + - - - + + - + + + + + + + + + + + + + + + + + + @@ -145,14 +163,62 @@ Always - - Always - Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs new file mode 100644 index 00000000..39803e65 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs @@ -0,0 +1,12 @@ +using Autofac.Core; + +namespace JSONAPI.Autofac.EntityFramework +{ + public static class JsonApiAutofacConfigurationExtensions + { + public static IModule GetEntityFrameworkAutofacModule(this JsonApiAutofacConfiguration jsonApiAutofacConfiguration) + { + return new JsonApiAutofacEntityFrameworkModule(); + } + } +} diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs new file mode 100644 index 00000000..38a825d6 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs @@ -0,0 +1,20 @@ +using Autofac; +using JSONAPI.ActionFilters; +using JSONAPI.EntityFramework; +using JSONAPI.EntityFramework.ActionFilters; +using JSONAPI.EntityFramework.Http; + +namespace JSONAPI.Autofac.EntityFramework +{ + public class JsonApiAutofacEntityFrameworkModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterGeneric(typeof(EntityFrameworkPayloadMaterializer<>)) + .AsImplementedInterfaces(); + builder.RegisterType() + .As(); + } + } +} diff --git a/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs b/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a30c9bd7 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac.EntityFramework")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac.EntityFramework")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5c85a230-f640-43cf-ae30-b685d237a6dc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac.EntityFramework/packages.config b/JSONAPI.Autofac.EntityFramework/packages.config new file mode 100644 index 00000000..f9427518 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..f7f46feb --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,28 @@ +using System; +using System.Data.Entity; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Payload; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp +{ + public class CustomEntityFrameworkResourceObjectMaterializer : EntityFrameworkResourceObjectMaterializer + { + public CustomEntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceTypeRegistry registry) : base(dbContext, registry) + { + } + + protected override Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration) + { + // This is to facilitate testing creation of a resource with a server-provided ID + if (typeRegistration.Type == typeof (Post) && String.IsNullOrEmpty(resourceObject.Id)) + { + ((Post) newObject).Id = "230"; + return Task.FromResult(0); + } + + return base.SetIdForNewResource(resourceObject, newObject, typeRegistration); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index 96a19c0b..5a55ebf8 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -126,6 +126,7 @@ + @@ -141,6 +142,10 @@ + + {64abe648-efcb-46ee-9e1a-e163f52bf372} + JSONAPI.Autofac.EntityFramework + {af7861f3-550b-4f70-a33e-1e5f48d39333} JSONAPI.Autofac diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index d6cb5ea7..cf370542 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -7,6 +7,7 @@ using Autofac; using Autofac.Integration.WebApi; using JSONAPI.Autofac; +using JSONAPI.Autofac.EntityFramework; using JSONAPI.Core; using JSONAPI.EntityFramework.Http; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; @@ -62,14 +63,16 @@ public void Configuration(IAppBuilder app) configuration.RegisterResourceType(typeof(User)); configuration.RegisterResourceType(typeof(UserGroup)); var module = configuration.GetAutofacModule(); + var efModule = configuration.GetEntityFrameworkAutofacModule(); var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(module); - containerBuilder.RegisterGeneric(typeof (EntityFrameworkPayloadMaterializer<>)) - .AsImplementedInterfaces(); + containerBuilder.RegisterModule(efModule); containerBuilder.Register(c => HttpContext.Current.GetOwinContext()).As(); containerBuilder.Register(c => c.Resolve().Get(DbContextKey)).AsSelf().As(); containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + containerBuilder.RegisterType() + .As(); var container = containerBuilder.Build(); var httpConfig = new HttpConfiguration diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs new file mode 100644 index 00000000..aa9911db --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class PostsTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Post_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\CreatingResources\Requests\Post_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\CreatingResources\Responses\Post_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == "205"); + actualPost.Id.Should().Be("205"); + actualPost.Title.Should().Be("Added post"); + actualPost.Content.Should().Be("Added post content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Post_with_empty_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\CreatingResources\Responses\Post_with_empty_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == "230"); + actualPost.Id.Should().Be("230"); + actualPost.Title.Should().Be("New post"); + actualPost.Content.Should().Be("The server generated my ID"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 04, 13, 12, 09, 0, new TimeSpan(0, 3, 0, 0))); + actualPost.AuthorId.Should().Be("401"); + } + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs new file mode 100644 index 00000000..9d08272f --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class DeletingResourcesTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Delete() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "posts/203"); + + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allTodos = dbContext.Posts.ToArray(); + allTodos.Length.Should().Be(3); + var actualTodo = allTodos.FirstOrDefault(t => t.Id == "203"); + actualTodo.Should().BeNull(); + } + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs new file mode 100644 index 00000000..18e4861e --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -0,0 +1,70 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.EntityFramework.Tests.Acceptance +{ + [TestClass] + public class FetchingResourcesTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetAll() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetAllResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetWithFilterResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task GetById() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/202"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetByIdResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\UserGroup.csv", @"Acceptance\Data")] + public async Task Get_dasherized_resource() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "user-groups"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_dasherized_resource.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PostRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json new file mode 100644 index 00000000..8d2759bd --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json @@ -0,0 +1,18 @@ +{ + "data": { + "type": "posts", + "attributes": { + "title": "New post", + "content": "The server generated my ID", + "created": "2015-04-13T12:09:00+03:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PostResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json new file mode 100644 index 00000000..8b7706da --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "230", + "attributes": { + "content": "The server generated my ID", + "created": "2015-04-13T12:09:00.0000000+03:00", + "title": "New post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/230/relationships/author", + "related": "https://www.example.com/posts/230/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/230/relationships/comments", + "related": "https://www.example.com/posts/230/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/230/relationships/tags", + "related": "https://www.example.com/posts/230/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetAllResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetAllResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetAllResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetByIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetByIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetByIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetWithFilterResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/GetWithFilterResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetWithFilterResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_dasherized_resource.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UserGroups/Responses/GetAllResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_dasherized_resource.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayForToOneLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithArrayRelationshipValueRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithAttributeUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToManyLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithMissingToOneLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullForToManyLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithNullToOneUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithObjectForToManyLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToManyLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringForToOneLinkageRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithStringRelationshipValueRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToManyUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Requests/PatchWithToOneUpdateRequest.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayForToOneLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithArrayRelationshipValueResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithAttributeUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToManyLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithMissingToOneLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullForToManyLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithNullToOneUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithObjectForToManyLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToManyLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringForToOneLinkageResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithStringRelationshipValueResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToManyUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PatchWithToOneUpdateResponse.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs similarity index 76% rename from JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs rename to JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs index 8986215d..b2a3817b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs @@ -10,84 +10,8 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance { [TestClass] - public class PostsTests : AcceptanceTestsBase + public class UpdatingResourcesTests : AcceptanceTestsBase { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetAll() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetAllResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetWithFilter() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetWithFilterResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetById() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/202"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\GetByIdResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Post() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts\Requests\PostRequest.json"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PostResponse.json", HttpStatusCode.OK); - - using (var dbContext = new TestDbContext(effortConnection, false)) - { - var allPosts = dbContext.Posts.ToArray(); - allPosts.Length.Should().Be(5); - var actualPost = allPosts.First(t => t.Id == "205"); - actualPost.Id.Should().Be("205"); - actualPost.Title.Should().Be("Added post"); - actualPost.Content.Should().Be("Added post content"); - actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); - actualPost.AuthorId.Should().Be("401"); - } - } - } - [TestMethod] [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] @@ -98,9 +22,9 @@ public async Task PatchWithAttributeUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithAttributeUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -127,9 +51,9 @@ public async Task PatchWithToManyUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -156,9 +80,9 @@ public async Task PatchWithToManyHomogeneousDataUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -185,9 +109,9 @@ public async Task PatchWithToManyEmptyLinkageUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -214,9 +138,9 @@ public async Task PatchWithToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -243,9 +167,9 @@ public async Task PatchWithNullToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithNullToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithNullToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -272,9 +196,9 @@ public async Task PatchWithMissingToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithMissingToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -301,9 +225,9 @@ public async Task PatchWithToOneLinkageObjectMissingId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -330,9 +254,9 @@ public async Task PatchWithToOneLinkageObjectMissingType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -359,9 +283,9 @@ public async Task PatchWithArrayForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayForToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithArrayForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -388,9 +312,9 @@ public async Task PatchWithStringForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -417,9 +341,9 @@ public async Task PatchWithMissingToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithMissingToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithMissingToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -446,9 +370,9 @@ public async Task PatchWithToManyLinkageObjectMissingId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -475,9 +399,9 @@ public async Task PatchWithToManyLinkageObjectMissingType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -504,9 +428,9 @@ public async Task PatchWithObjectForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithObjectForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithObjectForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -533,9 +457,9 @@ public async Task PatchWithStringForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) @@ -563,9 +487,9 @@ public async Task PatchWithNullForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithNullForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithNullForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -592,9 +516,9 @@ public async Task PatchWithArrayRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithArrayRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithArrayRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -621,9 +545,9 @@ public async Task PatchWithStringRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PatchWithStringRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -639,31 +563,5 @@ public async Task PatchWithStringRelationshipValue() } } } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Delete() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitDelete(effortConnection, "posts/203"); - - var responseContent = await response.Content.ReadAsStringAsync(); - responseContent.Should().Be(""); - response.StatusCode.Should().Be(HttpStatusCode.NoContent); - - using (var dbContext = new TestDbContext(effortConnection, false)) - { - var allTodos = dbContext.Posts.ToArray(); - allTodos.Length.Should().Be(3); - var actualTodo = allTodos.FirstOrDefault(t => t.Id == "203"); - actualTodo.Should().BeNull(); - } - } - } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs deleted file mode 100644 index 4e93bf78..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UserGroupsTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class UserGroupsTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\UserGroup.csv", @"Acceptance\Data")] - public async Task Get() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "user-groups"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\UserGroups\Responses\GetAllResponse.json", HttpStatusCode.OK); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 5c1785a6..a455e9ba 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -111,13 +111,15 @@ + + - - + + @@ -134,7 +136,7 @@ - + @@ -142,50 +144,50 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + Always - - + + @@ -196,15 +198,17 @@ + + Designer - - - - - - + + + + + + Always diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index dae900e2..bfa4a562 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -13,10 +13,9 @@ namespace JSONAPI.EntityFramework { /// - /// This class manages converting IResourceObject instances from a request into records managed - /// by Entity Framework. + /// Default implementation of IEntityFrameworkResourceObjectMaterializer /// - public class EntityFrameworkResourceObjectMaterializer + public class EntityFrameworkResourceObjectMaterializer : IEntityFrameworkResourceObjectMaterializer { private readonly DbContext _dbContext; private readonly IResourceTypeRegistry _registry; @@ -35,14 +34,6 @@ public EntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceT .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); } - /// - /// Gets a record managed by Entity Framework that has merged in the data from - /// the supplied resource object. - /// - /// - /// - /// - /// public async Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken) { var registration = _registry.GetRegistrationForResourceTypeName(resourceObject.Type); @@ -51,7 +42,7 @@ public async Task MaterializeResourceObject(IResourceObject resourceObje if (material == null) { material = Activator.CreateInstance(registration.Type); - registration.IdProperty.SetValue(material, resourceObject.Id); + await SetIdForNewResource(resourceObject, material, registration); _dbContext.Set(registration.Type).Add(material); } @@ -60,6 +51,16 @@ public async Task MaterializeResourceObject(IResourceObject resourceObje return material; } + /// + /// Allows implementers to control how a new resource's ID should be set. + /// + protected virtual Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration) + { + typeRegistration.IdProperty.SetValue(newObject, resourceObject.Id); + + return Task.FromResult(0); + } + /// /// Gets an existing record from the store by ID, if it exists /// @@ -165,8 +166,10 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO } } - // ReSharper disable once UnusedMember.Local - private void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) + /// + /// Sets the value of a to-many relationship + /// + protected void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) { // TODO: we need to fetch this property asynchronously first var currentValue = relationship.Property.GetValue(material); diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs index add2707f..bea5c487 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs @@ -23,6 +23,7 @@ public class EntityFrameworkPayloadMaterializer : IPayloadMaterializer whe private readonly IResourceTypeRegistry _resourceTypeRegistry; private readonly IQueryableResourceCollectionPayloadBuilder _queryableResourceCollectionPayloadBuilder; private readonly ISingleResourcePayloadBuilder _singleResourcePayloadBuilder; + private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; private readonly IBaseUrlService _baseUrlService; private readonly MethodInfo _getRelatedToManyMethod; private readonly MethodInfo _getRelatedToOneMethod; @@ -34,18 +35,21 @@ public class EntityFrameworkPayloadMaterializer : IPayloadMaterializer whe /// /// /// + /// /// public EntityFrameworkPayloadMaterializer( DbContext dbContext, IResourceTypeRegistry resourceTypeRegistry, IQueryableResourceCollectionPayloadBuilder queryableResourceCollectionPayloadBuilder, ISingleResourcePayloadBuilder singleResourcePayloadBuilder, + IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, IBaseUrlService baseUrlService) { _dbContext = dbContext; _resourceTypeRegistry = resourceTypeRegistry; _queryableResourceCollectionPayloadBuilder = queryableResourceCollectionPayloadBuilder; _singleResourcePayloadBuilder = singleResourcePayloadBuilder; + _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; _baseUrlService = baseUrlService; _getRelatedToManyMethod = GetType() .GetMethod("GetRelatedToMany", BindingFlags.NonPublic | BindingFlags.Instance); @@ -92,8 +96,8 @@ public virtual async Task CreateRecord(ISingleResourcePa { var apiBaseUrl = GetBaseUrlFromRequest(request); var newRecord = await MaterializeAsync(requestPayload.PrimaryData, cancellationToken); - var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, apiBaseUrl, null); await _dbContext.SaveChangesAsync(cancellationToken); + var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, apiBaseUrl, null); return returnPayload; } @@ -132,8 +136,7 @@ protected string GetBaseUrlFromRequest(HttpRequestMessage request) /// protected virtual async Task MaterializeAsync(IResourceObject resourceObject, CancellationToken cancellationToken) { - var materializer = new EntityFrameworkResourceObjectMaterializer(_dbContext, _resourceTypeRegistry); - return await materializer.MaterializeResourceObject(resourceObject, cancellationToken); + return await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken); } /// diff --git a/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..cd40207d --- /dev/null +++ b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Json; +using JSONAPI.Payload; + +namespace JSONAPI.EntityFramework +{ + /// + /// This class manages converting IResourceObject instances from a request into records managed + /// by Entity Framework. + /// + public interface IEntityFrameworkResourceObjectMaterializer + { + /// + /// Gets a record managed by Entity Framework that has merged in the data from + /// the supplied resource object. + /// + /// + /// + /// + /// + Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 8b398142..80540a61 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -74,6 +74,7 @@ + diff --git a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj index 7d5162bb..85ce0859 100644 --- a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj +++ b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj @@ -127,6 +127,10 @@ + + {64abe648-efcb-46ee-9e1a-e163f52bf372} + JSONAPI.Autofac.EntityFramework + {af7861f3-550b-4f70-a33e-1e5f48d39333} JSONAPI.Autofac diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index ae9c493c..d856048e 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -1,6 +1,7 @@ using System.Web.Http; using Autofac; using JSONAPI.Autofac; +using JSONAPI.Autofac.EntityFramework; using JSONAPI.Core; using JSONAPI.EntityFramework.Http; using JSONAPI.TodoMVC.API.Models; @@ -27,9 +28,11 @@ private static HttpConfiguration GetWebApiConfiguration() var configuration = new JsonApiAutofacConfiguration(namingConventions); configuration.RegisterResourceType(typeof(Todo)); var module = configuration.GetAutofacModule(); + var efModule = configuration.GetEntityFrameworkAutofacModule(); var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(module); + containerBuilder.RegisterModule(efModule); containerBuilder.RegisterGeneric(typeof(EntityFrameworkPayloadMaterializer<>)) .AsImplementedInterfaces(); var container = containerBuilder.Build(); diff --git a/JSONAPI.sln b/JSONAPI.sln index bc07ba33..4f7cf212 100644 --- a/JSONAPI.sln +++ b/JSONAPI.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.EntityFramework.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac", "JSONAPI.Autofac\JSONAPI.Autofac.csproj", "{AF7861F3-550B-4F70-A33E-1E5F48D39333}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac.EntityFramework", "JSONAPI.Autofac.EntityFramework\JSONAPI.Autofac.EntityFramework.csproj", "{64ABE648-EFCB-46EE-9E1A-E163F52BF372}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {AF7861F3-550B-4F70-A33E-1E5F48D39333}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF7861F3-550B-4F70-A33E-1E5F48D39333}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF7861F3-550B-4F70-A33E-1E5F48D39333}.Release|Any CPU.Build.0 = Release|Any CPU + {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/JSONAPI/Json/ResourceObjectSerializer.cs b/JSONAPI/Json/ResourceObjectSerializer.cs index 1cb53cb0..385ecf00 100644 --- a/JSONAPI/Json/ResourceObjectSerializer.cs +++ b/JSONAPI/Json/ResourceObjectSerializer.cs @@ -152,8 +152,6 @@ public async Task Deserialize(JsonReader reader, string current if (string.IsNullOrEmpty(type)) throw new DeserializationException("Resource object missing type", "Expected a value for `type`", currentPath + "/type"); - if (string.IsNullOrEmpty(id)) - throw new DeserializationException("Resource object missing id", "Expected a value for `id`", currentPath + "/id"); return new ResourceObject(type, id, attributes ?? new Dictionary(), From 1abdddc43a712125b0821e02042dd118bcbfceef Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 29 Jun 2015 20:37:04 -0400 Subject: [PATCH 100/186] don't fail on unknown fields; ignore instead --- .../Patch_with_unknown_attribute_Request.json | 10 ++++ ...tch_with_unknown_relationship_Request.json | 14 +++++ ...Patch_with_unknown_attribute_Response.json | 31 ++++++++++ ...ch_with_unknown_relationship_Response.json | 31 ++++++++++ .../Acceptance/UpdatingResourcesTests.cs | 58 +++++++++++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 4 ++ ...tityFrameworkResourceObjectMaterializer.cs | 7 ++- 7 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json new file mode 100644 index 00000000..3394d6df --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title", + "some-fake-attribute": 99 + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json new file mode 100644 index 00000000..1b4d99fb --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json @@ -0,0 +1,14 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title" + }, + "relationships": { + "some-fake-relationship": { + "data": { "type": "author", "id": "45000" } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json new file mode 100644 index 00000000..aba0d13d --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json new file mode 100644 index 00000000..aba0d13d --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs index b2a3817b..c2141bb4 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs @@ -41,6 +41,64 @@ public async Task PatchWithAttributeUpdate() } } + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Patch_with_unknown_attribute() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\Patch_with_unknown_attribute_Request.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\Patch_with_unknown_attribute_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Patch_with_unknown_relationship() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\Patch_with_unknown_relationship_Request.json"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\Patch_with_unknown_relationship_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + [TestMethod] [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index a455e9ba..a9a576ae 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -200,6 +200,10 @@ + + + + Designer diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index bfa4a562..7ff3b89a 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -87,7 +87,8 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO { foreach (var attributeValue in resourceObject.Attributes) { - var attribute = (ResourceTypeAttribute) registration.GetFieldByName(attributeValue.Key); + var attribute = registration.GetFieldByName(attributeValue.Key) as ResourceTypeAttribute; + if (attribute == null) continue; attribute.SetValue(material, attributeValue.Value); } @@ -95,7 +96,9 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO { var linkage = relationshipValue.Value.Linkage; - var typeRelationship = (ResourceTypeRelationship) registration.GetFieldByName(relationshipValue.Key); + var typeRelationship = registration.GetFieldByName(relationshipValue.Key) as ResourceTypeRelationship; + if (typeRelationship == null) continue; + if (typeRelationship.IsToMany) { if (linkage == null) From f13058b69c5778dbe60774cccae17d92b509c635 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 29 Jun 2015 23:29:05 -0400 Subject: [PATCH 101/186] include relationships when materializing --- ...tityFrameworkResourceObjectMaterializer.cs | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index 7ff3b89a..65115081 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -20,6 +21,7 @@ public class EntityFrameworkResourceObjectMaterializer : IEntityFrameworkResourc private readonly DbContext _dbContext; private readonly IResourceTypeRegistry _registry; private readonly MethodInfo _openSetToManyRelationshipValueMethod; + private readonly MethodInfo _openGetExistingRecordGenericMethod; /// /// Creates a new EntityFrameworkEntityFrameworkResourceObjectMaterializer @@ -32,13 +34,25 @@ public EntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceT _registry = registry; _openSetToManyRelationshipValueMethod = GetType() .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); + _openGetExistingRecordGenericMethod = GetType() + .GetMethod("GetExistingRecordGeneric", BindingFlags.NonPublic | BindingFlags.Instance); } public async Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken) { var registration = _registry.GetRegistrationForResourceTypeName(resourceObject.Type); + + var relationshipsToInclude = new List(); + if (resourceObject.Relationships != null) + { + relationshipsToInclude.AddRange( + resourceObject.Relationships + .Select(relationshipObject => registration.GetFieldByName(relationshipObject.Key)) + .OfType()); + } - var material = await GetExistingRecord(registration, resourceObject.Id, cancellationToken); + + var material = await GetExistingRecord(registration, resourceObject.Id, relationshipsToInclude.ToArray(), cancellationToken); if (material == null) { material = Activator.CreateInstance(registration.Type); @@ -64,13 +78,13 @@ protected virtual Task SetIdForNewResource(IResourceObject resourceObject, objec /// /// Gets an existing record from the store by ID, if it exists /// - /// - /// - /// /// - protected virtual Task GetExistingRecord(IResourceTypeRegistration registration, string id, CancellationToken cancellationToken) + protected virtual async Task GetExistingRecord(IResourceTypeRegistration registration, string id, + ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) { - return _dbContext.Set(registration.Type).FindAsync(cancellationToken, id); + var method = _openGetExistingRecordGenericMethod.MakeGenericMethod(registration.Type); + var result = (dynamic) method.Invoke(this, new object[] {registration, id, relationshipsToInclude, cancellationToken}); + return await result; } /// @@ -127,7 +141,7 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO var relatedId = resourceIdentifierObject["id"].Value(); var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); - var relatedObject = await GetExistingRecord(relatedObjectRegistration, relatedId, cancellationToken); + var relatedObject = await GetExistingRecord(relatedObjectRegistration, relatedId, null, cancellationToken); newCollection.Add(relatedObject); } @@ -142,9 +156,6 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO if (linkage.LinkageToken == null) { - // For some reason we have to get the value first, or else setting it to null does nothing. - // TODO: This will cause a synchronous query. We can get rid of this line entirely by using Include when the object is first fetched. - typeRelationship.Property.GetValue(material); typeRelationship.Property.SetValue(material, null); } else @@ -161,7 +172,7 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); var relatedObject = - await GetExistingRecord(relatedObjectRegistration, relatedId, cancellationToken); + await GetExistingRecord(relatedObjectRegistration, relatedId, null, cancellationToken); typeRelationship.Property.SetValue(material, relatedObject); } @@ -169,12 +180,32 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO } } + /// + /// Gets a record by ID + /// + protected async Task GetExistingRecordGeneric(IResourceTypeRegistration registration, + string id, ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) where TRecord : class + { + var param = Expression.Parameter(registration.Type); + var filterExpression = registration.GetFilterByIdExpression(param, id); + var lambda = Expression.Lambda>(filterExpression, param); + var query = _dbContext.Set().AsQueryable() + .Where(lambda); + + if (relationshipsToInclude != null) + { + query = relationshipsToInclude.Aggregate(query, + (current, resourceTypeRelationship) => current.Include(resourceTypeRelationship.Property.Name)); + } + + return await query.FirstOrDefaultAsync(cancellationToken); + } + /// /// Sets the value of a to-many relationship /// protected void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) { - // TODO: we need to fetch this property asynchronously first var currentValue = relationship.Property.GetValue(material); var typedArray = relatedObjects.Select(o => (TRelated) o).ToArray(); if (relationship.Property.PropertyType.IsAssignableFrom(typeof (List))) From e34d6f9e413b52e26de84070b23768c13e937a0a Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 30 Jun 2015 15:32:37 -0400 Subject: [PATCH 102/186] separate out value converters into files --- .../Core/ComplexAttributeValueConverter.cs | 36 +++ .../Core/DateTimeAttributeValueConverter.cs | 48 +++ .../DateTimeOffsetAttributeValueConverter.cs | 49 +++ .../Core/DecimalAttributeValueConverter.cs | 46 +++ JSONAPI/Core/EnumAttributeValueConverter.cs | 57 ++++ JSONAPI/Core/GuidAttributeValueConverter.cs | 52 ++++ JSONAPI/Core/IAttributeValueConverter.cs | 293 ------------------ .../PrimitiveTypeAttributeValueConverter.cs | 43 +++ JSONAPI/JSONAPI.csproj | 7 + 9 files changed, 338 insertions(+), 293 deletions(-) create mode 100644 JSONAPI/Core/ComplexAttributeValueConverter.cs create mode 100644 JSONAPI/Core/DateTimeAttributeValueConverter.cs create mode 100644 JSONAPI/Core/DateTimeOffsetAttributeValueConverter.cs create mode 100644 JSONAPI/Core/DecimalAttributeValueConverter.cs create mode 100644 JSONAPI/Core/EnumAttributeValueConverter.cs create mode 100644 JSONAPI/Core/GuidAttributeValueConverter.cs create mode 100644 JSONAPI/Core/PrimitiveTypeAttributeValueConverter.cs diff --git a/JSONAPI/Core/ComplexAttributeValueConverter.cs b/JSONAPI/Core/ComplexAttributeValueConverter.cs new file mode 100644 index 00000000..ab892c97 --- /dev/null +++ b/JSONAPI/Core/ComplexAttributeValueConverter.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use with complex attributes. + /// + public class ComplexAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + + /// + /// Creates a new ComplexAttributeValueConverter + /// + /// + public ComplexAttributeValueConverter(PropertyInfo property) + { + _property = property; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value == null) return null; + return JToken.Parse(value.ToString()); + } + + public void SetValue(object resource, JToken value) + { + var serializedValue = value.ToString(); // TODO: this won't work if this converter is used for non-string properties + _property.SetValue(resource, serializedValue); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/DateTimeAttributeValueConverter.cs b/JSONAPI/Core/DateTimeAttributeValueConverter.cs new file mode 100644 index 00000000..ca3515ff --- /dev/null +++ b/JSONAPI/Core/DateTimeAttributeValueConverter.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use converting between DateTime CLR properties and ISO8601 string values. + /// + public class DateTimeAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + private readonly bool _isNullable; + + /// + /// Creates a new DateTimeAttributeValueConverter + /// + /// + /// + public DateTimeAttributeValueConverter(PropertyInfo property, bool isNullable) + { + _property = property; + _isNullable = isNullable; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value != null) return ((DateTime) value).ToString("s"); + if (_isNullable) return null; + return "0001-01-01"; + } + + public void SetValue(object resource, JToken value) + { + if (value == null || value.Type == JTokenType.Null) + { + _property.SetValue(resource, _isNullable ? (DateTime?)null : new DateTime()); + } + else + { + var dateTimeValue = value.Value(); + _property.SetValue(resource, dateTimeValue); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/DateTimeOffsetAttributeValueConverter.cs b/JSONAPI/Core/DateTimeOffsetAttributeValueConverter.cs new file mode 100644 index 00000000..82eac86f --- /dev/null +++ b/JSONAPI/Core/DateTimeOffsetAttributeValueConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use converting between DateTimeOffset CLR properties and ISO8601 string values. + /// + public class DateTimeOffsetAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + private readonly bool _isNullable; + + /// + /// Creates a new DateTimeOffsetAttributeValueConverter + /// + /// + /// + public DateTimeOffsetAttributeValueConverter(PropertyInfo property, bool isNullable) + { + _property = property; + _isNullable = isNullable; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value != null) return ((DateTimeOffset)value).ToString("o"); + if (_isNullable) return null; + return "0001-01-01T00:00:00Z"; + } + + public void SetValue(object resource, JToken value) + { + if (value == null || value.Type == JTokenType.Null) + { + _property.SetValue(resource, _isNullable ? (DateTimeOffset?)null : new DateTimeOffset()); + } + else + { + var stringValue = value.Value(); + var dateTimeOffsetValue = DateTimeOffset.Parse(stringValue); + _property.SetValue(resource, dateTimeOffsetValue); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/DecimalAttributeValueConverter.cs b/JSONAPI/Core/DecimalAttributeValueConverter.cs new file mode 100644 index 00000000..532c9e29 --- /dev/null +++ b/JSONAPI/Core/DecimalAttributeValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use converting between decimal CLR properties and string attributes. + /// + public class DecimalAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + + /// + /// Creates a new DecimalAttributeValueConverter + /// + /// + public DecimalAttributeValueConverter(PropertyInfo property) + { + _property = property; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value == null) return null; + return value.ToString(); + } + + public void SetValue(object resource, JToken value) + { + if (value == null) + _property.SetValue(resource, null); + else + { + var stringTokenValue = value.Value(); + Decimal d; + if (!Decimal.TryParse(stringTokenValue, out d)) + throw new JsonSerializationException("Could not parse decimal value."); + _property.SetValue(resource, d); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/EnumAttributeValueConverter.cs b/JSONAPI/Core/EnumAttributeValueConverter.cs new file mode 100644 index 00000000..2d7a8f44 --- /dev/null +++ b/JSONAPI/Core/EnumAttributeValueConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use converting between enum CLR properties and integer attributes. + /// + public class EnumAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + private readonly Type _enumType; + private readonly bool _isNullable; + + /// + /// Creates a new EnumAttributeValueConverter + /// + /// + /// + /// + public EnumAttributeValueConverter(PropertyInfo property, Type enumType, bool isNullable) + { + _property = property; + _enumType = enumType; + _isNullable = isNullable; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value != null) return (int) value; + if (_isNullable) return null; + return 0; + } + + public void SetValue(object resource, JToken value) + { + if (value == null) + { + if (_isNullable) + _property.SetValue(resource, null); + else + { + var enumValue = Enum.Parse(_enumType, "0"); + _property.SetValue(resource, enumValue); + } + } + else + { + var enumValue = Enum.Parse(_enumType, value.ToString()); + _property.SetValue(resource, enumValue); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/GuidAttributeValueConverter.cs b/JSONAPI/Core/GuidAttributeValueConverter.cs new file mode 100644 index 00000000..b6d6f2d5 --- /dev/null +++ b/JSONAPI/Core/GuidAttributeValueConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use converting between Guid CLR properties and string attributes. + /// + public class GuidAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + private readonly bool _isNullable; + + /// + /// Creates a new GuidAttributeValueConverter + /// + /// + /// + public GuidAttributeValueConverter(PropertyInfo property, bool isNullable) + { + _property = property; + _isNullable = isNullable; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value == null) + { + if (_isNullable) return null; + value = new Guid(); + } + return value.ToString(); + } + + public void SetValue(object resource, JToken value) + { + if (value == null) + { + _property.SetValue(resource, _isNullable ? (Guid?)null : new Guid()); + } + else + { + var stringTokenValue = value.Value(); + var guidValue = new Guid(stringTokenValue); + _property.SetValue(resource, guidValue); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/IAttributeValueConverter.cs b/JSONAPI/Core/IAttributeValueConverter.cs index 7871d437..79c47648 100644 --- a/JSONAPI/Core/IAttributeValueConverter.cs +++ b/JSONAPI/Core/IAttributeValueConverter.cs @@ -1,6 +1,3 @@ -using System; -using System.Reflection; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JSONAPI.Core @@ -24,294 +21,4 @@ public interface IAttributeValueConverter /// void SetValue(object resource, JToken value); } - - /// - /// Implementation of suitable for - /// primitive types. - /// - public class PrimitiveTypeAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - - /// - /// Creates a new PrimitiveTypeAttributeValueConverter - /// - /// - public PrimitiveTypeAttributeValueConverter(PropertyInfo property) - { - _property = property; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value == null) return null; - return JToken.FromObject(value); - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - { - _property.SetValue(resource, null); - } - else - { - var unpackedValue = value.Value(); - _property.SetValue(resource, unpackedValue); - } - } - } - - /// - /// Implementation of suitable for - /// use with complex attributes. - /// - public class ComplexAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - - /// - /// Creates a new ComplexAttributeValueConverter - /// - /// - public ComplexAttributeValueConverter(PropertyInfo property) - { - _property = property; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value == null) return null; - return JToken.Parse(value.ToString()); - } - - public void SetValue(object resource, JToken value) - { - var serializedValue = value.ToString(); // TODO: this won't work if this converter is used for non-string properties - _property.SetValue(resource, serializedValue); - } - } - - /// - /// Implementation of suitable for - /// use converting between decimal CLR properties and string attributes. - /// - public class DecimalAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - - /// - /// Creates a new DecimalAttributeValueConverter - /// - /// - public DecimalAttributeValueConverter(PropertyInfo property) - { - _property = property; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value == null) return null; - return value.ToString(); - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - _property.SetValue(resource, null); - else - { - var stringTokenValue = value.Value(); - Decimal d; - if (!Decimal.TryParse(stringTokenValue, out d)) - throw new JsonSerializationException("Could not parse decimal value."); - _property.SetValue(resource, d); - } - } - } - - /// - /// Implementation of suitable for - /// use converting between Guid CLR properties and string attributes. - /// - public class GuidAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - private readonly bool _isNullable; - - /// - /// Creates a new GuidAttributeValueConverter - /// - /// - /// - public GuidAttributeValueConverter(PropertyInfo property, bool isNullable) - { - _property = property; - _isNullable = isNullable; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value == null) - { - if (_isNullable) return null; - value = new Guid(); - } - return value.ToString(); - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - { - _property.SetValue(resource, _isNullable ? (Guid?)null : new Guid()); - } - else - { - var stringTokenValue = value.Value(); - var guidValue = new Guid(stringTokenValue); - _property.SetValue(resource, guidValue); - } - } - } - - /// - /// Implementation of suitable for - /// use converting between enum CLR properties and integer attributes. - /// - public class EnumAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - private readonly Type _enumType; - private readonly bool _isNullable; - - /// - /// Creates a new EnumAttributeValueConverter - /// - /// - /// - /// - public EnumAttributeValueConverter(PropertyInfo property, Type enumType, bool isNullable) - { - _property = property; - _enumType = enumType; - _isNullable = isNullable; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value != null) return (int) value; - if (_isNullable) return null; - return 0; - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - { - if (_isNullable) - _property.SetValue(resource, null); - else - { - var enumValue = Enum.Parse(_enumType, "0"); - _property.SetValue(resource, enumValue); - } - } - else - { - var enumValue = Enum.Parse(_enumType, value.ToString()); - _property.SetValue(resource, enumValue); - } - } - } - - /// - /// Implementation of suitable for - /// use converting between DateTime CLR properties and ISO8601 string values. - /// - public class DateTimeAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - private readonly bool _isNullable; - - /// - /// Creates a new DateTimeAttributeValueConverter - /// - /// - /// - public DateTimeAttributeValueConverter(PropertyInfo property, bool isNullable) - { - _property = property; - _isNullable = isNullable; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value != null) return ((DateTime) value).ToString("s"); - if (_isNullable) return null; - return "0001-01-01"; - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - { - _property.SetValue(resource, _isNullable ? (DateTime?)null : new DateTime()); - } - else - { - var dateTimeValue = value.Value(); - _property.SetValue(resource, dateTimeValue); - } - } - } - - /// - /// Implementation of suitable for - /// use converting between DateTimeOffset CLR properties and ISO8601 string values. - /// - public class DateTimeOffsetAttributeValueConverter : IAttributeValueConverter - { - private readonly PropertyInfo _property; - private readonly bool _isNullable; - - /// - /// Creates a new DateTimeOffsetAttributeValueConverter - /// - /// - /// - public DateTimeOffsetAttributeValueConverter(PropertyInfo property, bool isNullable) - { - _property = property; - _isNullable = isNullable; - } - - public JToken GetValue(object resource) - { - var value = _property.GetValue(resource); - if (value != null) return ((DateTimeOffset)value).ToString("o"); - if (_isNullable) return null; - return "0001-01-01T00:00:00Z"; - } - - public void SetValue(object resource, JToken value) - { - if (value == null) - { - _property.SetValue(resource, _isNullable ? (DateTimeOffset?)null : new DateTimeOffset()); - } - else - { - var stringValue = value.Value(); - var dateTimeOffsetValue = DateTimeOffset.Parse(stringValue); - _property.SetValue(resource, dateTimeOffsetValue); - } - } - } } \ No newline at end of file diff --git a/JSONAPI/Core/PrimitiveTypeAttributeValueConverter.cs b/JSONAPI/Core/PrimitiveTypeAttributeValueConverter.cs new file mode 100644 index 00000000..b245ee9e --- /dev/null +++ b/JSONAPI/Core/PrimitiveTypeAttributeValueConverter.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// primitive types. + /// + public class PrimitiveTypeAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + + /// + /// Creates a new PrimitiveTypeAttributeValueConverter + /// + /// + public PrimitiveTypeAttributeValueConverter(PropertyInfo property) + { + _property = property; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value == null) return null; + return JToken.FromObject(value); + } + + public void SetValue(object resource, JToken value) + { + if (value == null) + { + _property.SetValue(resource, null); + } + else + { + var unpackedValue = value.Value(); + _property.SetValue(resource, unpackedValue); + } + } + } +} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 30319ef8..72292309 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -81,12 +81,19 @@ + + + + + + + From 2963d608165102aaf4d614a1f8210989ee55368e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 13:50:07 -0400 Subject: [PATCH 103/186] rename primitives and general cleanup Payload has been replaced with Document. Serializer has been replaced with Formatter. Purged the last few instances of Model Manager and Metadata Manager Cleared up some Resharper warnings --- .../JsonApiAutofacEntityFrameworkModule.cs | 2 +- .../JsonApiAutofacConfiguration.cs | 2 +- JSONAPI.Autofac/JsonApiAutofacModule.cs | 36 +- .../Controllers/CommentsController.cs | 2 +- .../LanguageUserLinksController.cs | 4 +- .../Controllers/PostsController.cs | 2 +- .../Controllers/PresidentsController.cs | 15 +- .../Controllers/TagsController.cs | 2 +- .../Controllers/UserGroupsController.cs | 2 +- .../Controllers/UsersController.cs | 2 +- ...tityFrameworkResourceObjectMaterializer.cs | 2 +- .../{PayloadTests.cs => DocumentTests.cs} | 6 +- ..._returns_IResourceCollectionDocument.json} | 0 .../JSONAPI.EntityFramework.Tests.csproj | 4 +- ...tityFrameworkResourceObjectMaterializer.cs | 4 +- ...=> EntityFrameworkDocumentMaterializer.cs} | 68 +- ...tityFrameworkResourceObjectMaterializer.cs | 2 +- .../JSONAPI.EntityFramework.csproj | 2 +- .../DefaultPaginationTransformerTests.cs | 2 +- .../DefaultSortingTransformerTests.cs | 2 +- ... FallbackDocumentBuilderAttributeTests.cs} | 84 +-- JSONAPI.Tests/Core/MetadataManagerTests.cs | 41 -- .../Data/ByteIdSerializationTest.json | 25 - .../Data/DeserializeAttributeRequest.json | 82 --- .../Data/DeserializeCollectionRequest.json | 46 -- .../Data/DeserializeRawJsonTest.json | 14 - JSONAPI.Tests/Data/EmptyArrayResult.json | 3 - JSONAPI.Tests/Data/ErrorSerializerTest.json | 19 - .../Data/FormatterErrorSerializationTest.json | 1 - JSONAPI.Tests/Data/LinkTemplateTest.json | 20 - .../Data/MalformedRawJsonString.json | 17 - ...adataManagerPropertyWasPresentRequest.json | 14 - JSONAPI.Tests/Data/NonStandardIdTest.json | 11 - JSONAPI.Tests/Data/NullResourceResult.json | 3 - ...eformatsRawJsonStringWithUnquotedKeys.json | 19 - .../Data/SerializerIntegrationTest.json | 186 ----- .../Builders/ErrorDocumentBuilderTests.cs} | 48 +- .../Builders/FallbackDocumentBuilderTests.cs | 128 ++++ .../RegistryDrivenDocumentBuilderTests.cs} | 22 +- ...ivenSingleResourceDocumentBuilderTests.cs} | 38 +- .../DefaultLinkConventionsTests.cs | 4 +- .../ToManyResourceLinkageTests.cs | 4 +- .../ToOneResourceLinkageTests.cs | 4 +- JSONAPI.Tests/JSONAPI.Tests.csproj | 175 ++--- ...ests.cs => ErrorDocumentFormatterTests.cs} | 22 +- ...ializerTests.cs => ErrorFormatterTests.cs} | 20 +- .../Serialize_ErrorDocument.json} | 0 ...alize_error_with_all_possible_members.json | 0 .../Serialize_error_with_only_id.json | 0 .../Serialize_ErrorDocument.json | 1 + .../Serialize_ErrorPayload.json | 1 - .../JsonApiFormatter/Serialize_HttpError.json | 2 +- .../Serialize_ResourceCollectionDocument.json | 1 + .../Serialize_ResourceCollectionPayload.json | 1 - .../Serialize_SingleResourceDocument.json | 1 + .../Serialize_SingleResourcePayload.json | 1 - .../Serialize_link_with_metadata.json | 0 .../Serialize_link_without_metadata.json | 0 .../Deserialize_metadata.json | 0 .../Deserialize_null_metadata.json | 0 .../Serialize_metadata.json | 0 .../Serialize_null_metadata.json | 0 .../Deserialize_relationship_object.json | 0 ...elationship_with_all_possible_members.json | 0 ...ialize_relationship_with_linkage_only.json | 0 ...Serialize_relationship_with_meta_only.json | 0 ...e_relationship_with_related_link_only.json | 0 ...nship_with_self_link_and_related_link.json | 0 ...lize_relationship_with_self_link_only.json | 0 .../Deserialize_document_with_metadata.json} | 0 ...serialize_document_with_primary_data.json} | 0 ...imary_data_and_unknown_top_level_key.json} | 0 .../Deserialize_empty_document.json} | 0 ...ionDocument_for_all_possible_members.json} | 0 ...ectionDocument_for_primary_data_only.json} | 0 ...t_for_primary_data_only_and_metadata.json} | 0 .../Deserialize_fails_on_integer.json | 0 .../Deserialize_fails_on_string.json | 0 .../Deserialize_null_to_one_linkage.json | 0 .../Deserialize_to_many_linkage.json | 0 .../Deserialize_to_one_linkage.json | 0 .../Serialize_ToOneResourceLinkage.json | 0 .../Serialize_linkage.json | 0 .../Serialize_null_linkage.json | 0 .../Deserialize_resource_object.json | 0 ...or_resource_with_all_possible_members.json | 0 ...ceObject_for_resource_with_attributes.json | 0 ...esourceObject_for_resource_with_links.json | 0 ...urceObject_for_resource_with_metadata.json | 0 ...resource_with_only_null_relationships.json | 0 ...bject_for_resource_with_relationships.json | 0 ...g_integer_greater_than_int64_maxvalue.json | 0 ...bject_for_resource_without_attributes.json | 0 .../Deserialize_document_with_resource.json} | 0 .../Deserialize_null_document.json} | 0 ...rceDocument_for_all_possible_members.json} | 0 ...cument_for_primary_data_and_metadata.json} | 0 ...sourceDocument_for_primary_data_only.json} | 0 JSONAPI.Tests/Json/JsonApiFormatterTests.cs | 683 ++---------------- ...tsBase.cs => JsonApiFormatterTestsBase.cs} | 20 +- ...rializerTests.cs => LinkFormatterTests.cs} | 19 +- ...izerTests.cs => MetadataFormatterTests.cs} | 29 +- ...cs => RelationshipObjectFormatterTests.cs} | 103 ++- ...esourceCollectionDocumentFormatterTests.cs | 252 +++++++ ...esourceCollectionPayloadSerializerTests.cs | 252 ------- ...ts.cs => ResourceLinkageFormatterTests.cs} | 44 +- ...sts.cs => ResourceObjectFormatterTests.cs} | 96 +-- .../SingleResourceDocumentFormatterTests.cs | 164 +++++ .../SingleResourcePayloadSerializerTests.cs | 164 ----- .../Builders/FallbackPayloadBuilderTests.cs | 134 ---- .../Controllers/TodosController.cs | 2 +- JSONAPI.TodoMVC.API/Startup.cs | 2 +- .../DefaultFilteringTransformer.cs | 4 +- .../DefaultPaginationTransformer.cs | 2 +- .../DefaultSortingTransformer.cs | 6 +- ...cs => FallbackDocumentBuilderAttribute.cs} | 57 +- .../JsonApiExceptionFilterAttribute.cs | 20 +- JSONAPI/Core/JsonApiConfiguration.cs | 44 +- JSONAPI/Core/JsonApiHttpConfiguration.cs | 10 +- JSONAPI/Core/ResourceTypeRegistry.cs | 4 +- ...yableResourceCollectionDocumentBuilder.cs} | 29 +- .../Builders/ErrorDocumentBuilder.cs} | 20 +- .../Builders/FallbackDocumentBuilder.cs | 109 +++ .../Builders/IErrorDocumentBuilder.cs | 22 + .../Builders/IFallbackDocumentBuilder.cs} | 12 +- ...yableResourceCollectionDocumentBuilder.cs} | 12 +- .../IResourceCollectionDocumentBuilder.cs} | 10 +- .../ISingleResourceDocumentBuilder.cs} | 10 +- .../Builders/JsonApiException.cs | 6 +- .../RegistryDrivenDocumentBuilder.cs} | 10 +- ...rivenResourceCollectionDocumentBuilder.cs} | 16 +- ...tryDrivenSingleResourceDocumentBuilder.cs} | 16 +- .../DefaultLinkConventions.cs | 2 +- JSONAPI/{Payload => Documents}/Error.cs | 2 +- .../ErrorDocument.cs} | 10 +- .../ExceptionErrorMetadata.cs | 2 +- JSONAPI/{Payload => Documents}/IError.cs | 2 +- JSONAPI/Documents/IErrorDocument.cs | 13 + JSONAPI/Documents/IJsonApiDocument.cs | 13 + JSONAPI/{Payload => Documents}/ILink.cs | 2 +- .../ILinkConventions.cs | 5 +- JSONAPI/{Payload => Documents}/IMetadata.cs | 2 +- .../IRelationshipObject.cs | 2 +- .../IResourceCollectionDocument.cs} | 8 +- JSONAPI/Documents/IResourceIdentifier.cs | 18 + .../IResourceLinkage.cs | 2 +- .../{Payload => Documents}/IResourceObject.cs | 2 +- .../ISingleResourceDocument.cs} | 8 +- .../RelationshipObject.cs | 2 +- .../Documents/ResourceCollectionDocument.cs | 22 + .../ResourceIdentifier.cs} | 18 +- .../{Payload => Documents}/ResourceObject.cs | 2 +- JSONAPI/Documents/SingleResourceDocument.cs | 24 + .../ToManyResourceLinkage.cs | 2 +- .../ToOneResourceLinkage.cs | 2 +- ...terializer.cs => IDocumentMaterializer.cs} | 24 +- JSONAPI/Http/JsonApiController.cs | 50 +- JSONAPI/JSONAPI.csproj | 113 ++- JSONAPI/Json/BasicMetadata.cs | 2 +- JSONAPI/Json/DeserializationException.cs | 4 +- JSONAPI/Json/ErrorDocumentFormatter.cs | 55 ++ .../{ErrorSerializer.cs => ErrorFormatter.cs} | 26 +- JSONAPI/Json/ErrorPayloadSerializer.cs | 55 -- JSONAPI/Json/IErrorDocumentFormatter.cs | 11 + ...IErrorSerializer.cs => IErrorFormatter.cs} | 4 +- JSONAPI/Json/IErrorPayloadSerializer.cs | 11 - ...nApiSerializer.cs => IJsonApiFormatter.cs} | 6 +- .../{ILinkSerializer.cs => ILinkFormatter.cs} | 4 +- ...ataSerializer.cs => IMetadataFormatter.cs} | 4 +- ...zer.cs => IRelationshipObjectFormatter.cs} | 4 +- .../IResourceCollectionDocumentFormatter.cs | 11 + .../IResourceCollectionPayloadSerializer.cs | 11 - ...alizer.cs => IResourceLinkageFormatter.cs} | 4 +- ...ializer.cs => IResourceObjectFormatter.cs} | 4 +- .../Json/ISingleResourceDocumentFormatter.cs | 11 + .../Json/ISingleResourcePayloadSerializer.cs | 11 - JSONAPI/Json/JsonApiFormatter.cs | 60 +- .../{LinkSerializer.cs => LinkFormatter.cs} | 18 +- ...dataSerializer.cs => MetadataFormatter.cs} | 6 +- JSONAPI/Json/RelationAggregator.cs | 68 -- ...izer.cs => RelationshipObjectFormatter.cs} | 34 +- ...=> ResourceCollectionDocumentFormatter.cs} | 46 +- ...ializer.cs => ResourceLinkageFormatter.cs} | 6 +- ...rializer.cs => ResourceObjectFormatter.cs} | 38 +- ....cs => SingleResourceDocumentFormatter.cs} | 44 +- .../Builders/FallbackPayloadBuilder.cs | 113 --- .../Payload/Builders/IErrorPayloadBuilder.cs | 27 - JSONAPI/Payload/IErrorPayload.cs | 13 - JSONAPI/Payload/IJsonApiPayload.cs | 13 - JSONAPI/Payload/PayloadReaderException.cs | 20 - JSONAPI/Payload/ResourceCollectionPayload.cs | 25 - JSONAPI/Payload/SingleResourcePayload.cs | 30 - 192 files changed, 1765 insertions(+), 2988 deletions(-) rename JSONAPI.EntityFramework.Tests/Acceptance/{PayloadTests.cs => DocumentTests.cs} (68%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{Payload/Responses/Get_returns_IResourceCollectionPayload.json => Document/Responses/Get_returns_IResourceCollectionDocument.json} (100%) rename JSONAPI.EntityFramework/Http/{EntityFrameworkPayloadMaterializer.cs => EntityFrameworkDocumentMaterializer.cs} (68%) rename JSONAPI.Tests/ActionFilters/{FallbackPayloadBuilderAttributeTests.cs => FallbackDocumentBuilderAttributeTests.cs} (61%) delete mode 100644 JSONAPI.Tests/Core/MetadataManagerTests.cs delete mode 100644 JSONAPI.Tests/Data/ByteIdSerializationTest.json delete mode 100644 JSONAPI.Tests/Data/DeserializeAttributeRequest.json delete mode 100644 JSONAPI.Tests/Data/DeserializeCollectionRequest.json delete mode 100644 JSONAPI.Tests/Data/DeserializeRawJsonTest.json delete mode 100644 JSONAPI.Tests/Data/EmptyArrayResult.json delete mode 100644 JSONAPI.Tests/Data/ErrorSerializerTest.json delete mode 100644 JSONAPI.Tests/Data/FormatterErrorSerializationTest.json delete mode 100644 JSONAPI.Tests/Data/LinkTemplateTest.json delete mode 100644 JSONAPI.Tests/Data/MalformedRawJsonString.json delete mode 100644 JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json delete mode 100644 JSONAPI.Tests/Data/NonStandardIdTest.json delete mode 100644 JSONAPI.Tests/Data/NullResourceResult.json delete mode 100644 JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json delete mode 100644 JSONAPI.Tests/Data/SerializerIntegrationTest.json rename JSONAPI.Tests/{Payload/Builders/ErrorPayloadBuilderTests.cs => Documents/Builders/ErrorDocumentBuilderTests.cs} (76%) create mode 100644 JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs rename JSONAPI.Tests/{Payload/Builders/RegistryDrivenPayloadBuilderTests.cs => Documents/Builders/RegistryDrivenDocumentBuilderTests.cs} (74%) rename JSONAPI.Tests/{Payload/Builders/RegistryDrivenSingleResourcePayloadBuilderTests.cs => Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs} (89%) rename JSONAPI.Tests/{Payload => Documents}/DefaultLinkConventionsTests.cs (99%) rename JSONAPI.Tests/{Payload => Documents}/ToManyResourceLinkageTests.cs (97%) rename JSONAPI.Tests/{Payload => Documents}/ToOneResourceLinkageTests.cs (95%) rename JSONAPI.Tests/Json/{ErrorPayloadSerializerTests.cs => ErrorDocumentFormatterTests.cs} (52%) rename JSONAPI.Tests/Json/{ErrorSerializerTests.cs => ErrorFormatterTests.cs} (69%) rename JSONAPI.Tests/Json/Fixtures/{ErrorPayloadSerializer/Serialize_ErrorPayload.json => ErrorDocumentFormatter/Serialize_ErrorDocument.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ErrorSerializer => ErrorFormatter}/Serialize_error_with_all_possible_members.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ErrorSerializer => ErrorFormatter}/Serialize_error_with_only_id.json (100%) create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorDocument.json delete mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionDocument.json delete mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json create mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourceDocument.json delete mode 100644 JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json rename JSONAPI.Tests/Json/Fixtures/{LinkSerializer => LinkFormatter}/Serialize_link_with_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{LinkSerializer => LinkFormatter}/Serialize_link_without_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{MetadataSerializer => MetadataFormatter}/Deserialize_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{MetadataSerializer => MetadataFormatter}/Deserialize_null_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{MetadataSerializer => MetadataFormatter}/Serialize_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{MetadataSerializer => MetadataFormatter}/Serialize_null_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Deserialize_relationship_object.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_all_possible_members.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_linkage_only.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_meta_only.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_related_link_only.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_self_link_and_related_link.json (100%) rename JSONAPI.Tests/Json/Fixtures/{RelationshipObjectSerializer => RelationshipObjectFormatter}/Serialize_relationship_with_self_link_only.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Deserialize_payload_with_metadata.json => ResourceCollectionDocumentFormatter/Deserialize_document_with_metadata.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data.json => ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data_and_unknown_top_level_key.json => ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data_and_unknown_top_level_key.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Deserialize_empty_payload.json => ResourceCollectionDocumentFormatter/Deserialize_empty_document.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_all_possible_members.json => ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only.json => ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata.json => ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Deserialize_fails_on_integer.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Deserialize_fails_on_string.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Deserialize_null_to_one_linkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Deserialize_to_many_linkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Deserialize_to_one_linkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Serialize_ToOneResourceLinkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Serialize_linkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceLinkageSerializer => ResourceLinkageFormatter}/Serialize_null_linkage.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Deserialize_resource_object.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_all_possible_members.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_attributes.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_links.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_metadata.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_only_null_relationships.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_relationships.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json (100%) rename JSONAPI.Tests/Json/Fixtures/{ResourceObjectSerializer => ResourceObjectFormatter}/Serialize_ResourceObject_for_resource_without_attributes.json (100%) rename JSONAPI.Tests/Json/Fixtures/{SingleResourcePayloadSerializer/Deserialize_payload_with_resource.json => SingleResourceDocumentFormatter/Deserialize_document_with_resource.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{SingleResourcePayloadSerializer/Deserialize_null_payload.json => SingleResourceDocumentFormatter/Deserialize_null_document.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_all_possible_members.json => SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_all_possible_members.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_and_metadata.json => SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_and_metadata.json} (100%) rename JSONAPI.Tests/Json/Fixtures/{SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_only.json => SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_only.json} (100%) rename JSONAPI.Tests/Json/{JsonApiSerializerTestsBase.cs => JsonApiFormatterTestsBase.cs} (67%) rename JSONAPI.Tests/Json/{LinkSerializerTests.cs => LinkFormatterTests.cs} (51%) rename JSONAPI.Tests/Json/{MetadataSerializerTests.cs => MetadataFormatterTests.cs} (65%) rename JSONAPI.Tests/Json/{RelationshipObjectSerializerTests.cs => RelationshipObjectFormatterTests.cs} (59%) create mode 100644 JSONAPI.Tests/Json/ResourceCollectionDocumentFormatterTests.cs delete mode 100644 JSONAPI.Tests/Json/ResourceCollectionPayloadSerializerTests.cs rename JSONAPI.Tests/Json/{ResourceLinkageSerializerTests.cs => ResourceLinkageFormatterTests.cs} (61%) rename JSONAPI.Tests/Json/{ResourceObjectSerializerTests.cs => ResourceObjectFormatterTests.cs} (61%) create mode 100644 JSONAPI.Tests/Json/SingleResourceDocumentFormatterTests.cs delete mode 100644 JSONAPI.Tests/Json/SingleResourcePayloadSerializerTests.cs delete mode 100644 JSONAPI.Tests/Payload/Builders/FallbackPayloadBuilderTests.cs rename JSONAPI/ActionFilters/{FallbackPayloadBuilderAttribute.cs => FallbackDocumentBuilderAttribute.cs} (50%) rename JSONAPI/{Payload/Builders/DefaultQueryableResourceCollectionPayloadBuilder.cs => Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs} (60%) rename JSONAPI/{Payload/Builders/ErrorPayloadBuilder.cs => Documents/Builders/ErrorDocumentBuilder.cs} (89%) create mode 100644 JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs create mode 100644 JSONAPI/Documents/Builders/IErrorDocumentBuilder.cs rename JSONAPI/{Payload/Builders/IFallbackPayloadBuilder.cs => Documents/Builders/IFallbackDocumentBuilder.cs} (50%) rename JSONAPI/{Payload/Builders/IQueryableResourceCollectionPayloadBuilder.cs => Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs} (53%) rename JSONAPI/{Payload/Builders/IResourceCollectionPayloadBuilder.cs => Documents/Builders/IResourceCollectionDocumentBuilder.cs} (60%) rename JSONAPI/{Payload/Builders/ISingleResourcePayloadBuilder.cs => Documents/Builders/ISingleResourceDocumentBuilder.cs} (57%) rename JSONAPI/{Payload => Documents}/Builders/JsonApiException.cs (92%) rename JSONAPI/{Payload/Builders/RegistryDrivenPayloadBuilder.cs => Documents/Builders/RegistryDrivenDocumentBuilder.cs} (95%) rename JSONAPI/{Payload/Builders/RegistryDrivenResourceCollectionPayloadBuilder.cs => Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs} (54%) rename JSONAPI/{Payload/Builders/RegistryDrivenSingleResourcePayloadBuilder.cs => Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs} (54%) rename JSONAPI/{Payload => Documents}/DefaultLinkConventions.cs (99%) rename JSONAPI/{Payload => Documents}/Error.cs (95%) rename JSONAPI/{Payload/ErrorPayload.cs => Documents/ErrorDocument.cs} (61%) rename JSONAPI/{Payload => Documents}/ExceptionErrorMetadata.cs (97%) rename JSONAPI/{Payload => Documents}/IError.cs (98%) create mode 100644 JSONAPI/Documents/IErrorDocument.cs create mode 100644 JSONAPI/Documents/IJsonApiDocument.cs rename JSONAPI/{Payload => Documents}/ILink.cs (97%) rename JSONAPI/{Payload => Documents}/ILinkConventions.cs (95%) rename JSONAPI/{Payload => Documents}/IMetadata.cs (91%) rename JSONAPI/{Payload => Documents}/IRelationshipObject.cs (95%) rename JSONAPI/{Payload/IResourceCollectionPayload.cs => Documents/IResourceCollectionDocument.cs} (56%) create mode 100644 JSONAPI/Documents/IResourceIdentifier.cs rename JSONAPI/{Payload => Documents}/IResourceLinkage.cs (91%) rename JSONAPI/{Payload => Documents}/IResourceObject.cs (97%) rename JSONAPI/{Payload/ISingleResourcePayload.cs => Documents/ISingleResourceDocument.cs} (57%) rename JSONAPI/{Payload => Documents}/RelationshipObject.cs (97%) create mode 100644 JSONAPI/Documents/ResourceCollectionDocument.cs rename JSONAPI/{Payload/IResourceIdentifier.cs => Documents/ResourceIdentifier.cs} (58%) rename JSONAPI/{Payload => Documents}/ResourceObject.cs (97%) create mode 100644 JSONAPI/Documents/SingleResourceDocument.cs rename JSONAPI/{Payload => Documents}/ToManyResourceLinkage.cs (97%) rename JSONAPI/{Payload => Documents}/ToOneResourceLinkage.cs (96%) rename JSONAPI/Http/{IPayloadMaterializer.cs => IDocumentMaterializer.cs} (55%) create mode 100644 JSONAPI/Json/ErrorDocumentFormatter.cs rename JSONAPI/Json/{ErrorSerializer.cs => ErrorFormatter.cs} (77%) delete mode 100644 JSONAPI/Json/ErrorPayloadSerializer.cs create mode 100644 JSONAPI/Json/IErrorDocumentFormatter.cs rename JSONAPI/Json/{IErrorSerializer.cs => IErrorFormatter.cs} (59%) delete mode 100644 JSONAPI/Json/IErrorPayloadSerializer.cs rename JSONAPI/Json/{IJsonApiSerializer.cs => IJsonApiFormatter.cs} (86%) rename JSONAPI/Json/{ILinkSerializer.cs => ILinkFormatter.cs} (59%) rename JSONAPI/Json/{IMetadataSerializer.cs => IMetadataFormatter.cs} (58%) rename JSONAPI/Json/{IRelationshipObjectSerializer.cs => IRelationshipObjectFormatter.cs} (55%) create mode 100644 JSONAPI/Json/IResourceCollectionDocumentFormatter.cs delete mode 100644 JSONAPI/Json/IResourceCollectionPayloadSerializer.cs rename JSONAPI/Json/{IResourceLinkageSerializer.cs => IResourceLinkageFormatter.cs} (57%) rename JSONAPI/Json/{IResourceObjectSerializer.cs => IResourceObjectFormatter.cs} (57%) create mode 100644 JSONAPI/Json/ISingleResourceDocumentFormatter.cs delete mode 100644 JSONAPI/Json/ISingleResourcePayloadSerializer.cs rename JSONAPI/Json/{LinkSerializer.cs => LinkFormatter.cs} (68%) rename JSONAPI/Json/{MetadataSerializer.cs => MetadataFormatter.cs} (90%) delete mode 100644 JSONAPI/Json/RelationAggregator.cs rename JSONAPI/Json/{RelationshipObjectSerializer.cs => RelationshipObjectFormatter.cs} (68%) rename JSONAPI/Json/{ResourceCollectionPayloadSerializer.cs => ResourceCollectionDocumentFormatter.cs} (61%) rename JSONAPI/Json/{ResourceLinkageSerializer.cs => ResourceLinkageFormatter.cs} (95%) rename JSONAPI/Json/{ResourceObjectSerializer.cs => ResourceObjectFormatter.cs} (81%) rename JSONAPI/Json/{SingleResourcePayloadSerializer.cs => SingleResourceDocumentFormatter.cs} (59%) delete mode 100644 JSONAPI/Payload/Builders/FallbackPayloadBuilder.cs delete mode 100644 JSONAPI/Payload/Builders/IErrorPayloadBuilder.cs delete mode 100644 JSONAPI/Payload/IErrorPayload.cs delete mode 100644 JSONAPI/Payload/IJsonApiPayload.cs delete mode 100644 JSONAPI/Payload/PayloadReaderException.cs delete mode 100644 JSONAPI/Payload/ResourceCollectionPayload.cs delete mode 100644 JSONAPI/Payload/SingleResourcePayload.cs diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs index 38a825d6..094a53bd 100644 --- a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs @@ -11,7 +11,7 @@ public class JsonApiAutofacEntityFrameworkModule : Module protected override void Load(ContainerBuilder builder) { builder.RegisterType().As(); - builder.RegisterGeneric(typeof(EntityFrameworkPayloadMaterializer<>)) + builder.RegisterGeneric(typeof(EntityFrameworkDocumentMaterializer<>)) .AsImplementedInterfaces(); builder.RegisterType() .As(); diff --git a/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs b/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs index d940b3af..03a6e942 100644 --- a/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs +++ b/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using Autofac.Core; using JSONAPI.Core; -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Autofac { diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 7147e912..ea391788 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -3,10 +3,10 @@ using Autofac; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using JSONAPI.Http; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; namespace JSONAPI.Autofac { @@ -41,15 +41,15 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); // Serialization - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); // Queryable transforms builder.RegisterType().As().SingleInstance(); @@ -57,17 +57,17 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - // Payload building + // document building var linkConventions = _linkConventions ?? new DefaultLinkConventions(); builder.Register(c => linkConventions).As().SingleInstance(); builder.RegisterType().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); - builder.RegisterType().As(); + builder.RegisterType().As(); } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs index 3a3cfcbb..52aa1b7c 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class CommentsController : JsonApiController { - public CommentsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public CommentsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs index 06eb5986..24e99835 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs @@ -5,8 +5,8 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class LanguageUserLinksController : JsonApiController { - public LanguageUserLinksController(IPayloadMaterializer payloadMaterializer) - : base(payloadMaterializer) + public LanguageUserLinksController(IDocumentMaterializer documentMaterializer) + : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs index ef2bd946..f67dc20f 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class PostsController : JsonApiController { - public PostsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public PostsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs index 1d6a7348..bf788105 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; +using System.Linq; using System.Web.Http; +using JSONAPI.Documents; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Payload; -using Newtonsoft.Json.Linq; namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class PresidentsController : ApiController { - // This endpoint exists to demonstrate returning IResourceCollectionPayload + // This endpoint exists to demonstrate returning IResourceCollectionDocument [Route("presidents")] public IHttpActionResult GetPresidents() { @@ -34,8 +29,8 @@ public IHttpActionResult GetPresidents() var userResources = users.Select(u => (IResourceObject)new ResourceObject("users", u.Id)).ToArray(); - var payload = new ResourceCollectionPayload(userResources, null, null); - return Ok(payload); + var document = new ResourceCollectionDocument(userResources, null, null); + return Ok(document); } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs index eb2b3b39..0901f3a8 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class TagsController : JsonApiController { - public TagsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public TagsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs index 2ec50e2f..a4886e21 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class UserGroupsController : JsonApiController { - public UserGroupsController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public UserGroupsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs index 865b9c59..462b7a04 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers { public class UsersController : JsonApiController { - public UsersController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public UsersController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs index f7f46feb..a324c919 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs @@ -2,8 +2,8 @@ using System.Data.Entity; using System.Threading.Tasks; using JSONAPI.Core; +using JSONAPI.Documents; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Payload; namespace JSONAPI.EntityFramework.Tests.TestWebApp { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs similarity index 68% rename from JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs rename to JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs index 854afd31..56e5372a 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PayloadTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs @@ -5,16 +5,16 @@ namespace JSONAPI.EntityFramework.Tests.Acceptance { [TestClass] - public class PayloadTests : AcceptanceTestsBase + public class DocumentTests : AcceptanceTestsBase { [TestMethod] - public async Task Get_returns_IResourceCollectionPayload() + public async Task Get_returns_IResourceCollectionDocument() { using (var effortConnection = GetEffortConnection()) { var response = await SubmitGet(effortConnection, "presidents"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Payload\Responses\Get_returns_IResourceCollectionPayload.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Acceptance\Fixtures\Document\Responses\Get_returns_IResourceCollectionDocument.json", HttpStatusCode.OK); } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Payload/Responses/Get_returns_IResourceCollectionPayload.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index a9a576ae..90264dd1 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -114,7 +114,7 @@ - + @@ -180,7 +180,7 @@ - + Always diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index 65115081..ebfe05aa 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -7,8 +7,8 @@ using System.Threading; using System.Threading.Tasks; using JSONAPI.Core; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; using Newtonsoft.Json.Linq; namespace JSONAPI.EntityFramework @@ -95,7 +95,7 @@ protected virtual async Task GetExistingRecord(IResourceTypeRegistration /// /// /// - /// Thrown when a semantically incorrect part of the payload is encountered + /// Thrown when a semantically incorrect part of the document is encountered protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceObject, object material, IResourceTypeRegistration registration, CancellationToken cancellationToken) { diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs similarity index 68% rename from JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs rename to JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index bea5c487..00b184e5 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkPayloadMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -8,47 +8,47 @@ using System.Threading; using System.Threading.Tasks; using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using JSONAPI.Http; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; namespace JSONAPI.EntityFramework.Http { /// - /// Implementation of IPayloadMaterializer for use with Entity Framework. + /// Implementation of IDocumentMaterializer for use with Entity Framework. /// - public class EntityFrameworkPayloadMaterializer : IPayloadMaterializer where T : class + public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer where T : class { private readonly DbContext _dbContext; private readonly IResourceTypeRegistry _resourceTypeRegistry; - private readonly IQueryableResourceCollectionPayloadBuilder _queryableResourceCollectionPayloadBuilder; - private readonly ISingleResourcePayloadBuilder _singleResourcePayloadBuilder; + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; private readonly IBaseUrlService _baseUrlService; private readonly MethodInfo _getRelatedToManyMethod; private readonly MethodInfo _getRelatedToOneMethod; /// - /// Creates a new EntityFrameworkPayloadMaterializer + /// Creates a new EntityFrameworkDocumentMaterializer /// /// /// - /// - /// + /// + /// /// /// - public EntityFrameworkPayloadMaterializer( + public EntityFrameworkDocumentMaterializer( DbContext dbContext, IResourceTypeRegistry resourceTypeRegistry, - IQueryableResourceCollectionPayloadBuilder queryableResourceCollectionPayloadBuilder, - ISingleResourcePayloadBuilder singleResourcePayloadBuilder, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, IBaseUrlService baseUrlService) { _dbContext = dbContext; _resourceTypeRegistry = resourceTypeRegistry; - _queryableResourceCollectionPayloadBuilder = queryableResourceCollectionPayloadBuilder; - _singleResourcePayloadBuilder = singleResourcePayloadBuilder; + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; _baseUrlService = baseUrlService; _getRelatedToManyMethod = GetType() @@ -57,21 +57,21 @@ public EntityFrameworkPayloadMaterializer( .GetMethod("GetRelatedToOne", BindingFlags.NonPublic | BindingFlags.Instance); } - public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) + public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { var query = _dbContext.Set().AsQueryable(); - return _queryableResourceCollectionPayloadBuilder.BuildPayload(query, request, cancellationToken); + return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); } - public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) + public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); var singleResource = await FilterById(id, registration).FirstOrDefaultAsync(cancellationToken); - return _singleResourcePayloadBuilder.BuildPayload(singleResource, apiBaseUrl, null); + return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null); } - public virtual async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, + public virtual async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken) { var registration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); @@ -80,40 +80,40 @@ public virtual async Task GetRelated(string id, string relation if (relationship.IsToMany) { var method = _getRelatedToManyMethod.MakeGenericMethod(relationship.RelatedType); - var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); + var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); return await result; } else { var method = _getRelatedToOneMethod.MakeGenericMethod(relationship.RelatedType); - var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); + var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); return await result; } } - public virtual async Task CreateRecord(ISingleResourcePayload requestPayload, + public virtual async Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var newRecord = await MaterializeAsync(requestPayload.PrimaryData, cancellationToken); + var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); - var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, apiBaseUrl, null); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null); - return returnPayload; + return returnDocument; } - public virtual async Task UpdateRecord(string id, ISingleResourcePayload requestPayload, + public virtual async Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var newRecord = await MaterializeAsync(requestPayload.PrimaryData, cancellationToken); - var returnPayload = _singleResourcePayloadBuilder.BuildPayload(newRecord, apiBaseUrl, null); + var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null); await _dbContext.SaveChangesAsync(cancellationToken); - return returnPayload; + return returnDocument; } - public virtual async Task DeleteRecord(string id, CancellationToken cancellationToken) + public virtual async Task DeleteRecord(string id, CancellationToken cancellationToken) { var singleResource = await _dbContext.Set().FindAsync(cancellationToken, id); _dbContext.Set().Remove(singleResource); @@ -142,7 +142,7 @@ protected virtual async Task MaterializeAsync(IResourceObject resourceOb /// /// Generic method for getting the related resources for a to-many relationship /// - protected async Task GetRelatedToMany(string id, + protected async Task GetRelatedToMany(string id, ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) { var primaryEntityRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); @@ -153,13 +153,13 @@ protected async Task GetRelatedToMany(stri var primaryEntityQuery = FilterById(id, primaryEntityRegistration); var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda); - return await _queryableResourceCollectionPayloadBuilder.BuildPayload(relatedResourceQuery, request, cancellationToken); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, cancellationToken); } /// /// Generic method for getting the related resources for a to-one relationship /// - protected async Task GetRelatedToOne(string id, + protected async Task GetRelatedToOne(string id, ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) { var primaryEntityRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); @@ -169,7 +169,7 @@ protected async Task GetRelatedToOne(string id var primaryEntityQuery = FilterById(id, primaryEntityRegistration); var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); - return _singleResourcePayloadBuilder.BuildPayload(relatedResource, GetBaseUrlFromRequest(request), null); + return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null); } private IQueryable Filter(Expression> predicate, diff --git a/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs index cd40207d..321fd36c 100644 --- a/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; namespace JSONAPI.EntityFramework { diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 80540a61..b8944aad 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -72,7 +72,7 @@ - + diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs index a1dfb68f..c8587bc8 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -7,7 +7,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 2612d59a..0bf0f9fe 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -5,7 +5,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters diff --git a/JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs b/JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs similarity index 61% rename from JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs rename to JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs index e76e24bd..49a87216 100644 --- a/JSONAPI.Tests/ActionFilters/FallbackPayloadBuilderAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs @@ -10,15 +10,15 @@ using System.Web.Http.Filters; using FluentAssertions; using JSONAPI.ActionFilters; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace JSONAPI.Tests.ActionFilters { [TestClass] - public class FallbackPayloadBuilderAttributeTests + public class FallbackDocumentBuilderAttributeTests { private HttpActionExecutedContext GetActionExecutedContext(object objectContentValue, Exception exception = null) { @@ -34,62 +34,62 @@ private HttpActionExecutedContext GetActionExecutedContext(object objectContentV } [TestMethod] - public void OnActionExecutedAsync_leaves_ISingleResourcePayload_alone() + public void OnActionExecutedAsync_leaves_ISingleResourceDocument_alone() { // Arrange - var mockPayload = new Mock(MockBehavior.Strict); - var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var mockDocument = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); var cancellationTokenSource = new CancellationTokenSource(); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); - ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); } [TestMethod] - public void OnActionExecutedAsync_leaves_IResourceCollectionPayload_alone() + public void OnActionExecutedAsync_leaves_IResourceCollectionDocument_alone() { // Arrange - var mockPayload = new Mock(MockBehavior.Strict); - var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var mockDocument = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); var cancellationTokenSource = new CancellationTokenSource(); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); - ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); } [TestMethod] - public void OnActionExecutedAsync_leaves_IErrorPayload_alone_but_changes_request_status_to_match_error_status() + public void OnActionExecutedAsync_leaves_IErrorDocument_alone_but_changes_request_status_to_match_error_status() { // Arrange var mockError = new Mock(MockBehavior.Strict); mockError.Setup(e => e.Status).Returns(HttpStatusCode.Conflict); - var mockPayload = new Mock(MockBehavior.Strict); - mockPayload.Setup(p => p.Errors).Returns(new[] {mockError.Object}); - var actionExecutedContext = GetActionExecutedContext(mockPayload.Object); + var mockDocument = new Mock(MockBehavior.Strict); + mockDocument.Setup(p => p.Errors).Returns(new[] {mockError.Object}); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); var cancellationTokenSource = new CancellationTokenSource(); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); - ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockPayload.Object); + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); } @@ -101,11 +101,11 @@ public void OnActionExecutedAsync_does_nothing_if_there_is_an_exception() var theException = new Exception("This is an error."); var actionExecutedContext = GetActionExecutedContext(objectContent, theException); var cancellationTokenSource = new CancellationTokenSource(); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); @@ -119,22 +119,22 @@ private class Fruit } [TestMethod] - public void OnActionExecutedAsync_delegates_to_fallback_payload_builder_for_unknown_types() + public void OnActionExecutedAsync_delegates_to_fallback_document_builder_for_unknown_types() { // Arrange - var payload = new Fruit(); - var actionExecutedContext = GetActionExecutedContext(payload); + var resource = new Fruit(); + var actionExecutedContext = GetActionExecutedContext(resource); var cancellationTokenSource = new CancellationTokenSource(); - var mockResult = new Mock(MockBehavior.Strict); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); - mockFallbackPayloadBuilder.Setup(b => b.BuildPayload(payload, It.IsAny(), cancellationTokenSource.Token)) + var mockResult = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + mockFallbackDocumentBuilder.Setup(b => b.BuildDocument(resource, It.IsAny(), cancellationTokenSource.Token)) .Returns(Task.FromResult(mockResult.Object)); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); @@ -144,24 +144,24 @@ public void OnActionExecutedAsync_delegates_to_fallback_payload_builder_for_unkn } [TestMethod] - public void OnActionExecutedAsync_creates_IErrorPayload_for_HttpError() + public void OnActionExecutedAsync_creates_IErrorDocument_for_HttpError() { // Arrange var httpError = new HttpError("Some error"); var actionExecutedContext = GetActionExecutedContext(httpError); var cancellationTokenSource = new CancellationTokenSource(); - var mockFallbackPayloadBuilder = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); var mockError = new Mock(MockBehavior.Strict); mockError.Setup(e => e.Status).Returns(HttpStatusCode.OK); - var mockResult = new Mock(MockBehavior.Strict); + var mockResult = new Mock(MockBehavior.Strict); mockResult.Setup(r => r.Errors).Returns(new[] { mockError.Object }); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); - mockErrorPayloadBuilder.Setup(b => b.BuildFromHttpError(httpError, HttpStatusCode.OK)).Returns(mockResult.Object); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + mockErrorDocumentBuilder.Setup(b => b.BuildFromHttpError(httpError, HttpStatusCode.OK)).Returns(mockResult.Object); // Act - var attribute = new FallbackPayloadBuilderAttribute(mockFallbackPayloadBuilder.Object, mockErrorPayloadBuilder.Object); + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); task.Wait(); diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs deleted file mode 100644 index 6a9a9244..00000000 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.Json; -using System.IO; -using JSONAPI.Tests.Models; -using JSONAPI.Core; - -namespace JSONAPI.Tests.Core -{ - [TestClass] - public class MetadataManagerTests - { - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\MetadataManagerPropertyWasPresentRequest.json")] - public void PropertyWasPresentTest() - { - //using (var inputStream = File.OpenRead("MetadataManagerPropertyWasPresentRequest.json")) - //{ - // // Arrange - // var modelManager = new ModelManager(new PluralizationService()); - // modelManager.RegisterResourceType(typeof(Post)); - // modelManager.RegisterResourceType(typeof(Author)); - // JsonApiFormatter formatter = new JsonApiFormatter(modelManager); - - // var p = (Post) formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - - // // Act - // bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); - // bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); - // bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); - // bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); - - // // Assert - // Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); - // Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); - // Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); - // Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); - //} - } - } -} diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json deleted file mode 100644 index d4e9d1a4..00000000 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "data": [ - { - "type": "tags", - "id": "1", - "attributes": { - "text": "Ember" - } - }, - { - "type": "tags", - "id": "2", - "attributes": { - "text": "React" - } - }, - { - "type": "tags", - "id": "3", - "attributes": { - "text": "Angular" - } - } - ] -} diff --git a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json b/JSONAPI.Tests/Data/DeserializeAttributeRequest.json deleted file mode 100644 index 5c0a093b..00000000 --- a/JSONAPI.Tests/Data/DeserializeAttributeRequest.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "data": [ - { - "type": "samples", - "id": "1", - "attributes": { - "booleanField": false, - "nullableBooleanField": false, - "sByteField": 0, - "nullableSByteField": null, - "byteField": 0, - "nullableByteField": null, - "int16Field": 0, - "nullableInt16Field": null, - "uInt16Field": 0, - "nullableUInt16Field": null, - "int32Field": 0, - "nullableInt32Field": null, - "uInt32Field": 0, - "nullableUInt32Field": null, - "int64Field": 0, - "nullableInt64Field": null, - "uInt64Field": 0, - "nullableUInt64Field": null, - "doubleField": 0.0, - "nullableDoubleField": null, - "singleField": 0.0, - "nullableSingleField": null, - "decimalField": "0", - "nullableDecimalField": null, - "dateTimeField": "0001-01-01T00:00:00", - "nullableDateTimeField": null, - "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", - "nullableDateTimeOffsetField": null, - "guidField": "00000000-0000-0000-0000-000000000000", - "nullableGuidField": null, - "stringField": null, - "enumField": 0, - "nullableEnumField": null - } - }, - { - "type": "samples", - "id": "2", - "attributes": { - "booleanField": true, - "nullableBooleanField": true, - "sByteField": 123, - "nullableSByteField": 123, - "byteField": 253, - "nullableByteField": 253, - "int16Field": 32000, - "nullableInt16Field": 32000, - "uInt16Field": 64000, - "nullableUInt16Field": 64000, - "int32Field": 2000000000, - "nullableInt32Field": 2000000000, - "uInt32Field": 3000000000, - "nullableUInt32Field": 3000000000, - "int64Field": 9223372036854775807, - "nullableInt64Field": 9223372036854775807, - "uInt64Field": 9223372036854775808, - "nullableUInt64Field": 9223372036854775808, - "doubleField": 1056789.123, - "nullableDoubleField": 1056789.123, - "singleField": 1056789.13, - "nullableSingleField": 1056789.13, - "decimalField": "1056789.123", - "nullableDecimalField": "1056789.123", - "dateTimeField": "1776-07-04T00:00:00", - "nullableDateTimeField": "1776-07-04T00:00:00", - "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", - "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", - "stringField": "Some string 156", - "enumField": 1, - "nullableEnumField": 2 - } - } - ] - } diff --git a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json b/JSONAPI.Tests/Data/DeserializeCollectionRequest.json deleted file mode 100644 index 52b3f7b3..00000000 --- a/JSONAPI.Tests/Data/DeserializeCollectionRequest.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "data": [ - { - "type": "posts", - "id": "1", - "attributes": { - "title": "Linkbait!" - }, - "relationships": { - "author": { - "data": { - "type": "authors", - "id": "1" - } - }, - "comments": { - "data": [ - { - "type": "comments", - "id": "400" - }, - { - "type": "comments", - "id": "401" - } - ] - } - } - }, - { - "type": "posts", - "id": "2", - "attributes": { - "title": "Rant #1023" - }, - "relationships": { - "author": { - "data": { - "type": "authors", - "id": "1" - } - } - } - } - ] -} diff --git a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json b/JSONAPI.Tests/Data/DeserializeRawJsonTest.json deleted file mode 100644 index d24d3fd2..00000000 --- a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": [ - { - "id": "2", - "customData": null - }, - { - "id": "4", - "customData": { - "foo": "bar" - } - } - ] -} diff --git a/JSONAPI.Tests/Data/EmptyArrayResult.json b/JSONAPI.Tests/Data/EmptyArrayResult.json deleted file mode 100644 index b269b256..00000000 --- a/JSONAPI.Tests/Data/EmptyArrayResult.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "data": [ ] -} diff --git a/JSONAPI.Tests/Data/ErrorSerializerTest.json b/JSONAPI.Tests/Data/ErrorSerializerTest.json deleted file mode 100644 index 5d7446eb..00000000 --- a/JSONAPI.Tests/Data/ErrorSerializerTest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "errors": [ - { - "id": "OUTER-ID", - "status": "500", - "title": "System.Exception", - "detail": "Outer exception message", - "stackTrace": "Outer stack trace", - "inner": { - "id": "INNER-ID", - "status": "500", - "title": "Castle.Proxies.ExceptionProxy", - "detail": "Inner exception message", - "stackTrace": "Inner stack trace", - "inner": null - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json deleted file mode 100644 index f5439ca2..00000000 --- a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json +++ /dev/null @@ -1 +0,0 @@ -{"test":"foo"} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json deleted file mode 100644 index 95f9b61d..00000000 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "data": { - "type": "posts", - "id": "2", - "attributes": { - "title": "How to fry an egg" - }, - "relationships": { - "author": { - "data": { - "type": "users", - "id": "5" - }, - "links": { - "related": "/users/5" - } - } - } - } -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json deleted file mode 100644 index 51a07b7d..00000000 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "data": [ - { - "type": "comments", - "id": "5", - "attributes": { - "body": null, - "customData": { } - }, - "relationships": { - "post": { - "data": null - } - } - } - ] -} diff --git a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json b/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json deleted file mode 100644 index 35a76237..00000000 --- a/JSONAPI.Tests/Data/MetadataManagerPropertyWasPresentRequest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": { - "type": "posts", - "id": "42", - "relationships": { - "author": { - "data": { - "type": "authors", - "id": "18" - } - } - } - } -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json deleted file mode 100644 index 5e9f9226..00000000 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "data": [ - { - "type": "non-standard-id-things", - "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", - "attributes": { - "data": "Swap" - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/NullResourceResult.json b/JSONAPI.Tests/Data/NullResourceResult.json deleted file mode 100644 index 04ba24bd..00000000 --- a/JSONAPI.Tests/Data/NullResourceResult.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "data": null -} diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json deleted file mode 100644 index efc860ab..00000000 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": [ - { - "type": "comments", - "id": "5", - "attributes": { - "body": null, - "customData": { - "unquotedKey": 5 - } - }, - "relationships": { - "post": { - "data": null - } - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json deleted file mode 100644 index a5bd99e8..00000000 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - "data": [ - { - "type": "posts", - "id": "1", - "attributes": { - "title": "Linkbait!" - }, - "relationships": { - "comments": { - "data": [ - { - "type": "comments", - "id": "2" - }, - { - "type": "comments", - "id": "3" - }, - { - "type": "comments", - "id": "4" - } - ] - }, - "author": { - "data": { - "type": "authors", - "id": "1" - } - } - } - }, - { - "type": "posts", - "id": "2", - "attributes": { - "title": "Rant #1023" - }, - "relationships": { - "comments": { - "data": [ - { - "type": "comments", - "id": "5" - } - ] - }, - "author": { - "data": { - "type": "authors", - "id": "1" - } - } - } - }, - { - "type": "posts", - "id": "3", - "attributes": { - "title": "Polemic in E-flat minor #824" - }, - "relationships": { - "comments": { - "data": [ ] - }, - "author": { - "data": { - "type": "authors", - "id": "1" - } - } - } - }, - { - "type": "posts", - "id": "4", - "attributes": { - "title": "This post has no author." - }, - "relationships": { - "comments": { - "data": [ ] - }, - "author": { - "data": null - } - } - } - ], - "included": [ - { - "type": "comments", - "id": "2", - "attributes": { - "body": "Nuh uh!", - "customData": null - }, - "relationships": { - "post": { - "data": { - "type": "posts", - "id": "1" - } - } - } - }, - { - "type": "comments", - "id": "3", - "attributes": { - "body": "Yeah huh!", - "customData": null - }, - "relationships": { - "post": { - "data": { - "type": "posts", - "id": "1" - } - } - } - }, - { - "type": "comments", - "id": "4", - "attributes": { - "body": "Third Reich.", - "customData": { - "foo": "bar" - } - }, - - "relationships": { - "post": { - "data": { - "type": "posts", - "id": "1" - } - } - } - }, - { - "type": "comments", - "id": "5", - "attributes": { - "body": "I laughed, I cried!", - "customData": null - }, - "relationships": { - "post": { - "data": { - "type": "posts", - "id": "2" - } - } - } - }, - { - "type": "authors", - "id": "1", - "attributes": { - "name": "Jason Hater" - }, - "relationships": { - "posts": { - "data": [ - { - "type": "posts", - "id": "1" - }, - { - "type": "posts", - "id": "2" - }, - { - "type": "posts", - "id": "3" - } - ] - } - - } - } - ] -} diff --git a/JSONAPI.Tests/Payload/Builders/ErrorPayloadBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs similarity index 76% rename from JSONAPI.Tests/Payload/Builders/ErrorPayloadBuilderTests.cs rename to JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs index 45d95be9..e5fef359 100644 --- a/JSONAPI.Tests/Payload/Builders/ErrorPayloadBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs @@ -2,21 +2,21 @@ using System.Linq; using System.Net; using FluentAssertions; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; -namespace JSONAPI.Tests.Payload.Builders +namespace JSONAPI.Tests.Documents.Builders { [TestClass] - public class ErrorPayloadBuilderTests + public class ErrorDocumentBuilderTests { private const string GuidRegex = @"\b[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\b"; [TestMethod] - public void Builds_payload_from_exception() + public void Builds_document_from_exception() { // Arrange Exception theException; @@ -30,12 +30,12 @@ public void Builds_payload_from_exception() } // Act - var errorPayloadBuilder = new ErrorPayloadBuilder(); - var payload = errorPayloadBuilder.BuildFromException(theException); + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); // Assert - payload.Errors.Length.Should().Be(1); - var error = payload.Errors.First(); + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); error.Id.Should().MatchRegex(GuidRegex); error.Title.Should().Be("Unhandled exception"); error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); @@ -45,7 +45,7 @@ public void Builds_payload_from_exception() } [TestMethod] - public void Builds_payload_from_exception_with_inner_exception() + public void Builds_document_from_exception_with_inner_exception() { // Arrange Exception theException; @@ -66,12 +66,12 @@ public void Builds_payload_from_exception_with_inner_exception() } // Act - var errorPayloadBuilder = new ErrorPayloadBuilder(); - var payload = errorPayloadBuilder.BuildFromException(theException); + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); // Assert - payload.Errors.Length.Should().Be(1); - var error = payload.Errors.First(); + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); error.Id.Should().MatchRegex(GuidRegex); error.Title.Should().Be("Unhandled exception"); error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); @@ -85,7 +85,7 @@ public void Builds_payload_from_exception_with_inner_exception() } [TestMethod] - public void Builds_payload_from_exception_with_two_levels_deep_inner_exception() + public void Builds_document_from_exception_with_two_levels_deep_inner_exception() { // Arrange Exception theException; @@ -113,12 +113,12 @@ public void Builds_payload_from_exception_with_two_levels_deep_inner_exception() } // Act - var errorPayloadBuilder = new ErrorPayloadBuilder(); - var payload = errorPayloadBuilder.BuildFromException(theException); + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); // Assert - payload.Errors.Length.Should().Be(1); - var error = payload.Errors.First(); + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); error.Id.Should().MatchRegex(GuidRegex); error.Title.Should().Be("Unhandled exception"); error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); @@ -136,7 +136,7 @@ public void Builds_payload_from_exception_with_two_levels_deep_inner_exception() } [TestMethod] - public void Builds_payload_from_JsonApiException() + public void Builds_document_from_JsonApiException() { // Arrange var mockError = new Mock(MockBehavior.Strict); @@ -151,12 +151,12 @@ public void Builds_payload_from_JsonApiException() } // Act - var errorPayloadBuilder = new ErrorPayloadBuilder(); - var payload = errorPayloadBuilder.BuildFromException(theException); + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); // Assert - payload.Errors.Length.Should().Be(1); - payload.Errors.First().Should().Be(mockError.Object); + document.Errors.Length.Should().Be(1); + document.Errors.First().Should().Be(mockError.Object); } } } diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs new file mode 100644 index 00000000..9ba6af5f --- /dev/null +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -0,0 +1,128 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Documents.Builders +{ + [TestClass] + public class FallbackDocumentBuilderTests + { + private const string GuidRegex = @"\b[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\b"; + + class Fruit + { + public string Id { get; set; } + + public string Name { get; set; } + } + + [TestMethod] + public async Task Creates_single_resource_document_for_registered_non_collection_types() + { + // Arrange + var objectContent = new Fruit { Id = "984", Name = "Kiwi" }; + + var mockDocument = new Mock(MockBehavior.Strict); + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null)).Returns(mockDocument.Object); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + + var cancellationTokenSource = new CancellationTokenSource(); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com"); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + + [TestMethod] + public async Task Creates_resource_collection_document_for_queryables() + { + // Arrange + var items = new[] + { + new Fruit {Id = "43", Name = "Strawberry"}, + new Fruit {Id = "43", Name = "Grape"} + }.AsQueryable(); + + var mockDocument = new Mock(MockBehavior.Strict); + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + + var request = new HttpRequestMessage(); + + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); + + var cancellationTokenSource = new CancellationTokenSource(); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + mockQueryableDocumentBuilder + .Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token)) + .Returns(() => Task.FromResult(mockDocument.Object)); + + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + + [TestMethod] + public async Task Creates_resource_collection_document_for_non_queryable_enumerables() + { + // Arrange + var items = new[] + { + new Fruit {Id = "43", Name = "Strawberry"}, + new Fruit {Id = "43", Name = "Grape"} + }; + + var mockDocument = new Mock(MockBehavior.Strict); + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + + var cancellationTokenSource = new CancellationTokenSource(); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); + + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + mockResourceCollectionDocumentBuilder + .Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny(), It.IsAny())) + .Returns(() => (mockDocument.Object)); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + } +} diff --git a/JSONAPI.Tests/Payload/Builders/RegistryDrivenPayloadBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs similarity index 74% rename from JSONAPI.Tests/Payload/Builders/RegistryDrivenPayloadBuilderTests.cs rename to JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs index cbadcf36..ff943102 100644 --- a/JSONAPI.Tests/Payload/Builders/RegistryDrivenPayloadBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs @@ -1,11 +1,11 @@ using FluentAssertions; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.Tests.Payload.Builders +namespace JSONAPI.Tests.Documents.Builders { [TestClass] - public class RegistryDrivenPayloadBuilderTests + public class RegistryDrivenDocumentBuilderTests { [TestMethod] public void PathExpressionMatchesCurrentPath_is_true_when_pathToInclude_equals_currentPath_with_one_segment() @@ -15,7 +15,7 @@ public void PathExpressionMatchesCurrentPath_is_true_when_pathToInclude_equals_c const string pathToInclude = "posts"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeTrue(); @@ -29,7 +29,7 @@ public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_does_no const string pathToInclude = "comments"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeFalse(); @@ -43,7 +43,7 @@ public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_is_empt const string pathToInclude = ""; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeFalse(); @@ -57,7 +57,7 @@ public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_is_null const string pathToInclude = null; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeFalse(); @@ -71,7 +71,7 @@ public void PathExpressionMatchesCurrentPath_is_true_when_pathToInclude_equals_c const string pathToInclude = "posts.author"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeTrue(); @@ -85,7 +85,7 @@ public void PathExpressionMatchesCurrentPath_is_true_when_all_segments_of_curren const string pathToInclude = "posts.author.comments"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeTrue(); @@ -99,7 +99,7 @@ public void PathExpressionMatchesCurrentPath_is_false_when_all_segments_of_curre const string pathToInclude = "author.posts.author"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeFalse(); @@ -113,7 +113,7 @@ public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_starts_ const string pathToInclude = "posts.authora"; // Act - var matches = RegistryDrivenPayloadBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); // Assert matches.Should().BeFalse(); diff --git a/JSONAPI.Tests/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs similarity index 89% rename from JSONAPI.Tests/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilderTests.cs rename to JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index 72e1a55e..982917d3 100644 --- a/JSONAPI.Tests/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -2,16 +2,16 @@ using System.Linq; using FluentAssertions; using JSONAPI.Core; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; -namespace JSONAPI.Tests.Payload.Builders +namespace JSONAPI.Tests.Documents.Builders { [TestClass] - public class RegistryDrivenSingleResourcePayloadBuilderTests + public class RegistryDrivenSingleResourceDocumentBuilderTests { class Country { @@ -52,7 +52,7 @@ class Continent } [TestMethod] - public void Returns_correct_payload_for_resource() + public void Returns_correct_document_for_resource() { // Arrange var city1 = new City @@ -170,22 +170,22 @@ public void Returns_correct_payload_for_resource() var linkConventions = new DefaultLinkConventions(); // Act - var payloadBuilder = new RegistryDrivenSingleResourcePayloadBuilder(mockRegistry.Object, linkConventions); - var payload = payloadBuilder.BuildPayload(country, "http://www.example.com", new[] { "provinces.capital", "continent" }); + var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); + var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }); // Assert - payload.PrimaryData.Id.Should().Be("4"); - payload.PrimaryData.Type.Should().Be("countries"); - ((string) payload.PrimaryData.Attributes["name"]).Should().Be("Spain"); - payload.PrimaryData.Relationships.Count.Should().Be(3); + document.PrimaryData.Id.Should().Be("4"); + document.PrimaryData.Type.Should().Be("countries"); + ((string) document.PrimaryData.Attributes["name"]).Should().Be("Spain"); + document.PrimaryData.Relationships.Count.Should().Be(3); - var citiesRelationship = payload.PrimaryData.Relationships.First(); + var citiesRelationship = document.PrimaryData.Relationships.First(); citiesRelationship.Key.Should().Be("cities"); citiesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/countries/4/relationships/cities"); citiesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/countries/4/cities"); citiesRelationship.Value.Linkage.Should().BeNull(); - var provincesRelationship = payload.PrimaryData.Relationships.Skip(1).First(); + var provincesRelationship = document.PrimaryData.Relationships.Skip(1).First(); provincesRelationship.Key.Should().Be("provinces"); provincesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/countries/4/relationships/provinces"); provincesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/countries/4/provinces"); @@ -197,15 +197,15 @@ public void Returns_correct_payload_for_resource() ((string)provincesArray[1]["type"]).Should().Be("provinces"); ((string)provincesArray[1]["id"]).Should().Be("507"); - var continentRelationship = payload.PrimaryData.Relationships.Skip(2).First(); + var continentRelationship = document.PrimaryData.Relationships.Skip(2).First(); AssertToOneRelationship(continentRelationship, "continent", "http://www.example.com/countries/4/relationships/continent", "http://www.example.com/countries/4/continent", "continents", "1"); - payload.RelatedData.Length.Should().Be(4); // 2 provinces, 1 city, and 1 continent + document.RelatedData.Length.Should().Be(4); // 2 provinces, 1 city, and 1 continent - var province1RelatedData = payload.RelatedData[0]; + var province1RelatedData = document.RelatedData[0]; province1RelatedData.Id.Should().Be("506"); province1RelatedData.Attributes["name"].Value().Should().Be("Badajoz"); province1RelatedData.Type.Should().Be("provinces"); @@ -217,7 +217,7 @@ public void Returns_correct_payload_for_resource() "http://www.example.com/provinces/506/capital", "cities", "12"); - var province2RelatedData = payload.RelatedData[1]; + var province2RelatedData = document.RelatedData[1]; province2RelatedData.Id.Should().Be("507"); province2RelatedData.Type.Should().Be("provinces"); province2RelatedData.Attributes["name"].Value().Should().Be("Cuenca"); @@ -227,12 +227,12 @@ public void Returns_correct_payload_for_resource() "http://www.example.com/provinces/507/relationships/capital", "http://www.example.com/provinces/507/capital"); - var city3RelatedData = payload.RelatedData[2]; + var city3RelatedData = document.RelatedData[2]; city3RelatedData.Id.Should().Be("12"); city3RelatedData.Type.Should().Be("cities"); city3RelatedData.Attributes["name"].Value().Should().Be("Badajoz"); - var continentRelatedData = payload.RelatedData[3]; + var continentRelatedData = document.RelatedData[3]; continentRelatedData.Id.Should().Be("1"); continentRelatedData.Type.Should().Be("continents"); continentRelatedData.Attributes["name"].Value().Should().Be("Europe"); diff --git a/JSONAPI.Tests/Payload/DefaultLinkConventionsTests.cs b/JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs similarity index 99% rename from JSONAPI.Tests/Payload/DefaultLinkConventionsTests.cs rename to JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs index 0e8be34c..f34c93bd 100644 --- a/JSONAPI.Tests/Payload/DefaultLinkConventionsTests.cs +++ b/JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using FluentAssertions; using JSONAPI.Core; -using JSONAPI.Payload; +using JSONAPI.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace JSONAPI.Tests.Payload +namespace JSONAPI.Tests.Documents { [TestClass] public class DefaultLinkConventionsTests diff --git a/JSONAPI.Tests/Payload/ToManyResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs similarity index 97% rename from JSONAPI.Tests/Payload/ToManyResourceLinkageTests.cs rename to JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs index 6d3214ec..729ef38c 100644 --- a/JSONAPI.Tests/Payload/ToManyResourceLinkageTests.cs +++ b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs @@ -1,11 +1,11 @@ using System.Linq; using FluentAssertions; -using JSONAPI.Payload; +using JSONAPI.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; -namespace JSONAPI.Tests.Payload +namespace JSONAPI.Tests.Documents { [TestClass] public class ToManyResourceLinkageTests diff --git a/JSONAPI.Tests/Payload/ToOneResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs similarity index 95% rename from JSONAPI.Tests/Payload/ToOneResourceLinkageTests.cs rename to JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs index 3580d9ac..8dda5337 100644 --- a/JSONAPI.Tests/Payload/ToOneResourceLinkageTests.cs +++ b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs @@ -1,11 +1,11 @@ using System.Linq; using FluentAssertions; -using JSONAPI.Payload; +using JSONAPI.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; -namespace JSONAPI.Tests.Payload +namespace JSONAPI.Tests.Documents { [TestClass] public class ToOneResourceLinkageTests diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 3d9163a6..097eb362 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -80,12 +80,11 @@ - + - @@ -96,23 +95,23 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -124,99 +123,57 @@ - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JSONAPI.Tests/Json/ErrorPayloadSerializerTests.cs b/JSONAPI.Tests/Json/ErrorDocumentFormatterTests.cs similarity index 52% rename from JSONAPI.Tests/Json/ErrorPayloadSerializerTests.cs rename to JSONAPI.Tests/Json/ErrorDocumentFormatterTests.cs index e707e7d5..34ce4a1e 100644 --- a/JSONAPI.Tests/Json/ErrorPayloadSerializerTests.cs +++ b/JSONAPI.Tests/Json/ErrorDocumentFormatterTests.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -8,22 +8,22 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class ErrorPayloadSerializerTests : JsonApiSerializerTestsBase + public class ErrorDocumentFormatterTests : JsonApiFormatterTestsBase { [TestMethod] - public async Task Serialize_ErrorPayload() + public async Task Serialize_ErrorDocument() { var error1 = new Mock(MockBehavior.Strict); var error2 = new Mock(MockBehavior.Strict); - var mockErrorSerializer = new Mock(MockBehavior.Strict); - mockErrorSerializer.Setup(s => s.Serialize(error1.Object, It.IsAny())) + var mockErrorFormatter = new Mock(MockBehavior.Strict); + mockErrorFormatter.Setup(s => s.Serialize(error1.Object, It.IsAny())) .Returns((IError error, JsonWriter writer) => { writer.WriteValue("first error would go here"); return Task.FromResult(0); }); - mockErrorSerializer.Setup(s => s.Serialize(error2.Object, It.IsAny())) + mockErrorFormatter.Setup(s => s.Serialize(error2.Object, It.IsAny())) .Returns((IError error, JsonWriter writer) => { writer.WriteValue("second error would go here"); @@ -31,18 +31,18 @@ public async Task Serialize_ErrorPayload() }); var mockMetadata = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { writer.WriteValue("metadata goes here"); return Task.FromResult(0); }); - IErrorPayload payload = new ErrorPayload(new[] { error1.Object, error2.Object }, mockMetadata.Object); + IErrorDocument document = new ErrorDocument(new[] { error1.Object, error2.Object }, mockMetadata.Object); - var serializer = new ErrorPayloadSerializer(mockErrorSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/ErrorPayloadSerializer/Serialize_ErrorPayload.json"); + var formatter = new ErrorDocumentFormatter(mockErrorFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/ErrorDocumentFormatter/Serialize_ErrorDocument.json"); } } } diff --git a/JSONAPI.Tests/Json/ErrorSerializerTests.cs b/JSONAPI.Tests/Json/ErrorFormatterTests.cs similarity index 69% rename from JSONAPI.Tests/Json/ErrorSerializerTests.cs rename to JSONAPI.Tests/Json/ErrorFormatterTests.cs index 0e785b48..9865d662 100644 --- a/JSONAPI.Tests/Json/ErrorSerializerTests.cs +++ b/JSONAPI.Tests/Json/ErrorFormatterTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Threading.Tasks; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -10,7 +10,7 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class ErrorSerializerTests : JsonApiSerializerTestsBase + public class ErrorFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public async Task Serialize_error_with_only_id() @@ -18,8 +18,8 @@ public async Task Serialize_error_with_only_id() var error = new Mock(); error.Setup(e => e.Id).Returns("123456"); - var serializer = new ErrorSerializer(null, null); - await AssertSerializeOutput(serializer, error.Object, "Json/Fixtures/ErrorSerializer/Serialize_error_with_only_id.json"); + var formatter = new ErrorFormatter(null, null); + await AssertSerializeOutput(formatter, error.Object, "Json/Fixtures/ErrorFormatter/Serialize_error_with_only_id.json"); } [TestMethod] @@ -47,24 +47,24 @@ public async Task Serialize_error_with_all_possible_members() error.Setup(e => e.Parameter).Returns("sort"); error.Setup(e => e.Metadata).Returns(mockMetadata.Object); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer.Setup(s => s.Serialize(mockAboutLink.Object, It.IsAny())) + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter.Setup(s => s.Serialize(mockAboutLink.Object, It.IsAny())) .Returns((ILink link, JsonWriter writer) => { writer.WriteValue(link.Href); return Task.FromResult(0); }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { metadata.MetaObject.WriteTo(writer); return Task.FromResult(0); }); - var serializer = new ErrorSerializer(mockLinkSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, error.Object, "Json/Fixtures/ErrorSerializer/Serialize_error_with_all_possible_members.json"); + var formatter = new ErrorFormatter(mockLinkFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, error.Object, "Json/Fixtures/ErrorFormatter/Serialize_error_with_all_possible_members.json"); } } } diff --git a/JSONAPI.Tests/Json/Fixtures/ErrorPayloadSerializer/Serialize_ErrorPayload.json b/JSONAPI.Tests/Json/Fixtures/ErrorDocumentFormatter/Serialize_ErrorDocument.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ErrorPayloadSerializer/Serialize_ErrorPayload.json rename to JSONAPI.Tests/Json/Fixtures/ErrorDocumentFormatter/Serialize_ErrorDocument.json diff --git a/JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/ErrorFormatter/Serialize_error_with_all_possible_members.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_all_possible_members.json rename to JSONAPI.Tests/Json/Fixtures/ErrorFormatter/Serialize_error_with_all_possible_members.json diff --git a/JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_only_id.json b/JSONAPI.Tests/Json/Fixtures/ErrorFormatter/Serialize_error_with_only_id.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ErrorSerializer/Serialize_error_with_only_id.json rename to JSONAPI.Tests/Json/Fixtures/ErrorFormatter/Serialize_error_with_only_id.json diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorDocument.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorDocument.json new file mode 100644 index 00000000..3253e614 --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorDocument.json @@ -0,0 +1 @@ +"ErrorDocument output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json deleted file mode 100644 index 71800592..00000000 --- a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json +++ /dev/null @@ -1 +0,0 @@ -"ErrorPayload output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json index 77021070..849578d6 100644 --- a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json +++ b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json @@ -1 +1 @@ -"HttpError payload" \ No newline at end of file +"HttpError document" \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionDocument.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionDocument.json new file mode 100644 index 00000000..a6b86650 --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionDocument.json @@ -0,0 +1 @@ +"ResourceCollectionDocument output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json deleted file mode 100644 index 43abbd9b..00000000 --- a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json +++ /dev/null @@ -1 +0,0 @@ -"ResourceCollectionPayload output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourceDocument.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourceDocument.json new file mode 100644 index 00000000..5f1392e6 --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourceDocument.json @@ -0,0 +1 @@ +"SingleResourceDocument output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json b/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json deleted file mode 100644 index 1a4c6bb3..00000000 --- a/JSONAPI.Tests/Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json +++ /dev/null @@ -1 +0,0 @@ -"SingleResourcePayload output goes here." \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_with_metadata.json b/JSONAPI.Tests/Json/Fixtures/LinkFormatter/Serialize_link_with_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_with_metadata.json rename to JSONAPI.Tests/Json/Fixtures/LinkFormatter/Serialize_link_with_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_without_metadata.json b/JSONAPI.Tests/Json/Fixtures/LinkFormatter/Serialize_link_without_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/LinkSerializer/Serialize_link_without_metadata.json rename to JSONAPI.Tests/Json/Fixtures/LinkFormatter/Serialize_link_without_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_metadata.json b/JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Deserialize_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_metadata.json rename to JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Deserialize_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_null_metadata.json b/JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Deserialize_null_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Deserialize_null_metadata.json rename to JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Deserialize_null_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_metadata.json b/JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Serialize_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_metadata.json rename to JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Serialize_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_null_metadata.json b/JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Serialize_null_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/MetadataSerializer/Serialize_null_metadata.json rename to JSONAPI.Tests/Json/Fixtures/MetadataFormatter/Serialize_null_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Deserialize_relationship_object.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Deserialize_relationship_object.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Deserialize_relationship_object.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Deserialize_relationship_object.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_all_possible_members.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_all_possible_members.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_all_possible_members.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_linkage_only.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_linkage_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_linkage_only.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_linkage_only.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_meta_only.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_meta_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_meta_only.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_meta_only.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_related_link_only.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_related_link_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_related_link_only.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_related_link_only.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_and_related_link.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_and_related_link.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_and_related_link.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_and_related_link.json diff --git a/JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_only.json b/JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_only.json rename to JSONAPI.Tests/Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_only.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_metadata.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_metadata.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data_and_unknown_top_level_key.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data_and_unknown_top_level_key.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data_and_unknown_top_level_key.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data_and_unknown_top_level_key.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_empty_payload.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_empty_document.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_empty_payload.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_empty_document.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_all_possible_members.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata.json rename to JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_integer.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_integer.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_integer.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_integer.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_string.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_string.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_string.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_string.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_null_to_one_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_null_to_one_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_null_to_one_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_null_to_one_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_many_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_many_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_many_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_many_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_one_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_one_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_one_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_one_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_ToOneResourceLinkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_ToOneResourceLinkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_ToOneResourceLinkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_ToOneResourceLinkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_null_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageSerializer/Serialize_null_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Deserialize_resource_object.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Deserialize_resource_object.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Deserialize_resource_object.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Deserialize_resource_object.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_all_possible_members.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_all_possible_members.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_all_possible_members.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_attributes.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_attributes.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_attributes.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_attributes.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_links.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_links.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_links.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_links.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_metadata.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_metadata.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_only_null_relationships.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_only_null_relationships.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_only_null_relationships.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_only_null_relationships.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_relationships.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_relationships.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_relationships.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_relationships.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_without_attributes.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_without_attributes.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_without_attributes.json rename to JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_without_attributes.json diff --git a/JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_payload_with_resource.json b/JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_document_with_resource.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_payload_with_resource.json rename to JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_document_with_resource.json diff --git a/JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_null_payload.json b/JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_null_document.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_null_payload.json rename to JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_null_document.json diff --git a/JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_all_possible_members.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_all_possible_members.json rename to JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_all_possible_members.json diff --git a/JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_and_metadata.json b/JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_and_metadata.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_and_metadata.json rename to JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_and_metadata.json diff --git a/JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_only.json b/JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_only.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_only.json rename to JSONAPI.Tests/Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_only.json diff --git a/JSONAPI.Tests/Json/JsonApiFormatterTests.cs b/JSONAPI.Tests/Json/JsonApiFormatterTests.cs index 2565ddd2..9e5008f8 100644 --- a/JSONAPI.Tests/Json/JsonApiFormatterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiFormatterTests.cs @@ -7,8 +7,8 @@ using JSONAPI.Json; using System.IO; using System.Net; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using Moq; namespace JSONAPI.Tests.Json @@ -16,111 +16,111 @@ namespace JSONAPI.Tests.Json [TestClass] public class JsonApiFormatterTests { - private JsonApiFormatter BuildFormatter(ISingleResourcePayloadSerializer singleResourcePayloadSerializer = null, - IResourceCollectionPayloadSerializer resourceCollectionPayloadSerializer = null, - IErrorPayloadSerializer errorPayloadSerializer = null, - IErrorPayloadBuilder errorPayloadBuilder = null) + private JsonApiFormatter BuildFormatter(ISingleResourceDocumentFormatter singleResourceDocumentFormatter = null, + IResourceCollectionDocumentFormatter resourceCollectionDocumentFormatter = null, + IErrorDocumentFormatter errorDocumentFormatter = null, + IErrorDocumentBuilder errorDocumentBuilder = null) { - singleResourcePayloadSerializer = singleResourcePayloadSerializer ?? new Mock(MockBehavior.Strict).Object; - resourceCollectionPayloadSerializer = resourceCollectionPayloadSerializer ?? new Mock(MockBehavior.Strict).Object; - errorPayloadSerializer = errorPayloadSerializer ?? new Mock(MockBehavior.Strict).Object; - errorPayloadBuilder = errorPayloadBuilder ?? new ErrorPayloadBuilder(); - return new JsonApiFormatter(singleResourcePayloadSerializer, resourceCollectionPayloadSerializer, errorPayloadSerializer, errorPayloadBuilder); + singleResourceDocumentFormatter = singleResourceDocumentFormatter ?? new Mock(MockBehavior.Strict).Object; + resourceCollectionDocumentFormatter = resourceCollectionDocumentFormatter ?? new Mock(MockBehavior.Strict).Object; + errorDocumentFormatter = errorDocumentFormatter ?? new Mock(MockBehavior.Strict).Object; + errorDocumentBuilder = errorDocumentBuilder ?? new ErrorDocumentBuilder(); + return new JsonApiFormatter(singleResourceDocumentFormatter, resourceCollectionDocumentFormatter, errorDocumentFormatter, errorDocumentBuilder); } [TestMethod] - public void Serialize_SingleResourcePayload() + public void Serialize_SingleResourceDocument() { // Arrange - var payload = new Mock(MockBehavior.Strict); - var singleResourcePayloadSerializer = new Mock(MockBehavior.Strict); - singleResourcePayloadSerializer.Setup(s => s.Serialize(payload.Object, It.IsAny())) - .Returns((ISingleResourcePayload p, JsonWriter writer) => + var mockSingleResourceDocument = new Mock(MockBehavior.Strict); + var singleResourceDocumentFormatter = new Mock(MockBehavior.Strict); + singleResourceDocumentFormatter.Setup(s => s.Serialize(mockSingleResourceDocument.Object, It.IsAny())) + .Returns((ISingleResourceDocument p, JsonWriter writer) => { - writer.WriteValue("SingleResourcePayload output goes here."); + writer.WriteValue("SingleResourceDocument output goes here."); return Task.FromResult(0); }); - var formatter = BuildFormatter(singleResourcePayloadSerializer.Object); + var formatter = BuildFormatter(singleResourceDocumentFormatter.Object); var stream = new MemoryStream(); // Act - formatter.WriteToStreamAsync(payload.Object.GetType(), payload.Object, stream, null, null).Wait(); + formatter.WriteToStreamAsync(mockSingleResourceDocument.Object.GetType(), mockSingleResourceDocument.Object, stream, null, null).Wait(); // Assert - TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_SingleResourcePayload.json"); + TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_SingleResourceDocument.json"); } [TestMethod] - public void Serialize_ResourceCollectionPayload() + public void Serialize_ResourceCollectionDocument() { // Arrange - var payload = new Mock(MockBehavior.Strict); - var resourceCollectionPayloadSerializer = new Mock(MockBehavior.Strict); - resourceCollectionPayloadSerializer.Setup(s => s.Serialize(payload.Object, It.IsAny())) - .Returns((IResourceCollectionPayload p, JsonWriter writer) => + var mockResourceCollectionDocument = new Mock(MockBehavior.Strict); + var resourceCollectionDocumentFormatter = new Mock(MockBehavior.Strict); + resourceCollectionDocumentFormatter.Setup(s => s.Serialize(mockResourceCollectionDocument.Object, It.IsAny())) + .Returns((IResourceCollectionDocument p, JsonWriter writer) => { - writer.WriteValue("ResourceCollectionPayload output goes here."); + writer.WriteValue("ResourceCollectionDocument output goes here."); return Task.FromResult(0); }); - var formatter = BuildFormatter(resourceCollectionPayloadSerializer: resourceCollectionPayloadSerializer.Object); + var formatter = BuildFormatter(resourceCollectionDocumentFormatter: resourceCollectionDocumentFormatter.Object); var stream = new MemoryStream(); // Act - formatter.WriteToStreamAsync(payload.Object.GetType(), payload.Object, stream, null, null).Wait(); + formatter.WriteToStreamAsync(mockResourceCollectionDocument.Object.GetType(), mockResourceCollectionDocument.Object, stream, null, null).Wait(); // Assert - TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionPayload.json"); + TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_ResourceCollectionDocument.json"); } [TestMethod] - public void Serialize_ErrorPayload() + public void Serialize_ErrorDocument() { // Arrange - var payload = new Mock(MockBehavior.Strict); - var errorPayloadSerializer = new Mock(MockBehavior.Strict); - errorPayloadSerializer.Setup(s => s.Serialize(payload.Object, It.IsAny())) - .Returns((IErrorPayload p, JsonWriter writer) => + var errorDocument = new Mock(MockBehavior.Strict); + var errorDocumentFormatter = new Mock(MockBehavior.Strict); + errorDocumentFormatter.Setup(s => s.Serialize(errorDocument.Object, It.IsAny())) + .Returns((IErrorDocument p, JsonWriter writer) => { - writer.WriteValue("ErrorPayload output goes here."); + writer.WriteValue("ErrorDocument output goes here."); return Task.FromResult(0); }); - var formatter = BuildFormatter(errorPayloadSerializer: errorPayloadSerializer.Object); + var formatter = BuildFormatter(errorDocumentFormatter: errorDocumentFormatter.Object); var stream = new MemoryStream(); // Act - formatter.WriteToStreamAsync(payload.Object.GetType(), payload.Object, stream, null, null).Wait(); + formatter.WriteToStreamAsync(errorDocument.Object.GetType(), errorDocument.Object, stream, null, null).Wait(); // Assert - TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_ErrorPayload.json"); + TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_ErrorDocument.json"); } [TestMethod] public void Serialize_HttpError() { // Arrange - var payload = new HttpError(new Exception("This is the exception message"), true); - var mockErrorPayloadBuilder = new Mock(MockBehavior.Strict); - var mockErrorPayload = new Mock(MockBehavior.Strict); - mockErrorPayloadBuilder.Setup(b => b.BuildFromHttpError(payload, HttpStatusCode.InternalServerError)) - .Returns(mockErrorPayload.Object); - - var mockErrorPayloadSerializer = new Mock(MockBehavior.Strict); - mockErrorPayloadSerializer.Setup(s => s.Serialize(mockErrorPayload.Object, It.IsAny())) - .Returns((IErrorPayload errorPayload, JsonWriter writer) => + var httpError = new HttpError(new Exception("This is the exception message"), true); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocument = new Mock(MockBehavior.Strict); + mockErrorDocumentBuilder.Setup(b => b.BuildFromHttpError(httpError, HttpStatusCode.InternalServerError)) + .Returns(mockErrorDocument.Object); + + var mockErrorDocumentFormatter = new Mock(MockBehavior.Strict); + mockErrorDocumentFormatter.Setup(s => s.Serialize(mockErrorDocument.Object, It.IsAny())) + .Returns((IErrorDocument errorDocument, JsonWriter writer) => { - writer.WriteValue("HttpError payload"); + writer.WriteValue("HttpError document"); return Task.FromResult(0); }); var stream = new MemoryStream(); // Act - var formatter = BuildFormatter(errorPayloadBuilder: mockErrorPayloadBuilder.Object, errorPayloadSerializer: mockErrorPayloadSerializer.Object); - formatter.WriteToStreamAsync(payload.GetType(), payload, stream, null, null).Wait(); + var formatter = BuildFormatter(errorDocumentBuilder: mockErrorDocumentBuilder.Object, errorDocumentFormatter: mockErrorDocumentFormatter.Object); + formatter.WriteToStreamAsync(httpError.GetType(), httpError, stream, null, null).Wait(); // Assert TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Serialize_HttpError.json"); @@ -141,596 +141,49 @@ public void Writes_error_for_anything_else() var stream = new MemoryStream(); // Act - var payload = new Color { Id = "1", Name = "Blue" }; - formatter.WriteToStreamAsync(payload.GetType(), payload, stream, null, null).Wait(); + var resource = new Color { Id = "1", Name = "Blue" }; + formatter.WriteToStreamAsync(resource.GetType(), resource, stream, null, null).Wait(); // Assert TestHelpers.StreamContentsMatchFixtureContents(stream, "Json/Fixtures/JsonApiFormatter/Writes_error_for_anything_else.json"); } [TestMethod] - public void ReadFromStreamAsync_deserializes_ISingleResourcePayload() + public void ReadFromStreamAsync_deserializes_ISingleResourceDocument() { // Arrange - var mockSingleResourcePayload = new Mock(MockBehavior.Strict); - var singleResourcePayloadSerializer = new Mock(MockBehavior.Strict); - singleResourcePayloadSerializer.Setup(s => s.Deserialize(It.IsAny(), "")) - .Returns(Task.FromResult(mockSingleResourcePayload.Object)); + var mockSingleResourceDocument = new Mock(MockBehavior.Strict); + var singleResourceDocumentFormatter = new Mock(MockBehavior.Strict); + singleResourceDocumentFormatter.Setup(s => s.Deserialize(It.IsAny(), "")) + .Returns(Task.FromResult(mockSingleResourceDocument.Object)); - var formatter = BuildFormatter(singleResourcePayloadSerializer.Object); + var formatter = BuildFormatter(singleResourceDocumentFormatter.Object); var stream = new MemoryStream(); // Act - var deserialized = formatter.ReadFromStreamAsync(typeof(ISingleResourcePayload), stream, null, null).Result; + var deserialized = formatter.ReadFromStreamAsync(typeof(ISingleResourceDocument), stream, null, null).Result; // Assert - deserialized.Should().BeSameAs(mockSingleResourcePayload.Object); + deserialized.Should().BeSameAs(mockSingleResourceDocument.Object); } [TestMethod] - public void ReadFromStreamAsync_deserializes_IResourceCollectionPayload() + public void ReadFromStreamAsync_deserializes_IResourceCollectionDocument() { // Arrange - var mockResourceCollectionPayload = new Mock(MockBehavior.Strict); - var resourceCollectionPayloadSerializer = new Mock(MockBehavior.Strict); - resourceCollectionPayloadSerializer.Setup(s => s.Deserialize(It.IsAny(), "")) - .Returns(Task.FromResult(mockResourceCollectionPayload.Object)); + var mockResourceCollectionDocument = new Mock(MockBehavior.Strict); + var resourceCollectionDocumentFormatter = new Mock(MockBehavior.Strict); + resourceCollectionDocumentFormatter.Setup(s => s.Deserialize(It.IsAny(), "")) + .Returns(Task.FromResult(mockResourceCollectionDocument.Object)); - var formatter = BuildFormatter(resourceCollectionPayloadSerializer: resourceCollectionPayloadSerializer.Object); + var formatter = BuildFormatter(resourceCollectionDocumentFormatter: resourceCollectionDocumentFormatter.Object); var stream = new MemoryStream(); // Act - var deserialized = formatter.ReadFromStreamAsync(typeof(IResourceCollectionPayload), stream, null, null).Result; + var deserialized = formatter.ReadFromStreamAsync(typeof(IResourceCollectionDocument), stream, null, null).Result; // Assert - deserialized.Should().BeSameAs(mockResourceCollectionPayload.Object); + deserialized.Should().BeSameAs(mockResourceCollectionDocument.Object); } - - //Author a; - //Post p, p2, p3, p4; - //Sample s1, s2; - //Tag t1, t2, t3; - - //private class MockErrorSerializer : IErrorSerializer - //{ - // public bool CanSerialize(Type type) - // { - // return true; - // } - - // public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer) - // { - // writer.WriteStartObject(); - // writer.WritePropertyName("test"); - // serializer.Serialize(writer, "foo"); - // writer.WriteEndObject(); - // } - //} - - //private class NonStandardIdThing - //{ - // [JSONAPI.Attributes.UseAsId] - // public Guid Uuid { get; set; } - // public string Data { get; set; } - //} - - //[TestInitialize] - //public void SetupModels() - //{ - // a = new Author - // { - // Id = 1, - // Name = "Jason Hater", - // }; - - // t1 = new Tag - // { - // Id = 1, - // Text = "Ember" - // }; - // t2 = new Tag - // { - // Id = 2, - // Text = "React" - // }; - // t3 = new Tag - // { - // Id = 3, - // Text = "Angular" - // }; - - // p = new Post() - // { - // Id = 1, - // Title = "Linkbait!", - // Author = a - // }; - // p2 = new Post - // { - // Id = 2, - // Title = "Rant #1023", - // Author = a - // }; - // p3 = new Post - // { - // Id = 3, - // Title = "Polemic in E-flat minor #824", - // Author = a - // }; - // p4 = new Post - // { - // Id = 4, - // Title = "This post has no author." - // }; - - // a.Posts = new List { p, p2, p3 }; - - // p.Comments = new List() { - // new Comment() { - // Id = 2, - // Body = "Nuh uh!", - // Post = p - // }, - // new Comment() { - // Id = 3, - // Body = "Yeah huh!", - // Post = p - // }, - // new Comment() { - // Id = 4, - // Body = "Third Reich.", - // Post = p, - // CustomData = "{ \"foo\": \"bar\" }" - // } - // }; - // p2.Comments = new List { - // new Comment { - // Id = 5, - // Body = "I laughed, I cried!", - // Post = p2 - // } - // }; - - //} - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\SerializerIntegrationTest.json")] - public void SerializerIntegrationTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Author)); - //modelManager.RegisterResourceType(typeof(Comment)); - //modelManager.RegisterResourceType(typeof(Post)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }.ToList(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - //// Assert - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); - //Assert.AreEqual(expected, output.Trim()); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\SerializerIntegrationTest.json")] - public void SerializeArrayIntegrationTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Author)); - //modelManager.RegisterResourceType(typeof(Comment)); - //modelManager.RegisterResourceType(typeof(Post)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Post), new[] { p, p2, p3, p4 }, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - //// Assert - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //var expected = JsonHelpers.MinifyJson(File.ReadAllText("SerializerIntegrationTest.json")); - //Assert.AreEqual(expected, output.Trim()); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\AttributeSerializationTest.json")] - public void Serializes_attributes_properly() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Sample)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Sample), new[] { s1, s2 }, stream, null, null); - - //// Assert - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //var expected = JsonHelpers.MinifyJson(File.ReadAllText("AttributeSerializationTest.json")); - //Assert.AreEqual(expected, output.Trim()); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\ByteIdSerializationTest.json")] - public void Serializes_byte_ids_properly() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Tag)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Tag), new[] { t1, t2, t3 }, stream, null, null); - - //// Assert - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //var expected = JsonHelpers.MinifyJson(File.ReadAllText("ByteIdSerializationTest.json")); - //Assert.AreEqual(expected, output.Trim()); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\ReformatsRawJsonStringWithUnquotedKeys.json")] - public void Reformats_raw_json_string_with_unquoted_keys() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Comment)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //var payload = new [] { new Comment { Id = 5, CustomData = "{ unquotedKey: 5 }"}}; - //formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); - - //// Assert - //var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("ReformatsRawJsonStringWithUnquotedKeys.json")); - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\NullResourceResult.json")] - public void Serializes_null_resource_properly() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Comment)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Comment), null, stream, null, null); - - //// Assert - //var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("NullResourceResult.json")); - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\EmptyArrayResult.json")] - public void Serializes_null_resource_array_as_empty_array() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Comment)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(Comment[]), null, stream, null, null); - - //// Assert - //var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("EmptyArrayResult.json")); - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\EmptyArrayResult.json")] - public void Serializes_null_list_as_empty_array() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Comment)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //formatter.WriteToStreamAsync(typeof(List), null, stream, null, null); - - //// Assert - //var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("EmptyArrayResult.json")); - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\MalformedRawJsonString.json")] - public void Does_not_serialize_malformed_raw_json_string() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Comment)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //// Act - //var payload = new[] { new Comment { Id = 5, CustomData = "{ x }" } }; - //formatter.WriteToStreamAsync(typeof(Comment), payload, stream, null, null); - - //// Assert - //var minifiedExpectedJson = JsonHelpers.MinifyJson(File.ReadAllText("MalformedRawJsonString.json")); - //string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //Trace.WriteLine(output); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\FormatterErrorSerializationTest.json")] - public void Should_serialize_error() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //var formatter = new JsonApiFormatter(modelManager, new MockErrorSerializer()); - //var stream = new MemoryStream(); - - //// Act - //var payload = new HttpError(new Exception(), true); - //formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - //// Assert - //var expectedJson = File.ReadAllText("FormatterErrorSerializationTest.json"); - //var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - //var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\ErrorSerializerTest.json")] - public void SerializeErrorIntegrationTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //var mockInnerException = new Mock(MockBehavior.Strict); - //mockInnerException.Setup(m => m.Message).Returns("Inner exception message"); - //mockInnerException.Setup(m => m.StackTrace).Returns("Inner stack trace"); - - //var outerException = new Exception("Outer exception message", mockInnerException.Object); - - //var payload = new HttpError(outerException, true) - //{ - // StackTrace = "Outer stack trace" - //}; - - //// Act - //formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - //// Assert - //var expectedJson = File.ReadAllText("ErrorSerializerTest.json"); - //var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - //var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - - //// We don't know what the GUIDs will be, so replace them - //var regex = new Regex(@"[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}"); - //output = regex.Replace(output, "OUTER-ID", 1); - //output = regex.Replace(output, "INNER-ID", 1); - //output.Should().Be(minifiedExpectedJson); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\DeserializeCollectionRequest.json")] - public void Deserializes_collections_properly() - { - //using (var inputStream = File.OpenRead("DeserializeCollectionRequest.json")) - //{ - // // Arrange - // var modelManager = new ModelManager(new PluralizationService()); - // modelManager.RegisterResourceType(typeof(Post)); - // modelManager.RegisterResourceType(typeof(Author)); - // modelManager.RegisterResourceType(typeof(Comment)); - // var formatter = new JsonApiFormatter(modelManager); - - // // Act - // var posts = (IList)formatter.ReadFromStreamAsync(typeof(Post), inputStream, null, null).Result; - - // // Assert - // posts.Count.Should().Be(2); - // posts[0].Id.Should().Be(p.Id); - // posts[0].Title.Should().Be(p.Title); - // posts[0].Author.Id.Should().Be(a.Id); - // posts[0].Comments.Count.Should().Be(2); - // posts[0].Comments[0].Id.Should().Be(400); - // posts[0].Comments[1].Id.Should().Be(401); - // posts[1].Id.Should().Be(p2.Id); - // posts[1].Title.Should().Be(p2.Title); - // posts[1].Author.Id.Should().Be(a.Id); - //} - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\DeserializeAttributeRequest.json")] - public async Task Deserializes_attributes_properly() - { - //using (var inputStream = File.OpenRead("DeserializeAttributeRequest.json")) - //{ - // // Arrange - // var modelManager = new ModelManager(new PluralizationService()); - // modelManager.RegisterResourceType(typeof(Sample)); - // var formatter = new JsonApiFormatter(modelManager); - - // // Act - // var deserialized = (IList)await formatter.ReadFromStreamAsync(typeof(Sample), inputStream, null, null); - - // // Assert - // deserialized.Count.Should().Be(2); - // deserialized[0].ShouldBeEquivalentTo(s1); - // deserialized[1].ShouldBeEquivalentTo(s2); - //} - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\DeserializeRawJsonTest.json")] - public async Task DeserializeRawJsonTest() - { - //using (var inputStream = File.OpenRead("DeserializeRawJsonTest.json")) - //{ - // // Arrange - // var modelManager = new ModelManager(new PluralizationService()); - // modelManager.RegisterResourceType(typeof(Comment)); - // var formatter = new JsonApiFormatter(modelManager); - - // // Act - // var comments = ((IEnumerable)await formatter.ReadFromStreamAsync(typeof (Comment), inputStream, null, null)).ToArray(); - - // // Assert - // Assert.AreEqual(2, comments.Count()); - // Assert.AreEqual(null, comments[0].CustomData); - // Assert.AreEqual("{\"foo\":\"bar\"}", comments[1].CustomData); - //} - } - - // Issue #1 - [Ignore] - [TestMethod(), Timeout(1000)] - public void DeserializeExtraPropertyTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Author)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""type"":""authors"",""attributes"":{""name"":""Jason Hater"",""bogus"":""PANIC!""},""relationships"":{""posts"":{""data"":[]}}}}")); - - //// Act - //Author a; - //a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - //// Assert - //Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! - } - - // Issue #1 - [Ignore] - [TestMethod(), Timeout(1000)] - public void DeserializeExtraRelationshipTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(Author)); - //var formatter = new JsonApiFormatter(modelManager); - //MemoryStream stream = new MemoryStream(); - - //stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""data"":{""id"":13,""type"":""authors"",""attributes"":{""name"":""Jason Hater""},""relationships"":{""posts"":{""data"":[]},""bogus"":{""data"":[]}}}}")); - - //// Act - //Author a; - //a = (Author)formatter.ReadFromStreamAsync(typeof(Author), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - //// Assert - //Assert.AreEqual("Jason Hater", a.Name); // Completed without exceptions and didn't timeout! - } - - [TestMethod] - [Ignore] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void SerializeNonStandardIdTest() - { - //// Arrange - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(NonStandardIdThing)); - //var formatter = new JsonApiFormatter(modelManager); - //var stream = new MemoryStream(); - //var payload = new List { - // new NonStandardIdThing { Uuid = new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"), Data = "Swap" } - //}; - - //// Act - //formatter.WriteToStreamAsync(typeof(List), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - //// Assert - //var expectedJson = File.ReadAllText("NonStandardIdTest.json"); - //var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); - //var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - //output.Should().Be(minifiedExpectedJson); - } - - #region Non-standard Id attribute tests - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardIdTest() - { - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(NonStandardIdThing)); - //var formatter = new JsonApiFormatter(modelManager); - //var stream = new FileStream("NonStandardIdTest.json",FileMode.Open); - - //// Act - //IList things; - //things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - //stream.Close(); - - //// Assert - //things.Count.Should().Be(1); - //things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - } - - [Ignore] - [TestMethod] - [DeploymentItem(@"Data\NonStandardIdTest.json")] - public void DeserializeNonStandardId() - { - //var modelManager = new ModelManager(new PluralizationService()); - //modelManager.RegisterResourceType(typeof(NonStandardIdThing)); - //var formatter = new JsonApiFormatter(modelManager); - //string json = File.ReadAllText("NonStandardIdTest.json"); - //json = Regex.Replace(json, @"""uuid"":\s*""0657fd6d-a4ab-43c4-84e5-0933c84b4f4f""\s*,",""); // remove the uuid attribute - //var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(json)); - - //// Act - //IList things; - //things = (IList)formatter.ReadFromStreamAsync(typeof(NonStandardIdThing), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - //// Assert - //json.Should().NotContain("uuid", "The \"uuid\" attribute was supposed to be removed, test methodology problem!"); - //things.Count.Should().Be(1); - //things.First().Uuid.Should().Be(new Guid("0657fd6d-a4ab-43c4-84e5-0933c84b4f4f")); - } - - #endregion - } } diff --git a/JSONAPI.Tests/Json/JsonApiSerializerTestsBase.cs b/JSONAPI.Tests/Json/JsonApiFormatterTestsBase.cs similarity index 67% rename from JSONAPI.Tests/Json/JsonApiSerializerTestsBase.cs rename to JSONAPI.Tests/Json/JsonApiFormatterTestsBase.cs index cbb30e64..c4e60065 100644 --- a/JSONAPI.Tests/Json/JsonApiSerializerTestsBase.cs +++ b/JSONAPI.Tests/Json/JsonApiFormatterTestsBase.cs @@ -8,12 +8,12 @@ namespace JSONAPI.Tests.Json { - public abstract class JsonApiSerializerTestsBase + public abstract class JsonApiFormatterTestsBase { - protected async Task AssertSerializeOutput(TSerializer serializer, TComponent component, string expectedJsonFile) - where TSerializer : IJsonApiSerializer + protected async Task AssertSerializeOutput(TFormatter formatter, TComponent component, string expectedJsonFile) + where TFormatter : IJsonApiFormatter { - var output = await GetSerializedString(serializer, component); + var output = await GetSerializedString(formatter, component); // Assert var expectedJson = TestHelpers.ReadEmbeddedFile(expectedJsonFile); @@ -21,8 +21,8 @@ protected async Task AssertSerializeOutput(TSerializer output.Should().Be(minifiedExpectedJson); } - protected async Task GetSerializedString(TSerializer serializer, TComponent component) - where TSerializer : IJsonApiSerializer + protected async Task GetSerializedString(TFormatter formatter, TComponent component) + where TFormatter : IJsonApiFormatter { using (var stream = new MemoryStream()) { @@ -30,7 +30,7 @@ protected async Task GetSerializedString(TSeria { using (var writer = new JsonTextWriter(textWriter)) { - await serializer.Serialize(component, writer); + await formatter.Serialize(component, writer); writer.Flush(); return Encoding.ASCII.GetString(stream.ToArray()); } @@ -38,8 +38,8 @@ protected async Task GetSerializedString(TSeria } } - protected async Task GetDeserializedOutput(TSerializer serializer, string requestFileName) - where TSerializer : IJsonApiSerializer + protected async Task GetDeserializedOutput(TFormatter formatter, string requestFileName) + where TFormatter : IJsonApiFormatter { var resourcePath = "JSONAPI.Tests." + requestFileName.Replace("\\", ".").Replace("/", "."); using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath)) @@ -50,7 +50,7 @@ protected async Task GetDeserializedOutput( using (var reader = new JsonTextReader(textReader)) { reader.Read(); - var deserialized = await serializer.Deserialize(reader, ""); + var deserialized = await formatter.Deserialize(reader, ""); reader.Read().Should().BeFalse(); // There should be nothing left to read. return deserialized; } diff --git a/JSONAPI.Tests/Json/LinkSerializerTests.cs b/JSONAPI.Tests/Json/LinkFormatterTests.cs similarity index 51% rename from JSONAPI.Tests/Json/LinkSerializerTests.cs rename to JSONAPI.Tests/Json/LinkFormatterTests.cs index 2fcf29c2..3c871686 100644 --- a/JSONAPI.Tests/Json/LinkSerializerTests.cs +++ b/JSONAPI.Tests/Json/LinkFormatterTests.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Tests.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -9,22 +8,22 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class LinkSerializerTests : JsonApiSerializerTestsBase + public class LinkFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public async Task Serialize_link_without_metadata() { ILink link = new Link("http://www.example.com", null); - var serializer = new LinkSerializer(null); - await AssertSerializeOutput(serializer, link, "Json/Fixtures/LinkSerializer/Serialize_link_without_metadata.json"); + var formatter = new LinkFormatter(null); + await AssertSerializeOutput(formatter, link, "Json/Fixtures/LinkFormatter/Serialize_link_without_metadata.json"); } [TestMethod] public async Task Serialize_link_with_metadata() { - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { writer.WriteValue("IMetadata placeholder 1"); @@ -34,9 +33,9 @@ public async Task Serialize_link_with_metadata() var mockMetadata = new Mock(MockBehavior.Strict); ILink link = new Link("http://www.example.com", mockMetadata.Object); - var serializer = new LinkSerializer(mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, link, "Json/Fixtures/LinkSerializer/Serialize_link_with_metadata.json"); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + var formatter = new LinkFormatter(mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, link, "Json/Fixtures/LinkFormatter/Serialize_link_with_metadata.json"); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); } } } diff --git a/JSONAPI.Tests/Json/MetadataSerializerTests.cs b/JSONAPI.Tests/Json/MetadataFormatterTests.cs similarity index 65% rename from JSONAPI.Tests/Json/MetadataSerializerTests.cs rename to JSONAPI.Tests/Json/MetadataFormatterTests.cs index fc75b130..4d925451 100644 --- a/JSONAPI.Tests/Json/MetadataSerializerTests.cs +++ b/JSONAPI.Tests/Json/MetadataFormatterTests.cs @@ -1,9 +1,8 @@ using System; using System.Threading.Tasks; using FluentAssertions; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Tests.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -12,13 +11,13 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class MetadataSerializerTests : JsonApiSerializerTestsBase + public class MetadataFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public async Task Serialize_null_metadata() { - var serializer = new MetadataSerializer(); - await AssertSerializeOutput(serializer, (IMetadata)null, "Json/Fixtures/MetadataSerializer/Serialize_null_metadata.json"); + var formatter = new MetadataFormatter(); + await AssertSerializeOutput(formatter, (IMetadata)null, "Json/Fixtures/MetadataFormatter/Serialize_null_metadata.json"); } [TestMethod] @@ -38,8 +37,8 @@ public async Task Serialize_metadata() return obj; }); - var serializer = new MetadataSerializer(); - await AssertSerializeOutput(serializer, mockMetadata.Object, "Json/Fixtures/MetadataSerializer/Serialize_metadata.json"); + var formatter = new MetadataFormatter(); + await AssertSerializeOutput(formatter, mockMetadata.Object, "Json/Fixtures/MetadataFormatter/Serialize_metadata.json"); } [TestMethod] @@ -49,12 +48,12 @@ public void Serialize_metadata_should_fail_if_object_is_null() mockMetadata.Setup(m => m.MetaObject) .Returns(() => null); - var serializer = new MetadataSerializer(); + var formatter = new MetadataFormatter(); Func action = async () => { await - GetSerializedString(serializer, mockMetadata.Object); + GetSerializedString(formatter, mockMetadata.Object); }; action.ShouldThrow() .WithMessage("The meta object cannot be null."); @@ -66,10 +65,10 @@ public void Deserialize_null_metadata() // Arrange // Act - var serializer = new MetadataSerializer(); + var formatter = new MetadataFormatter(); var metadata = - GetDeserializedOutput(serializer, - "Json/Fixtures/MetadataSerializer/Deserialize_null_metadata.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/MetadataFormatter/Deserialize_null_metadata.json").Result; // Assert metadata.Should().BeNull(); @@ -81,10 +80,10 @@ public void Deserialize_metadata() // Arrange // Act - var serializer = new MetadataSerializer(); + var formatter = new MetadataFormatter(); var metadata = - GetDeserializedOutput(serializer, - "Json/Fixtures/MetadataSerializer/Deserialize_metadata.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/MetadataFormatter/Deserialize_metadata.json").Result; // Assert ((int) metadata.MetaObject["foo"]).Should().Be(13); diff --git a/JSONAPI.Tests/Json/RelationshipObjectSerializerTests.cs b/JSONAPI.Tests/Json/RelationshipObjectFormatterTests.cs similarity index 59% rename from JSONAPI.Tests/Json/RelationshipObjectSerializerTests.cs rename to JSONAPI.Tests/Json/RelationshipObjectFormatterTests.cs index f9864ab6..b5d687ff 100644 --- a/JSONAPI.Tests/Json/RelationshipObjectSerializerTests.cs +++ b/JSONAPI.Tests/Json/RelationshipObjectFormatterTests.cs @@ -1,9 +1,8 @@ using System; using System.Threading.Tasks; using FluentAssertions; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Tests.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -11,18 +10,18 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class RelationshipObjectSerializerTests : JsonApiSerializerTestsBase + public class RelationshipObjectFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public void Serialize_relationship_with_no_required_fields() { - var serializer = new RelationshipObjectSerializer(null, null, null); + var formatter = new RelationshipObjectFormatter(null, null, null); IRelationshipObject relationshipObject = new RelationshipObject(null, null, null); Func action = async () => { await - GetSerializedString(serializer, relationshipObject); + GetSerializedString(formatter, relationshipObject); }; action.ShouldThrow() .WithMessage("At least one of `links`, `data`, or `meta` must be present in a relationship object."); @@ -33,8 +32,8 @@ public async Task Serialize_relationship_with_self_link_only() { var mockSelfLink = new Mock(MockBehavior.Strict); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter .Setup(s => s.Serialize(mockSelfLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { @@ -42,11 +41,11 @@ public async Task Serialize_relationship_with_self_link_only() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(mockLinkSerializer.Object, null, null); + var formatter = new RelationshipObjectFormatter(mockLinkFormatter.Object, null, null); IRelationshipObject resourceObject = new RelationshipObject(mockSelfLink.Object, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_only.json"); - mockLinkSerializer.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_only.json"); + mockLinkFormatter.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -54,8 +53,8 @@ public async Task Serialize_relationship_with_related_link_only() { var mockRelatedLink = new Mock(MockBehavior.Strict); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter .Setup(s => s.Serialize(mockRelatedLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { @@ -63,11 +62,11 @@ public async Task Serialize_relationship_with_related_link_only() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(mockLinkSerializer.Object, null, null); + var formatter = new RelationshipObjectFormatter(mockLinkFormatter.Object, null, null); IRelationshipObject resourceObject = new RelationshipObject(null, mockRelatedLink.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_related_link_only.json"); - mockLinkSerializer.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_related_link_only.json"); + mockLinkFormatter.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -76,15 +75,15 @@ public async Task Serialize_relationship_with_self_link_and_related_link() var mockSelfLink = new Mock(MockBehavior.Strict); var mockRelatedLink = new Mock(MockBehavior.Strict); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter .Setup(s => s.Serialize(mockSelfLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { writer.WriteValue("some self link"); return Task.FromResult(0); }).Verifiable(); - mockLinkSerializer + mockLinkFormatter .Setup(s => s.Serialize(mockRelatedLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { @@ -92,20 +91,20 @@ public async Task Serialize_relationship_with_self_link_and_related_link() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(mockLinkSerializer.Object, null, null); + var formatter = new RelationshipObjectFormatter(mockLinkFormatter.Object, null, null); IRelationshipObject resourceObject = new RelationshipObject(mockSelfLink.Object, mockRelatedLink.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_self_link_and_related_link.json"); - mockLinkSerializer.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); - mockLinkSerializer.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_self_link_and_related_link.json"); + mockLinkFormatter.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); + mockLinkFormatter.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); } [TestMethod] public async Task Serialize_relationship_with_linkage_only() { var mockLinkage = new Mock(MockBehavior.Strict); - var mockLinkageSerializer = new Mock(MockBehavior.Strict); - mockLinkageSerializer + var mockLinkageFormatter = new Mock(MockBehavior.Strict); + mockLinkageFormatter .Setup(s => s.Serialize(mockLinkage.Object, It.IsAny())) .Returns((IResourceLinkage metadata, JsonWriter writer) => { @@ -113,19 +112,19 @@ public async Task Serialize_relationship_with_linkage_only() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(null, mockLinkageSerializer.Object, null); + var formatter = new RelationshipObjectFormatter(null, mockLinkageFormatter.Object, null); IRelationshipObject resourceObject = new RelationshipObject(mockLinkage.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_linkage_only.json"); - mockLinkageSerializer.Verify(s => s.Serialize(mockLinkage.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_linkage_only.json"); + mockLinkageFormatter.Verify(s => s.Serialize(mockLinkage.Object, It.IsAny()), Times.Once); } [TestMethod] public async Task Serialize_relationship_with_meta_only() { var mockMetadata = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter .Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { @@ -133,11 +132,11 @@ public async Task Serialize_relationship_with_meta_only() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(null, null, mockMetadataSerializer.Object); + var formatter = new RelationshipObjectFormatter(null, null, mockMetadataFormatter.Object); IRelationshipObject resourceObject = new RelationshipObject(null, null, mockMetadata.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_meta_only.json"); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_meta_only.json"); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -146,15 +145,15 @@ public async Task Serialize_relationship_with_all_possible_members() var mockSelfLink = new Mock(MockBehavior.Strict); var mockRelatedLink = new Mock(MockBehavior.Strict); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter .Setup(s => s.Serialize(mockSelfLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { writer.WriteValue("some self link"); return Task.FromResult(0); }).Verifiable(); - mockLinkSerializer + mockLinkFormatter .Setup(s => s.Serialize(mockRelatedLink.Object, It.IsAny())) .Returns((ILink metadata, JsonWriter writer) => { @@ -163,8 +162,8 @@ public async Task Serialize_relationship_with_all_possible_members() }).Verifiable(); var mockLinkage = new Mock(MockBehavior.Strict); - var mockLinkageSerializer = new Mock(MockBehavior.Strict); - mockLinkageSerializer + var mockLinkageFormatter = new Mock(MockBehavior.Strict); + mockLinkageFormatter .Setup(s => s.Serialize(mockLinkage.Object, It.IsAny())) .Returns((IResourceLinkage metadata, JsonWriter writer) => { @@ -173,8 +172,8 @@ public async Task Serialize_relationship_with_all_possible_members() }).Verifiable(); var mockMetadata = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter .Setup(s => s.Serialize(mockMetadata.Object, It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { @@ -182,14 +181,14 @@ public async Task Serialize_relationship_with_all_possible_members() return Task.FromResult(0); }).Verifiable(); - var serializer = new RelationshipObjectSerializer(mockLinkSerializer.Object, mockLinkageSerializer.Object, mockMetadataSerializer.Object); + var formatter = new RelationshipObjectFormatter(mockLinkFormatter.Object, mockLinkageFormatter.Object, mockMetadataFormatter.Object); IRelationshipObject resourceObject = new RelationshipObject(mockLinkage.Object, mockSelfLink.Object, mockRelatedLink.Object, mockMetadata.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/RelationshipObjectSerializer/Serialize_relationship_with_all_possible_members.json"); - mockLinkSerializer.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); - mockLinkSerializer.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); - mockLinkageSerializer.Verify(s => s.Serialize(mockLinkage.Object, It.IsAny()), Times.Once); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/RelationshipObjectFormatter/Serialize_relationship_with_all_possible_members.json"); + mockLinkFormatter.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); + mockLinkFormatter.Verify(s => s.Serialize(mockRelatedLink.Object, It.IsAny()), Times.Once); + mockLinkageFormatter.Verify(s => s.Serialize(mockLinkage.Object, It.IsAny()), Times.Once); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -199,8 +198,8 @@ public void Deserialize_relationship_object() var mockLinkage = new Mock(MockBehavior.Strict); var mockMetadata = new Mock(MockBehavior.Strict); - var mockLinkageSerializer = new Mock(MockBehavior.Strict); - mockLinkageSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data")) + var mockLinkageFormatter = new Mock(MockBehavior.Strict); + mockLinkageFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data")) .Returns((JsonReader reader, string currentPath) => { reader.TokenType.Should().Be(JsonToken.String); @@ -208,8 +207,8 @@ public void Deserialize_relationship_object() return Task.FromResult(mockLinkage.Object); }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(s => s.Deserialize(It.IsAny(), "/meta")) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(s => s.Deserialize(It.IsAny(), "/meta")) .Returns((JsonReader reader, string currentPath) => { reader.TokenType.Should().Be(JsonToken.String); @@ -218,10 +217,10 @@ public void Deserialize_relationship_object() }); // Act - var serializer = new RelationshipObjectSerializer(null, mockLinkageSerializer.Object, mockMetadataSerializer.Object); + var formatter = new RelationshipObjectFormatter(null, mockLinkageFormatter.Object, mockMetadataFormatter.Object); var relationshipObject = - GetDeserializedOutput(serializer, - "Json/Fixtures/RelationshipObjectSerializer/Deserialize_relationship_object.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/RelationshipObjectFormatter/Deserialize_relationship_object.json").Result; // Assert relationshipObject.Linkage.Should().BeSameAs(mockLinkage.Object); diff --git a/JSONAPI.Tests/Json/ResourceCollectionDocumentFormatterTests.cs b/JSONAPI.Tests/Json/ResourceCollectionDocumentFormatterTests.cs new file mode 100644 index 00000000..212d4957 --- /dev/null +++ b/JSONAPI.Tests/Json/ResourceCollectionDocumentFormatterTests.cs @@ -0,0 +1,252 @@ +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Documents; +using JSONAPI.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; + +namespace JSONAPI.Tests.Json +{ + [TestClass] + public class ResourceCollectionDocumentFormatterTests : JsonApiFormatterTestsBase + { + [TestMethod] + public async Task Serialize_ResourceCollectionDocument_for_primary_data_only() + { + var primaryData1 = new Mock(MockBehavior.Strict); + var primaryData2 = new Mock(MockBehavior.Strict); + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 1"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 2"); + return Task.FromResult(0); + }); + + var primaryData = new[] { primaryData1.Object, primaryData2.Object }; + IResourceCollectionDocument document = new ResourceCollectionDocument(primaryData, null, null); + + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, null); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only.json"); + } + + [TestMethod] + public async Task Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata() + { + var primaryData1 = new Mock(MockBehavior.Strict); + var primaryData2 = new Mock(MockBehavior.Strict); + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 1"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 2"); + return Task.FromResult(0); + }); + + var mockMetadata = new Mock(MockBehavior.Strict); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(mockMetadata.Object, It.IsAny())) + .Returns((IMetadata resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder metadata object"); + return Task.FromResult(0); + }); + + var primaryData = new[] { primaryData1.Object, primaryData2.Object }; + IResourceCollectionDocument document = new ResourceCollectionDocument(primaryData, null, mockMetadata.Object); + + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json"); + } + + [TestMethod] + public async Task Serialize_ResourceCollectionDocument_for_all_possible_members() + { + var primaryData1 = new Mock(MockBehavior.Strict); + var primaryData2 = new Mock(MockBehavior.Strict); + var relatedResource1 = new Mock(MockBehavior.Strict); + var relatedResource2 = new Mock(MockBehavior.Strict); + var relatedResource3 = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 1"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data 2"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource1.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 1"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource2.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 2"); + return Task.FromResult(0); + }); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource3.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 3"); + return Task.FromResult(0); + }); + + var mockMetadata = new Mock(MockBehavior.Strict); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(mockMetadata.Object, It.IsAny())) + .Returns((IMetadata resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder metadata object"); + return Task.FromResult(0); + }); + + var primaryData = new[] { primaryData1.Object, primaryData2.Object }; + var relatedResources = new[] { relatedResource1.Object, relatedResource2.Object, relatedResource3.Object }; + IResourceCollectionDocument document = new ResourceCollectionDocument(primaryData, relatedResources, mockMetadata.Object); + + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json"); + } + + [TestMethod] + public void Deserialize_empty_document() + { + // Arrange + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + + // Act + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_empty_document.json").Result; + + // Assert + document.PrimaryData.Should().BeEquivalentTo(); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeNull(); + } + + [TestMethod] + public void Deserialize_document_with_primary_data() + { + // Arrange + var mockResource1 = new Mock(MockBehavior.Strict); + var mockResource2 = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data/0")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("PD1"); + return Task.FromResult(mockResource1.Object); + }); + mockResourceObjectFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data/1")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("PD2"); + return Task.FromResult(mockResource2.Object); + }); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + + // Act + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data.json").Result; + + // Assert + document.PrimaryData.Should().BeEquivalentTo(mockResource1.Object, mockResource2.Object); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeNull(); + } + + [TestMethod] + public void Deserialize_document_with_primary_data_and_unknown_top_level_key() + { + // Arrange + var mockResource1 = new Mock(MockBehavior.Strict); + var mockResource2 = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data/0")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("PD1"); + return Task.FromResult(mockResource1.Object); + }); + mockResourceObjectFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data/1")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("PD2"); + return Task.FromResult(mockResource2.Object); + }); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + + // Act + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_primary_data_and_unknown_top_level_key.json").Result; + + // Assert + document.PrimaryData.Should().BeEquivalentTo(mockResource1.Object, mockResource2.Object); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeNull(); + } + + [TestMethod] + public void Deserialize_document_with_metadata() + { + // Arrange + var mockMetadata = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(s => s.Deserialize(It.IsAny(), "/meta")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("metadata goes here"); + return Task.FromResult(mockMetadata.Object); + }); + + // Act + var formatter = new ResourceCollectionDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceCollectionDocumentFormatter/Deserialize_document_with_metadata.json").Result; + + // Assert + document.PrimaryData.Should().BeEquivalentTo(); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeSameAs(mockMetadata.Object); + } + } +} diff --git a/JSONAPI.Tests/Json/ResourceCollectionPayloadSerializerTests.cs b/JSONAPI.Tests/Json/ResourceCollectionPayloadSerializerTests.cs deleted file mode 100644 index b70d68a0..00000000 --- a/JSONAPI.Tests/Json/ResourceCollectionPayloadSerializerTests.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System.Threading.Tasks; -using FluentAssertions; -using JSONAPI.Json; -using JSONAPI.Payload; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Newtonsoft.Json; - -namespace JSONAPI.Tests.Json -{ - [TestClass] - public class ResourceCollectionPayloadSerializerTests : JsonApiSerializerTestsBase - { - [TestMethod] - public async Task Serialize_ResourceCollectionPayload_for_primary_data_only() - { - var primaryData1 = new Mock(MockBehavior.Strict); - var primaryData2 = new Mock(MockBehavior.Strict); - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 1"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 2"); - return Task.FromResult(0); - }); - - var primaryData = new[] { primaryData1.Object, primaryData2.Object }; - IResourceCollectionPayload payload = new ResourceCollectionPayload(primaryData, null, null); - - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, null); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only.json"); - } - - [TestMethod] - public async Task Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata() - { - var primaryData1 = new Mock(MockBehavior.Strict); - var primaryData2 = new Mock(MockBehavior.Strict); - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 1"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 2"); - return Task.FromResult(0); - }); - - var mockMetadata = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(mockMetadata.Object, It.IsAny())) - .Returns((IMetadata resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder metadata object"); - return Task.FromResult(0); - }); - - var primaryData = new[] { primaryData1.Object, primaryData2.Object }; - IResourceCollectionPayload payload = new ResourceCollectionPayload(primaryData, null, mockMetadata.Object); - - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_primary_data_only_and_metadata.json"); - } - - [TestMethod] - public async Task Serialize_ResourceCollectionPayload_for_all_possible_members() - { - var primaryData1 = new Mock(MockBehavior.Strict); - var primaryData2 = new Mock(MockBehavior.Strict); - var relatedResource1 = new Mock(MockBehavior.Strict); - var relatedResource2 = new Mock(MockBehavior.Strict); - var relatedResource3 = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData1.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 1"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(primaryData2.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data 2"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource1.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 1"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource2.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 2"); - return Task.FromResult(0); - }); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource3.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 3"); - return Task.FromResult(0); - }); - - var mockMetadata = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(mockMetadata.Object, It.IsAny())) - .Returns((IMetadata resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder metadata object"); - return Task.FromResult(0); - }); - - var primaryData = new[] { primaryData1.Object, primaryData2.Object }; - var relatedResources = new[] { relatedResource1.Object, relatedResource2.Object, relatedResource3.Object }; - IResourceCollectionPayload payload = new ResourceCollectionPayload(primaryData, relatedResources, mockMetadata.Object); - - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/ResourceCollectionPayloadSerializer/Serialize_ResourceCollectionPayload_for_all_possible_members.json"); - } - - [TestMethod] - public void Deserialize_empty_payload() - { - // Arrange - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - - // Act - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_empty_payload.json").Result; - - // Assert - payload.PrimaryData.Should().BeEquivalentTo(); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeNull(); - } - - [TestMethod] - public void Deserialize_payload_with_primary_data() - { - // Arrange - var mockResource1 = new Mock(MockBehavior.Strict); - var mockResource2 = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data/0")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("PD1"); - return Task.FromResult(mockResource1.Object); - }); - mockResourceObjectSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data/1")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("PD2"); - return Task.FromResult(mockResource2.Object); - }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - - // Act - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data.json").Result; - - // Assert - payload.PrimaryData.Should().BeEquivalentTo(mockResource1.Object, mockResource2.Object); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeNull(); - } - - [TestMethod] - public void Deserialize_payload_with_primary_data_and_unknown_top_level_key() - { - // Arrange - var mockResource1 = new Mock(MockBehavior.Strict); - var mockResource2 = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data/0")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("PD1"); - return Task.FromResult(mockResource1.Object); - }); - mockResourceObjectSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data/1")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("PD2"); - return Task.FromResult(mockResource2.Object); - }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - - // Act - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_primary_data_and_unknown_top_level_key.json").Result; - - // Assert - payload.PrimaryData.Should().BeEquivalentTo(mockResource1.Object, mockResource2.Object); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeNull(); - } - - [TestMethod] - public void Deserialize_payload_with_metadata() - { - // Arrange - var mockMetadata = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(s => s.Deserialize(It.IsAny(), "/meta")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("metadata goes here"); - return Task.FromResult(mockMetadata.Object); - }); - - // Act - var serializer = new ResourceCollectionPayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceCollectionPayloadSerializer/Deserialize_payload_with_metadata.json").Result; - - // Assert - payload.PrimaryData.Should().BeEquivalentTo(); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeSameAs(mockMetadata.Object); - } - } -} diff --git a/JSONAPI.Tests/Json/ResourceLinkageSerializerTests.cs b/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs similarity index 61% rename from JSONAPI.Tests/Json/ResourceLinkageSerializerTests.cs rename to JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs index e89c1dcc..22b5cfcf 100644 --- a/JSONAPI.Tests/Json/ResourceLinkageSerializerTests.cs +++ b/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs @@ -2,18 +2,16 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Tests.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Json { [TestClass] - public class ResourceLinkageSerializerTests : JsonApiSerializerTestsBase + public class ResourceLinkageFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public async Task Serialize_linkage() @@ -21,8 +19,8 @@ public async Task Serialize_linkage() var linkageObject = new Mock(MockBehavior.Strict); linkageObject.Setup(l => l.LinkageToken).Returns("linkage goes here"); - var serializer = new ResourceLinkageSerializer(); - await AssertSerializeOutput(serializer, linkageObject.Object, "Json/Fixtures/ResourceLinkageSerializer/Serialize_linkage.json"); + var formatter = new ResourceLinkageFormatter(); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_linkage.json"); } [TestMethod] @@ -31,8 +29,8 @@ public async Task Serialize_null_linkage() var linkageObject = new Mock(MockBehavior.Strict); linkageObject.Setup(l => l.LinkageToken).Returns((JToken)null); - var serializer = new ResourceLinkageSerializer(); - await AssertSerializeOutput(serializer, linkageObject.Object, "Json/Fixtures/ResourceLinkageSerializer/Serialize_null_linkage.json"); + var formatter = new ResourceLinkageFormatter(); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json"); } [TestMethod] @@ -41,10 +39,10 @@ public void Deserialize_to_one_linkage() // Arrange // Act - var serializer = new ResourceLinkageSerializer(); + var formatter = new ResourceLinkageFormatter(); var linkage = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_one_linkage.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_one_linkage.json").Result; // Assert var linkageToken = (JObject)linkage.LinkageToken; @@ -59,10 +57,10 @@ public void Deserialize_null_to_one_linkage() // Arrange // Act - var serializer = new ResourceLinkageSerializer(); + var formatter = new ResourceLinkageFormatter(); var linkage = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceLinkageSerializer/Deserialize_null_to_one_linkage.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceLinkageFormatter/Deserialize_null_to_one_linkage.json").Result; // Assert linkage.LinkageToken.Should().BeNull(); @@ -74,10 +72,10 @@ public void Deserialize_to_many_linkage() // Arrange // Act - var serializer = new ResourceLinkageSerializer(); + var formatter = new ResourceLinkageFormatter(); var linkage = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceLinkageSerializer/Deserialize_to_many_linkage.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_many_linkage.json").Result; // Assert var linkageToken = (JArray)linkage.LinkageToken; @@ -99,10 +97,10 @@ public void Deserialize_fails_on_string() // Arrange // Act - var serializer = new ResourceLinkageSerializer(); + var formatter = new ResourceLinkageFormatter(); - Func action = () => GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_string.json"); + Func action = () => GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_string.json"); // Assert action.ShouldThrow().WithMessage("Expected an array, object, or null for linkage, but got String"); @@ -114,10 +112,10 @@ public void Deserialize_fails_on_integer() // Arrange // Act - var serializer = new ResourceLinkageSerializer(); + var formatter = new ResourceLinkageFormatter(); - Func action = () => GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceLinkageSerializer/Deserialize_fails_on_integer.json"); + Func action = () => GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceLinkageFormatter/Deserialize_fails_on_integer.json"); // Assert action.ShouldThrow().WithMessage("Expected an array, object, or null for linkage, but got Integer"); diff --git a/JSONAPI.Tests/Json/ResourceObjectSerializerTests.cs b/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs similarity index 61% rename from JSONAPI.Tests/Json/ResourceObjectSerializerTests.cs rename to JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs index 3e376a15..f11e32d5 100644 --- a/JSONAPI.Tests/Json/ResourceObjectSerializerTests.cs +++ b/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs @@ -3,8 +3,8 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using JSONAPI.Documents; using JSONAPI.Json; -using JSONAPI.Payload; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -13,15 +13,15 @@ namespace JSONAPI.Tests.Json { [TestClass] - public class ResourceObjectSerializerTests : JsonApiSerializerTestsBase + public class ResourceObjectFormatterTests : JsonApiFormatterTestsBase { [TestMethod] public async Task Serialize_ResourceObject_for_resource_without_attributes() { IResourceObject resourceObject = new ResourceObject("countries", "1100"); - var serializer = new ResourceObjectSerializer(null, null, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_without_attributes.json"); + var formatter = new ResourceObjectFormatter(null, null, null); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_without_attributes.json"); } [TestMethod] @@ -35,8 +35,8 @@ public async Task Serialize_ResourceObject_for_resource_with_attributes() }; IResourceObject resourceObject = new ResourceObject("shapes", "1400", attributes); - var serializer = new ResourceObjectSerializer(null, null, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_attributes.json"); + var formatter = new ResourceObjectFormatter(null, null, null); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_attributes.json"); } [TestMethod] @@ -45,14 +45,14 @@ public async Task Serialize_ResourceObject_for_resource_with_relationships() var mockCapital = new Mock(MockBehavior.Strict); var mockNeighbors = new Mock(MockBehavior.Strict); - var mockRelationshipObjectSerializer = new Mock(MockBehavior.Strict); - mockRelationshipObjectSerializer.Setup(m => m.Serialize(mockCapital.Object, It.IsAny())) + var mockRelationshipObjectFormatter = new Mock(MockBehavior.Strict); + mockRelationshipObjectFormatter.Setup(m => m.Serialize(mockCapital.Object, It.IsAny())) .Returns((IRelationshipObject relationshipObject, JsonWriter writer) => { writer.WriteValue("IRelationship Placeholder - capital"); return Task.FromResult(0); }).Verifiable(); - mockRelationshipObjectSerializer.Setup(m => m.Serialize(mockNeighbors.Object, It.IsAny())) + mockRelationshipObjectFormatter.Setup(m => m.Serialize(mockNeighbors.Object, It.IsAny())) .Returns((IRelationshipObject relationshipObject, JsonWriter writer) => { writer.WriteValue("IRelationship Placeholder - neighbors"); @@ -66,10 +66,10 @@ public async Task Serialize_ResourceObject_for_resource_with_relationships() }; IResourceObject resourceObject = new ResourceObject("states", "1400", relationships: relationships); - var serializer = new ResourceObjectSerializer(mockRelationshipObjectSerializer.Object, null, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_relationships.json"); - mockRelationshipObjectSerializer.Verify(s => s.Serialize(mockCapital.Object, It.IsAny()), Times.Once); - mockRelationshipObjectSerializer.Verify(s => s.Serialize(mockNeighbors.Object, It.IsAny()), Times.Once); + var formatter = new ResourceObjectFormatter(mockRelationshipObjectFormatter.Object, null, null); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_relationships.json"); + mockRelationshipObjectFormatter.Verify(s => s.Serialize(mockCapital.Object, It.IsAny()), Times.Once); + mockRelationshipObjectFormatter.Verify(s => s.Serialize(mockNeighbors.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -81,15 +81,15 @@ public async Task Serialize_ResourceObject_for_resource_with_only_null_relations }; IResourceObject resourceObject = new ResourceObject("states", "1400", relationships: relationships); - var serializer = new ResourceObjectSerializer(null, null, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_only_null_relationships.json"); + var formatter = new ResourceObjectFormatter(null, null, null); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_only_null_relationships.json"); } [TestMethod] public async Task Serialize_ResourceObject_for_resource_with_links() { - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) .Returns((ILink link, JsonWriter writer) => { writer.WriteValue("ILink placeholder 1"); @@ -100,16 +100,16 @@ public async Task Serialize_ResourceObject_for_resource_with_links() IResourceObject resourceObject = new ResourceObject("states", "1400", selfLink: mockSelfLink.Object); - var serializer = new ResourceObjectSerializer(null, mockLinkSerializer.Object, null); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_links.json"); - mockLinkSerializer.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); + var formatter = new ResourceObjectFormatter(null, mockLinkFormatter.Object, null); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_links.json"); + mockLinkFormatter.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); } [TestMethod] public async Task Serialize_ResourceObject_for_resource_with_metadata() { - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { writer.WriteValue("IMetadata placeholder 1"); @@ -119,9 +119,9 @@ public async Task Serialize_ResourceObject_for_resource_with_metadata() var mockMetadata = new Mock(MockBehavior.Strict); IResourceObject resourceObject = new ResourceObject("states", "1400", metadata: mockMetadata.Object); - var serializer = new ResourceObjectSerializer(null, null, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_metadata.json"); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + var formatter = new ResourceObjectFormatter(null, null, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_metadata.json"); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); } [TestMethod] @@ -130,30 +130,30 @@ public async Task Serialize_ResourceObject_for_resource_with_all_possible_member var mockCapital = new Mock(MockBehavior.Strict); var mockNeighbors = new Mock(MockBehavior.Strict); - var mockRelationshipObjectSerializer = new Mock(MockBehavior.Strict); - mockRelationshipObjectSerializer.Setup(m => m.Serialize(mockCapital.Object, It.IsAny())) + var mockRelationshipObjectFormatter = new Mock(MockBehavior.Strict); + mockRelationshipObjectFormatter.Setup(m => m.Serialize(mockCapital.Object, It.IsAny())) .Returns((IRelationshipObject relationshipObject, JsonWriter writer) => { writer.WriteValue("IRelationship Placeholder - capital"); return Task.FromResult(0); }).Verifiable(); - mockRelationshipObjectSerializer.Setup(m => m.Serialize(mockNeighbors.Object, It.IsAny())) + mockRelationshipObjectFormatter.Setup(m => m.Serialize(mockNeighbors.Object, It.IsAny())) .Returns((IRelationshipObject relationshipObject, JsonWriter writer) => { writer.WriteValue("IRelationship Placeholder - neighbors"); return Task.FromResult(0); }).Verifiable(); - var mockLinkSerializer = new Mock(MockBehavior.Strict); - mockLinkSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + var mockLinkFormatter = new Mock(MockBehavior.Strict); + mockLinkFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) .Returns((ILink link, JsonWriter writer) => { writer.WriteValue("ILink placeholder 1"); return Task.FromResult(0); }).Verifiable(); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) .Returns((IMetadata metadata, JsonWriter writer) => { writer.WriteValue("IMetadata placeholder 1"); @@ -179,12 +179,12 @@ public async Task Serialize_ResourceObject_for_resource_with_all_possible_member IResourceObject resourceObject = new ResourceObject("states", "1400", attributes, relationships, mockSelfLink.Object, mockMetadata.Object); - var serializer = new ResourceObjectSerializer(mockRelationshipObjectSerializer.Object, mockLinkSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, resourceObject, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_all_possible_members.json"); - mockRelationshipObjectSerializer.Verify(s => s.Serialize(mockCapital.Object, It.IsAny()), Times.Once); - mockRelationshipObjectSerializer.Verify(s => s.Serialize(mockNeighbors.Object, It.IsAny()), Times.Once); - mockLinkSerializer.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + var formatter = new ResourceObjectFormatter(mockRelationshipObjectFormatter.Object, mockLinkFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, resourceObject, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_all_possible_members.json"); + mockRelationshipObjectFormatter.Verify(s => s.Serialize(mockCapital.Object, It.IsAny()), Times.Once); + mockRelationshipObjectFormatter.Verify(s => s.Serialize(mockNeighbors.Object, It.IsAny()), Times.Once); + mockLinkFormatter.Verify(s => s.Serialize(mockSelfLink.Object, It.IsAny()), Times.Once); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); } class Sample @@ -211,8 +211,8 @@ public async Task Serialize_ResourceObject_for_resource_with_unsigned_long_integ resourceObject.Setup(o => o.Attributes).Returns(attributes); resourceObject.Setup(o => o.Relationships).Returns(new Dictionary()); - var serializer = new ResourceObjectSerializer(null, null, null); - await AssertSerializeOutput(serializer, resourceObject.Object, "Json/Fixtures/ResourceObjectSerializer/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json"); + var formatter = new ResourceObjectFormatter(null, null, null); + await AssertSerializeOutput(formatter, resourceObject.Object, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_resource_with_unsigned_long_integer_greater_than_int64_maxvalue.json"); } [TestMethod] @@ -223,15 +223,15 @@ public void Deserialize_resource_object() var mockCommentsRelationship = new Mock(MockBehavior.Strict); var mockMetadata = new Mock(MockBehavior.Strict); - var mockRelationshipSerializer = new Mock(MockBehavior.Strict); - mockRelationshipSerializer.Setup(s => s.Deserialize(It.IsAny(), "/relationships/author")) + var mockRelationshipFormatter = new Mock(MockBehavior.Strict); + mockRelationshipFormatter.Setup(s => s.Deserialize(It.IsAny(), "/relationships/author")) .Returns((JsonReader reader, string currentPath) => { reader.TokenType.Should().Be(JsonToken.String); reader.Value.Should().Be("AUTHOR_RELATIONSHIP"); return Task.FromResult(mockAuthorRelationship.Object); }); - mockRelationshipSerializer.Setup(s => s.Deserialize(It.IsAny(), "/relationships/comments")) + mockRelationshipFormatter.Setup(s => s.Deserialize(It.IsAny(), "/relationships/comments")) .Returns((JsonReader reader, string currentPath) => { reader.TokenType.Should().Be(JsonToken.String); @@ -239,8 +239,8 @@ public void Deserialize_resource_object() return Task.FromResult(mockCommentsRelationship.Object); }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(s => s.Deserialize(It.IsAny(), "/meta")) + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(s => s.Deserialize(It.IsAny(), "/meta")) .Returns((JsonReader reader, string currentPath) => { reader.TokenType.Should().Be(JsonToken.String); @@ -249,10 +249,10 @@ public void Deserialize_resource_object() }); // Act - var serializer = new ResourceObjectSerializer(mockRelationshipSerializer.Object, null, mockMetadataSerializer.Object); + var formatter = new ResourceObjectFormatter(mockRelationshipFormatter.Object, null, mockMetadataFormatter.Object); var resourceObject = - GetDeserializedOutput(serializer, - "Json/Fixtures/ResourceObjectSerializer/Deserialize_resource_object.json").Result; + GetDeserializedOutput(formatter, + "Json/Fixtures/ResourceObjectFormatter/Deserialize_resource_object.json").Result; // Assert resourceObject.Type.Should().Be("posts"); diff --git a/JSONAPI.Tests/Json/SingleResourceDocumentFormatterTests.cs b/JSONAPI.Tests/Json/SingleResourceDocumentFormatterTests.cs new file mode 100644 index 00000000..69ac40e2 --- /dev/null +++ b/JSONAPI.Tests/Json/SingleResourceDocumentFormatterTests.cs @@ -0,0 +1,164 @@ +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Documents; +using JSONAPI.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; + +namespace JSONAPI.Tests.Json +{ + [TestClass] + public class SingleResourceDocumentFormatterTests : JsonApiFormatterTestsBase + { + [TestMethod] + public async Task Serialize_SingleResourceDocument_for_primary_data_only() + { + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder resource object"); + return Task.FromResult(0); + }).Verifiable(); + + var mockResource = new Mock(MockBehavior.Strict); + ISingleResourceDocument document = new SingleResourceDocument(mockResource.Object, null, null); + + var formatter = new SingleResourceDocumentFormatter(mockResourceObjectFormatter.Object, null); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_only.json"); + mockResourceObjectFormatter.Verify(s => s.Serialize(mockResource.Object, It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Serialize_SingleResourceDocument_for_primary_data_and_metadata() + { + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder resource object"); + return Task.FromResult(0); + }).Verifiable(); + + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + .Returns((IMetadata resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder metadata object"); + return Task.FromResult(0); + }).Verifiable(); + + var mockResource = new Mock(MockBehavior.Strict); + var mockMetadata = new Mock(MockBehavior.Strict); + ISingleResourceDocument document = new SingleResourceDocument(mockResource.Object, null, mockMetadata.Object); + + var formatter = new SingleResourceDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_primary_data_and_metadata.json"); + mockResourceObjectFormatter.Verify(s => s.Serialize(mockResource.Object, It.IsAny()), Times.Once); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task Serialize_SingleResourceDocument_for_all_possible_members() + { + var mockPrimaryData = new Mock(MockBehavior.Strict); + var relatedResource1 = new Mock(MockBehavior.Strict); + var relatedResource2 = new Mock(MockBehavior.Strict); + var relatedResource3 = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(m => m.Serialize(mockPrimaryData.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Primary data object"); + return Task.FromResult(0); + }).Verifiable(); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource1.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 1"); + return Task.FromResult(0); + }).Verifiable(); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource2.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 2"); + return Task.FromResult(0); + }).Verifiable(); + mockResourceObjectFormatter.Setup(m => m.Serialize(relatedResource3.Object, It.IsAny())) + .Returns((IResourceObject resourceObject, JsonWriter writer) => + { + writer.WriteValue("Related data object 3"); + return Task.FromResult(0); + }).Verifiable(); + + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + mockMetadataFormatter.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) + .Returns((IMetadata resourceObject, JsonWriter writer) => + { + writer.WriteValue("Placeholder metadata object"); + return Task.FromResult(0); + }).Verifiable(); + + var mockMetadata = new Mock(MockBehavior.Strict); + var relatedResources = new[] { relatedResource1.Object, relatedResource2.Object, relatedResource3.Object }; + ISingleResourceDocument document = new SingleResourceDocument(mockPrimaryData.Object, relatedResources, mockMetadata.Object); + + var formatter = new SingleResourceDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + await AssertSerializeOutput(formatter, document, "Json/Fixtures/SingleResourceDocumentFormatter/Serialize_SingleResourceDocument_for_all_possible_members.json"); + mockResourceObjectFormatter.Verify(s => s.Serialize(mockPrimaryData.Object, It.IsAny()), Times.Once); + mockResourceObjectFormatter.Verify(s => s.Serialize(relatedResource1.Object, It.IsAny()), Times.Once); + mockResourceObjectFormatter.Verify(s => s.Serialize(relatedResource2.Object, It.IsAny()), Times.Once); + mockResourceObjectFormatter.Verify(s => s.Serialize(relatedResource3.Object, It.IsAny()), Times.Once); + mockMetadataFormatter.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); + } + + [TestMethod] + public void Deserialize_null_document() + { + // Arrange + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + + // Act + var formatter = new SingleResourceDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_null_document.json").Result; + + // Assert + document.PrimaryData.Should().BeNull(); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeNull(); + } + + [TestMethod] + public void Deserialize_document_with_resource() + { + // Arrange + var mockResourceObject = new Mock(MockBehavior.Strict); + + var mockResourceObjectFormatter = new Mock(MockBehavior.Strict); + mockResourceObjectFormatter.Setup(s => s.Deserialize(It.IsAny(), "/data")) + .Returns((JsonReader reader, string currentPath) => + { + reader.TokenType.Should().Be(JsonToken.String); + reader.Value.Should().Be("primary data goes here"); + return Task.FromResult(mockResourceObject.Object); + }); + var mockMetadataFormatter = new Mock(MockBehavior.Strict); + + // Act + var formatter = new SingleResourceDocumentFormatter(mockResourceObjectFormatter.Object, mockMetadataFormatter.Object); + var document = + GetDeserializedOutput(formatter, + "Json/Fixtures/SingleResourceDocumentFormatter/Deserialize_document_with_resource.json").Result; + + // Assert + document.PrimaryData.Should().BeSameAs(mockResourceObject.Object); + document.RelatedData.Should().BeEquivalentTo(); + document.Metadata.Should().BeNull(); + } + } +} diff --git a/JSONAPI.Tests/Json/SingleResourcePayloadSerializerTests.cs b/JSONAPI.Tests/Json/SingleResourcePayloadSerializerTests.cs deleted file mode 100644 index 0030eea4..00000000 --- a/JSONAPI.Tests/Json/SingleResourcePayloadSerializerTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System.Threading.Tasks; -using FluentAssertions; -using JSONAPI.Json; -using JSONAPI.Payload; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Newtonsoft.Json; - -namespace JSONAPI.Tests.Json -{ - [TestClass] - public class SingleResourcePayloadSerializerTests : JsonApiSerializerTestsBase - { - [TestMethod] - public async Task Serialize_SingleResourcePayload_for_primary_data_only() - { - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder resource object"); - return Task.FromResult(0); - }).Verifiable(); - - var mockResource = new Mock(MockBehavior.Strict); - ISingleResourcePayload payload = new SingleResourcePayload(mockResource.Object, null, null); - - var serializer = new SingleResourcePayloadSerializer(mockResourceObjectSerializer.Object, null); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_only.json"); - mockResourceObjectSerializer.Verify(s => s.Serialize(mockResource.Object, It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task Serialize_SingleResourcePayload_for_primary_data_and_metadata() - { - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder resource object"); - return Task.FromResult(0); - }).Verifiable(); - - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) - .Returns((IMetadata resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder metadata object"); - return Task.FromResult(0); - }).Verifiable(); - - var mockResource = new Mock(MockBehavior.Strict); - var mockMetadata = new Mock(MockBehavior.Strict); - ISingleResourcePayload payload = new SingleResourcePayload(mockResource.Object, null, mockMetadata.Object); - - var serializer = new SingleResourcePayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_primary_data_and_metadata.json"); - mockResourceObjectSerializer.Verify(s => s.Serialize(mockResource.Object, It.IsAny()), Times.Once); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); - } - - [TestMethod] - public async Task Serialize_SingleResourcePayload_for_all_possible_members() - { - var mockPrimaryData = new Mock(MockBehavior.Strict); - var relatedResource1 = new Mock(MockBehavior.Strict); - var relatedResource2 = new Mock(MockBehavior.Strict); - var relatedResource3 = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(m => m.Serialize(mockPrimaryData.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Primary data object"); - return Task.FromResult(0); - }).Verifiable(); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource1.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 1"); - return Task.FromResult(0); - }).Verifiable(); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource2.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 2"); - return Task.FromResult(0); - }).Verifiable(); - mockResourceObjectSerializer.Setup(m => m.Serialize(relatedResource3.Object, It.IsAny())) - .Returns((IResourceObject resourceObject, JsonWriter writer) => - { - writer.WriteValue("Related data object 3"); - return Task.FromResult(0); - }).Verifiable(); - - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - mockMetadataSerializer.Setup(m => m.Serialize(It.IsAny(), It.IsAny())) - .Returns((IMetadata resourceObject, JsonWriter writer) => - { - writer.WriteValue("Placeholder metadata object"); - return Task.FromResult(0); - }).Verifiable(); - - var mockMetadata = new Mock(MockBehavior.Strict); - var relatedResources = new[] { relatedResource1.Object, relatedResource2.Object, relatedResource3.Object }; - ISingleResourcePayload payload = new SingleResourcePayload(mockPrimaryData.Object, relatedResources, mockMetadata.Object); - - var serializer = new SingleResourcePayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - await AssertSerializeOutput(serializer, payload, "Json/Fixtures/SingleResourcePayloadSerializer/Serialize_SingleResourcePayload_for_all_possible_members.json"); - mockResourceObjectSerializer.Verify(s => s.Serialize(mockPrimaryData.Object, It.IsAny()), Times.Once); - mockResourceObjectSerializer.Verify(s => s.Serialize(relatedResource1.Object, It.IsAny()), Times.Once); - mockResourceObjectSerializer.Verify(s => s.Serialize(relatedResource2.Object, It.IsAny()), Times.Once); - mockResourceObjectSerializer.Verify(s => s.Serialize(relatedResource3.Object, It.IsAny()), Times.Once); - mockMetadataSerializer.Verify(s => s.Serialize(mockMetadata.Object, It.IsAny()), Times.Once); - } - - [TestMethod] - public void Deserialize_null_payload() - { - // Arrange - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - - // Act - var serializer = new SingleResourcePayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_null_payload.json").Result; - - // Assert - payload.PrimaryData.Should().BeNull(); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeNull(); - } - - [TestMethod] - public void Deserialize_payload_with_resource() - { - // Arrange - var mockResourceObject = new Mock(MockBehavior.Strict); - - var mockResourceObjectSerializer = new Mock(MockBehavior.Strict); - mockResourceObjectSerializer.Setup(s => s.Deserialize(It.IsAny(), "/data")) - .Returns((JsonReader reader, string currentPath) => - { - reader.TokenType.Should().Be(JsonToken.String); - reader.Value.Should().Be("primary data goes here"); - return Task.FromResult(mockResourceObject.Object); - }); - var mockMetadataSerializer = new Mock(MockBehavior.Strict); - - // Act - var serializer = new SingleResourcePayloadSerializer(mockResourceObjectSerializer.Object, mockMetadataSerializer.Object); - var payload = - GetDeserializedOutput(serializer, - "Json/Fixtures/SingleResourcePayloadSerializer/Deserialize_payload_with_resource.json").Result; - - // Assert - payload.PrimaryData.Should().BeSameAs(mockResourceObject.Object); - payload.RelatedData.Should().BeEquivalentTo(); - payload.Metadata.Should().BeNull(); - } - } -} diff --git a/JSONAPI.Tests/Payload/Builders/FallbackPayloadBuilderTests.cs b/JSONAPI.Tests/Payload/Builders/FallbackPayloadBuilderTests.cs deleted file mode 100644 index 40f9afb3..00000000 --- a/JSONAPI.Tests/Payload/Builders/FallbackPayloadBuilderTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Controllers; -using FluentAssertions; -using JSONAPI.Core; -using JSONAPI.Http; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; - -namespace JSONAPI.Tests.Payload.Builders -{ - [TestClass] - public class FallbackPayloadBuilderTests - { - private const string GuidRegex = @"\b[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\b"; - - class Fruit - { - public string Id { get; set; } - - public string Name { get; set; } - } - - [TestMethod] - public async Task Creates_single_resource_payload_for_registered_non_collection_types() - { - // Arrange - var objectContent = new Fruit { Id = "984", Name = "Kiwi" }; - - var mockPayload = new Mock(MockBehavior.Strict); - - var singleResourcePayloadBuilder = new Mock(MockBehavior.Strict); - singleResourcePayloadBuilder.Setup(b => b.BuildPayload(objectContent, It.IsAny(), null)).Returns(mockPayload.Object); - - var mockQueryablePayloadBuilder = new Mock(MockBehavior.Strict); - var mockResourceCollectionPayloadBuilder = new Mock(MockBehavior.Strict); - - var cancellationTokenSource = new CancellationTokenSource(); - - var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); - var mockBaseUrlService = new Mock(MockBehavior.Strict); - mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com"); - - // Act - var fallbackPayloadBuilder = new FallbackPayloadBuilder(singleResourcePayloadBuilder.Object, - mockQueryablePayloadBuilder.Object, mockResourceCollectionPayloadBuilder.Object, mockBaseUrlService.Object); - var resultPayload = await fallbackPayloadBuilder.BuildPayload(objectContent, request, cancellationTokenSource.Token); - - // Assert - resultPayload.Should().BeSameAs(mockPayload.Object); - } - - [TestMethod] - public async Task Creates_resource_collection_payload_for_queryables() - { - // Arrange - var items = new[] - { - new Fruit {Id = "43", Name = "Strawberry"}, - new Fruit {Id = "43", Name = "Grape"} - }.AsQueryable(); - - var mockPayload = new Mock(MockBehavior.Strict); - - var singleResourcePayloadBuilder = new Mock(MockBehavior.Strict); - - var request = new HttpRequestMessage(); - - var mockBaseUrlService = new Mock(MockBehavior.Strict); - mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); - - var cancellationTokenSource = new CancellationTokenSource(); - - var mockQueryablePayloadBuilder = new Mock(MockBehavior.Strict); - mockQueryablePayloadBuilder - .Setup(b => b.BuildPayload(items, request, cancellationTokenSource.Token)) - .Returns(() => Task.FromResult(mockPayload.Object)); - - var mockResourceCollectionPayloadBuilder = new Mock(MockBehavior.Strict); - - // Act - var fallbackPayloadBuilder = new FallbackPayloadBuilder(singleResourcePayloadBuilder.Object, - mockQueryablePayloadBuilder.Object, mockResourceCollectionPayloadBuilder.Object, mockBaseUrlService.Object); - var resultPayload = await fallbackPayloadBuilder.BuildPayload(items, request, cancellationTokenSource.Token); - - // Assert - resultPayload.Should().BeSameAs(mockPayload.Object); - } - - [TestMethod] - public async Task Creates_resource_collection_payload_for_non_queryable_enumerables() - { - // Arrange - var items = new[] - { - new Fruit {Id = "43", Name = "Strawberry"}, - new Fruit {Id = "43", Name = "Grape"} - }; - - var mockPayload = new Mock(MockBehavior.Strict); - - var singleResourcePayloadBuilder = new Mock(MockBehavior.Strict); - - var cancellationTokenSource = new CancellationTokenSource(); - - var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); - - var mockBaseUrlService = new Mock(MockBehavior.Strict); - mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); - - var mockQueryablePayloadBuilder = new Mock(MockBehavior.Strict); - var mockResourceCollectionPayloadBuilder = new Mock(MockBehavior.Strict); - mockResourceCollectionPayloadBuilder - .Setup(b => b.BuildPayload(items, "https://www.example.com/", It.IsAny(), It.IsAny())) - .Returns(() => (mockPayload.Object)); - - // Act - var fallbackPayloadBuilder = new FallbackPayloadBuilder(singleResourcePayloadBuilder.Object, - mockQueryablePayloadBuilder.Object, mockResourceCollectionPayloadBuilder.Object, mockBaseUrlService.Object); - var resultPayload = await fallbackPayloadBuilder.BuildPayload(items, request, cancellationTokenSource.Token); - - // Assert - resultPayload.Should().BeSameAs(mockPayload.Object); - } - } -} diff --git a/JSONAPI.TodoMVC.API/Controllers/TodosController.cs b/JSONAPI.TodoMVC.API/Controllers/TodosController.cs index 3feae1e4..b347d51c 100644 --- a/JSONAPI.TodoMVC.API/Controllers/TodosController.cs +++ b/JSONAPI.TodoMVC.API/Controllers/TodosController.cs @@ -5,7 +5,7 @@ namespace JSONAPI.TodoMVC.API.Controllers { public class TodosController : JsonApiController { - public TodosController(IPayloadMaterializer payloadMaterializer) : base(payloadMaterializer) + public TodosController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) { } } diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index d856048e..dec3d4fd 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -33,7 +33,7 @@ private static HttpConfiguration GetWebApiConfiguration() var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule(module); containerBuilder.RegisterModule(efModule); - containerBuilder.RegisterGeneric(typeof(EntityFrameworkPayloadMaterializer<>)) + containerBuilder.RegisterGeneric(typeof(EntityFrameworkDocumentMaterializer<>)) .AsImplementedInterfaces(); var container = containerBuilder.Build(); httpConfig.UseJsonApiWithAutofac(container); diff --git a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index 7beb067e..0624da63 100644 --- a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -10,7 +10,7 @@ namespace JSONAPI.ActionFilters { /// - /// This transformer filters an IQueryable payload based on query-string values. + /// This transformer filters an IQueryable based on query-string values. /// public class DefaultFilteringTransformer : IQueryableFilteringTransformer { @@ -19,7 +19,7 @@ public class DefaultFilteringTransformer : IQueryableFilteringTransformer /// /// Creates a new FilteringQueryableTransformer /// - /// The model manager used to look up registered type information. + /// The registry used to look up registered type information. public DefaultFilteringTransformer(IResourceTypeRegistry resourceTypeRegistry) { _resourceTypeRegistry = resourceTypeRegistry; diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs index c534bda2..de610f75 100644 --- a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Net.Http; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents.Builders; namespace JSONAPI.ActionFilters { diff --git a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs index 9aeca49c..a31be9bf 100644 --- a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs +++ b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs @@ -5,12 +5,12 @@ using System.Net.Http; using System.Reflection; using JSONAPI.Core; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents.Builders; namespace JSONAPI.ActionFilters { /// - /// This transform sorts an IQueryable payload according to query parameters. + /// This transform sorts an IQueryable according to query parameters. /// public class DefaultSortingTransformer : IQueryableSortingTransformer { @@ -19,7 +19,7 @@ public class DefaultSortingTransformer : IQueryableSortingTransformer /// /// Creates a new SortingQueryableTransformer /// - /// The model manager used to look up registered type information. + /// The registry used to look up registered type information. public DefaultSortingTransformer(IResourceTypeRegistry resourceTypeRegistry) { _resourceTypeRegistry = resourceTypeRegistry; diff --git a/JSONAPI/ActionFilters/FallbackPayloadBuilderAttribute.cs b/JSONAPI/ActionFilters/FallbackDocumentBuilderAttribute.cs similarity index 50% rename from JSONAPI/ActionFilters/FallbackPayloadBuilderAttribute.cs rename to JSONAPI/ActionFilters/FallbackDocumentBuilderAttribute.cs index 2b8e78bd..405cf3c1 100644 --- a/JSONAPI/ActionFilters/FallbackPayloadBuilderAttribute.cs +++ b/JSONAPI/ActionFilters/FallbackDocumentBuilderAttribute.cs @@ -1,35 +1,32 @@ -using System; -using System.Linq; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http; -using System.Web.Http.Controllers; using System.Web.Http.Filters; -using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; namespace JSONAPI.ActionFilters { /// - /// Converts ObjectContent to payload form if it isn't already + /// Converts ObjectContent to JSON API document form if it isn't already /// - public class FallbackPayloadBuilderAttribute : ActionFilterAttribute + public class FallbackDocumentBuilderAttribute : ActionFilterAttribute { - private readonly IFallbackPayloadBuilder _fallbackPayloadBuilder; - private readonly IErrorPayloadBuilder _errorPayloadBuilder; + private readonly IFallbackDocumentBuilder _fallbackDocumentBuilder; + private readonly IErrorDocumentBuilder _errorDocumentBuilder; /// - /// Creates a FallbackPayloadBuilderAttribute + /// Creates a FallbackDocumentBuilderAttribute /// - /// - /// - public FallbackPayloadBuilderAttribute(IFallbackPayloadBuilder fallbackPayloadBuilder, IErrorPayloadBuilder errorPayloadBuilder) + /// + /// + public FallbackDocumentBuilderAttribute(IFallbackDocumentBuilder fallbackDocumentBuilder, IErrorDocumentBuilder errorDocumentBuilder) { - _fallbackPayloadBuilder = fallbackPayloadBuilder; - _errorPayloadBuilder = errorPayloadBuilder; + _fallbackDocumentBuilder = fallbackDocumentBuilder; + _errorDocumentBuilder = errorDocumentBuilder; } public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, @@ -52,38 +49,38 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio return; } - // These payload types should be passed through; they are already ready to be serialized. - if (objectContent.Value is ISingleResourcePayload || - objectContent.Value is IResourceCollectionPayload) + // These document types should be passed through; they are already ready to be serialized. + if (objectContent.Value is ISingleResourceDocument || + objectContent.Value is IResourceCollectionDocument) return; - var errorPayload = objectContent.Value as IErrorPayload; - if (errorPayload != null) + var errorDocument = objectContent.Value as IErrorDocument; + if (errorDocument != null) { actionExecutedContext.Response.StatusCode = - errorPayload.Errors.First().Status; + errorDocument.Errors.First().Status; return; } - object payloadValue; + object documentValue; var httpError = objectContent.Value as HttpError; if (httpError != null) { - payloadValue = _errorPayloadBuilder.BuildFromHttpError(httpError, actionExecutedContext.Response.StatusCode); + documentValue = _errorDocumentBuilder.BuildFromHttpError(httpError, actionExecutedContext.Response.StatusCode); } else { - payloadValue = - await _fallbackPayloadBuilder.BuildPayload(objectContent.Value, actionExecutedContext.Request, cancellationToken); + documentValue = + await _fallbackDocumentBuilder.BuildDocument(objectContent.Value, actionExecutedContext.Request, cancellationToken); } - errorPayload = payloadValue as IErrorPayload; - if (payloadValue is IErrorPayload) + errorDocument = documentValue as IErrorDocument; + if (documentValue is IErrorDocument) { - actionExecutedContext.Response.StatusCode = errorPayload.Errors.First().Status; + actionExecutedContext.Response.StatusCode = errorDocument.Errors.First().Status; } - actionExecutedContext.Response.Content = new ObjectContent(payloadValue.GetType(), payloadValue, objectContent.Formatter); + actionExecutedContext.Response.Content = new ObjectContent(documentValue.GetType(), documentValue, objectContent.Formatter); } } diff --git a/JSONAPI/ActionFilters/JsonApiExceptionFilterAttribute.cs b/JSONAPI/ActionFilters/JsonApiExceptionFilterAttribute.cs index d7ed4fc1..be96b26e 100644 --- a/JSONAPI/ActionFilters/JsonApiExceptionFilterAttribute.cs +++ b/JSONAPI/ActionFilters/JsonApiExceptionFilterAttribute.cs @@ -2,40 +2,40 @@ using System.Net; using System.Net.Http; using System.Web.Http.Filters; +using JSONAPI.Documents.Builders; using JSONAPI.Json; -using JSONAPI.Payload.Builders; namespace JSONAPI.ActionFilters { /// - /// Filter for catching exceptions and converting them to IErrorPayload + /// Filter for catching exceptions and converting them to IErrorDocument /// public class JsonApiExceptionFilterAttribute : ExceptionFilterAttribute { - private readonly IErrorPayloadBuilder _errorPayloadBuilder; + private readonly IErrorDocumentBuilder _errorDocumentBuilder; private readonly JsonApiFormatter _jsonApiFormatter; /// /// /// - /// + /// /// - public JsonApiExceptionFilterAttribute(IErrorPayloadBuilder errorPayloadBuilder, JsonApiFormatter jsonApiFormatter) + public JsonApiExceptionFilterAttribute(IErrorDocumentBuilder errorDocumentBuilder, JsonApiFormatter jsonApiFormatter) { - _errorPayloadBuilder = errorPayloadBuilder; + _errorDocumentBuilder = errorDocumentBuilder; _jsonApiFormatter = jsonApiFormatter; } public override void OnException(HttpActionExecutedContext actionExecutedContext) { - var payload = _errorPayloadBuilder.BuildFromException(actionExecutedContext.Exception); + var document = _errorDocumentBuilder.BuildFromException(actionExecutedContext.Exception); actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError) { - Content = new ObjectContent(payload.GetType(), payload, _jsonApiFormatter) + Content = new ObjectContent(document.GetType(), document, _jsonApiFormatter) }; - if (payload.Errors != null && payload.Errors.Length > 0) + if (document.Errors != null && document.Errors.Length > 0) { - var status = payload.Errors.First().Status; + var status = document.Errors.First().Status; actionExecutedContext.Response.StatusCode = status != default(HttpStatusCode) ? status : HttpStatusCode.InternalServerError; } } diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index 5fb8fd4c..0f8ee2b1 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -1,9 +1,9 @@ using System.Web.Http; using JSONAPI.ActionFilters; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using JSONAPI.Http; using JSONAPI.Json; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; namespace JSONAPI.Core { @@ -34,7 +34,7 @@ public JsonApiConfiguration(IResourceTypeRegistry resourceTypeRegistry, ILinkCon } /// - /// Allows overriding the queryable payload builder to use. This is useful for + /// Allows overriding the queryable document builder to use. This is useful for /// /// public void UseQueryableEnumeration(IQueryableEnumerationTransformer queryableEnumerationTransformer) @@ -51,15 +51,15 @@ public void Apply(HttpConfiguration httpConfig) var linkConventions = _linkConventions ?? new DefaultLinkConventions(); // Serialization - var metadataSerializer = new MetadataSerializer(); - var linkSerializer = new LinkSerializer(metadataSerializer); - var resourceLinkageSerializer = new ResourceLinkageSerializer(); - var relationshipObjectSerializer = new RelationshipObjectSerializer(linkSerializer, resourceLinkageSerializer, metadataSerializer); - var resourceObjectSerializer = new ResourceObjectSerializer(relationshipObjectSerializer, linkSerializer, metadataSerializer); - var errorSerializer = new ErrorSerializer(linkSerializer, metadataSerializer); - var singleResourcePayloadSerializer = new SingleResourcePayloadSerializer(resourceObjectSerializer, metadataSerializer); - var resourceCollectionPayloadSerializer = new ResourceCollectionPayloadSerializer(resourceObjectSerializer, metadataSerializer); - var errorPayloadSerializer = new ErrorPayloadSerializer(errorSerializer, metadataSerializer); + var metadataFormatter = new MetadataFormatter(); + var linkFormatter = new LinkFormatter(metadataFormatter); + var resourceLinkageFormatter = new ResourceLinkageFormatter(); + var relationshipObjectFormatter = new RelationshipObjectFormatter(linkFormatter, resourceLinkageFormatter, metadataFormatter); + var resourceObjectFormatter = new ResourceObjectFormatter(relationshipObjectFormatter, linkFormatter, metadataFormatter); + var errorFormatter = new ErrorFormatter(linkFormatter, metadataFormatter); + var singleResourceDocumentFormatter = new SingleResourceDocumentFormatter(resourceObjectFormatter, metadataFormatter); + var resourceCollectionDocumentFormatter = new ResourceCollectionDocumentFormatter(resourceObjectFormatter, metadataFormatter); + var errorDocumentFormatter = new ErrorDocumentFormatter(errorFormatter, metadataFormatter); // Queryable transforms var queryableEnumerationTransformer = _queryableEnumerationTransformer ?? new SynchronousEnumerationTransformer(); @@ -69,20 +69,20 @@ public void Apply(HttpConfiguration httpConfig) // Builders var baseUrlService = new BaseUrlService(); - var singleResourcePayloadBuilder = new RegistryDrivenSingleResourcePayloadBuilder(_resourceTypeRegistry, linkConventions); - var resourceCollectionPayloadBuilder = new RegistryDrivenResourceCollectionPayloadBuilder(_resourceTypeRegistry, linkConventions); - var queryableResourcePayloadBuilder = new DefaultQueryableResourceCollectionPayloadBuilder(resourceCollectionPayloadBuilder, + var singleResourceDocumentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(_resourceTypeRegistry, linkConventions); + var resourceCollectionDocumentBuilder = new RegistryDrivenResourceCollectionDocumentBuilder(_resourceTypeRegistry, linkConventions); + var queryableResourceCollectionDocumentBuilder = new DefaultQueryableResourceCollectionDocumentBuilder(resourceCollectionDocumentBuilder, queryableEnumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer, baseUrlService); - var errorPayloadBuilder = new ErrorPayloadBuilder(); - var fallbackPayloadBuilder = new FallbackPayloadBuilder(singleResourcePayloadBuilder, - queryableResourcePayloadBuilder, resourceCollectionPayloadBuilder, baseUrlService); + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder, + queryableResourceCollectionDocumentBuilder, resourceCollectionDocumentBuilder, baseUrlService); // Dependencies for JsonApiHttpConfiguration - var formatter = new JsonApiFormatter(singleResourcePayloadSerializer, resourceCollectionPayloadSerializer, errorPayloadSerializer, errorPayloadBuilder); - var fallbackPayloadBuilderAttribute = new FallbackPayloadBuilderAttribute(fallbackPayloadBuilder, errorPayloadBuilder); - var exceptionFilterAttribute = new JsonApiExceptionFilterAttribute(errorPayloadBuilder, formatter); + var formatter = new JsonApiFormatter(singleResourceDocumentFormatter, resourceCollectionDocumentFormatter, errorDocumentFormatter, errorDocumentBuilder); + var fallbackDocumentBuilderAttribute = new FallbackDocumentBuilderAttribute(fallbackDocumentBuilder, errorDocumentBuilder); + var exceptionFilterAttribute = new JsonApiExceptionFilterAttribute(errorDocumentBuilder, formatter); - var jsonApiHttpConfiguration = new JsonApiHttpConfiguration(formatter, fallbackPayloadBuilderAttribute, exceptionFilterAttribute); + var jsonApiHttpConfiguration = new JsonApiHttpConfiguration(formatter, fallbackDocumentBuilderAttribute, exceptionFilterAttribute); jsonApiHttpConfiguration.Apply(httpConfig); } } diff --git a/JSONAPI/Core/JsonApiHttpConfiguration.cs b/JSONAPI/Core/JsonApiHttpConfiguration.cs index 37a12e23..12e2344b 100644 --- a/JSONAPI/Core/JsonApiHttpConfiguration.cs +++ b/JSONAPI/Core/JsonApiHttpConfiguration.cs @@ -13,22 +13,22 @@ namespace JSONAPI.Core public class JsonApiHttpConfiguration { private readonly JsonApiFormatter _formatter; - private readonly FallbackPayloadBuilderAttribute _fallbackPayloadBuilderAttribute; + private readonly FallbackDocumentBuilderAttribute _fallbackDocumentBuilderAttribute; private readonly JsonApiExceptionFilterAttribute _jsonApiExceptionFilterAttribute; /// /// Creates a new configuration /// public JsonApiHttpConfiguration(JsonApiFormatter formatter, - FallbackPayloadBuilderAttribute fallbackPayloadBuilderAttribute, + FallbackDocumentBuilderAttribute fallbackDocumentBuilderAttribute, JsonApiExceptionFilterAttribute jsonApiExceptionFilterAttribute) { if (formatter == null) throw new ArgumentNullException("formatter"); - if (fallbackPayloadBuilderAttribute == null) throw new ArgumentNullException("fallbackPayloadBuilderAttribute"); + if (fallbackDocumentBuilderAttribute == null) throw new ArgumentNullException("fallbackDocumentBuilderAttribute"); if (jsonApiExceptionFilterAttribute == null) throw new ArgumentNullException("jsonApiExceptionFilterAttribute"); _formatter = formatter; - _fallbackPayloadBuilderAttribute = fallbackPayloadBuilderAttribute; + _fallbackDocumentBuilderAttribute = fallbackDocumentBuilderAttribute; _jsonApiExceptionFilterAttribute = jsonApiExceptionFilterAttribute; } @@ -41,7 +41,7 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(_formatter); - httpConfig.Filters.Add(_fallbackPayloadBuilderAttribute); + httpConfig.Filters.Add(_fallbackDocumentBuilderAttribute); httpConfig.Filters.Add(_jsonApiExceptionFilterAttribute); httpConfig.Services.Replace(typeof(IHttpControllerSelector), diff --git a/JSONAPI/Core/ResourceTypeRegistry.cs b/JSONAPI/Core/ResourceTypeRegistry.cs index 839cb11f..154b601c 100644 --- a/JSONAPI/Core/ResourceTypeRegistry.cs +++ b/JSONAPI/Core/ResourceTypeRegistry.cs @@ -102,7 +102,7 @@ public ResourceTypeRegistry(INamingConventions namingConventions) } /// - /// Represents a type's registration with a model manager + /// Represents a type's registration with a registry /// protected sealed class ResourceTypeRegistration : IResourceTypeRegistration { @@ -211,7 +211,7 @@ private ResourceTypeRegistration FindRegistrationForType(Type type) } /// - /// Registeres a type with this ModelManager, using a default resource type name. + /// Registeres a type with this ResourceTypeRegistry, using a default resource type name. /// /// The type to register. /// The resource type name to use diff --git a/JSONAPI/Payload/Builders/DefaultQueryableResourceCollectionPayloadBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs similarity index 60% rename from JSONAPI/Payload/Builders/DefaultQueryableResourceCollectionPayloadBuilder.cs rename to JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index d1affcc3..30a6849f 100644 --- a/JSONAPI/Payload/Builders/DefaultQueryableResourceCollectionPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -1,19 +1,18 @@ -using System; -using System.Linq; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using JSONAPI.ActionFilters; using JSONAPI.Http; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Provides a default implementation of an IQueryablePayloadBuilder + /// Provides a default implementation of an IQueryableResourceCollectionDocumentBuilder /// - public class DefaultQueryableResourceCollectionPayloadBuilder : IQueryableResourceCollectionPayloadBuilder + public class DefaultQueryableResourceCollectionDocumentBuilder : IQueryableResourceCollectionDocumentBuilder { - private readonly IResourceCollectionPayloadBuilder _resourceCollectionPayloadBuilder; + private readonly IResourceCollectionDocumentBuilder _resourceCollectionDocumentBuilder; private readonly IQueryableEnumerationTransformer _enumerationTransformer; private readonly IQueryableFilteringTransformer _filteringTransformer; private readonly IQueryableSortingTransformer _sortingTransformer; @@ -21,23 +20,17 @@ public class DefaultQueryableResourceCollectionPayloadBuilder : IQueryableResour private readonly IBaseUrlService _baseUrlService; /// - /// + /// Creates a new DefaultQueryableResourceCollectionDocumentBuilder /// - /// - /// - /// - /// - /// - /// - public DefaultQueryableResourceCollectionPayloadBuilder( - IResourceCollectionPayloadBuilder resourceCollectionPayloadBuilder, + public DefaultQueryableResourceCollectionDocumentBuilder( + IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, IQueryableEnumerationTransformer enumerationTransformer, IQueryableFilteringTransformer filteringTransformer, IQueryableSortingTransformer sortingTransformer, IQueryablePaginationTransformer paginationTransformer, IBaseUrlService baseUrlService) { - _resourceCollectionPayloadBuilder = resourceCollectionPayloadBuilder; + _resourceCollectionDocumentBuilder = resourceCollectionDocumentBuilder; _enumerationTransformer = enumerationTransformer; _filteringTransformer = filteringTransformer; _sortingTransformer = sortingTransformer; @@ -45,7 +38,7 @@ public DefaultQueryableResourceCollectionPayloadBuilder( _baseUrlService = baseUrlService; } - public async Task BuildPayload(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken) + public async Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken) { if (_filteringTransformer != null) query = _filteringTransformer.Filter(query, request); @@ -62,7 +55,7 @@ public async Task BuildPayload(IQueryable quer var linkBaseUrl = _baseUrlService.GetBaseUrl(request); var results = await _enumerationTransformer.Enumerate(query, cancellationToken); - return _resourceCollectionPayloadBuilder.BuildPayload(results, linkBaseUrl, null, null); + return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, null, null); } } } diff --git a/JSONAPI/Payload/Builders/ErrorPayloadBuilder.cs b/JSONAPI/Documents/Builders/ErrorDocumentBuilder.cs similarity index 89% rename from JSONAPI/Payload/Builders/ErrorPayloadBuilder.cs rename to JSONAPI/Documents/Builders/ErrorDocumentBuilder.cs index 58323ce8..aead058c 100644 --- a/JSONAPI/Payload/Builders/ErrorPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/ErrorDocumentBuilder.cs @@ -5,34 +5,34 @@ using JSONAPI.Json; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Default implementation of IErrorPayloadBuilder + /// Default implementation of IErrorDocumentBuilder /// - public class ErrorPayloadBuilder : IErrorPayloadBuilder + public class ErrorDocumentBuilder : IErrorDocumentBuilder { private readonly IDictionary> _specificExceptionHandlers; /// - /// Creates a new ErrorPayloadBuilder + /// Creates a new ErrorDocumentBuilder /// - public ErrorPayloadBuilder() + public ErrorDocumentBuilder() { _specificExceptionHandlers = new Dictionary>(); _specificExceptionHandlers[typeof(JsonApiException)] = GetErrorForJsonApiException; _specificExceptionHandlers[typeof(DeserializationException)] = GetErrorForDeserializationException; } - public IErrorPayload BuildFromException(Exception exception) + public IErrorDocument BuildFromException(Exception exception) { var error = BuildErrorForException(exception); var topLevelMetadata = GetTopLevelMetadata(); - return new ErrorPayload(new [] { error }, topLevelMetadata); + return new ErrorDocument(new [] { error }, topLevelMetadata); } - public IErrorPayload BuildFromHttpError(HttpError httpError, HttpStatusCode statusCode) + public IErrorDocument BuildFromHttpError(HttpError httpError, HttpStatusCode statusCode) { var error = new Error { @@ -45,7 +45,7 @@ public IErrorPayload BuildFromHttpError(HttpError httpError, HttpStatusCode stat var topLevelMetadata = GetTopLevelMetadata(); - return new ErrorPayload(new[] { (IError)error }, topLevelMetadata); + return new ErrorDocument(new[] { (IError)error }, topLevelMetadata); } /// @@ -94,7 +94,7 @@ protected virtual ILink GetAboutLinkForException(Exception exception) } /// - /// Allows configuring top-level metadata for an error response payload. + /// Allows configuring top-level metadata for an error response document. /// /// protected virtual IMetadata GetTopLevelMetadata() diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs new file mode 100644 index 00000000..cec5ccd1 --- /dev/null +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Http; + +namespace JSONAPI.Documents.Builders +{ + /// + /// Default implementation of IFallbackDocumentBuilder + /// + public class FallbackDocumentBuilder : IFallbackDocumentBuilder + { + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + private readonly IResourceCollectionDocumentBuilder _resourceCollectionDocumentBuilder; + private readonly IBaseUrlService _baseUrlService; + private readonly Lazy _openBuildDocumentFromQueryableMethod; + private readonly Lazy _openBuildDocumentFromEnumerableMethod; + + /// + /// Creates a new FallbackDocumentBuilder + /// + public FallbackDocumentBuilder(ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, + IBaseUrlService baseUrlService) + { + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + _resourceCollectionDocumentBuilder = resourceCollectionDocumentBuilder; + _baseUrlService = baseUrlService; + + _openBuildDocumentFromQueryableMethod = + new Lazy( + () => _queryableResourceCollectionDocumentBuilder.GetType() + .GetMethod("BuildDocument", BindingFlags.Instance | BindingFlags.Public)); + + _openBuildDocumentFromEnumerableMethod = + new Lazy( + () => _resourceCollectionDocumentBuilder.GetType() + .GetMethod("BuildDocument", BindingFlags.Instance | BindingFlags.Public)); + } + + public async Task BuildDocument(object obj, HttpRequestMessage requestMessage, + CancellationToken cancellationToken) + { + var type = obj.GetType(); + + var queryableInterfaces = type.GetInterfaces(); + var queryableInterface = + queryableInterfaces.FirstOrDefault( + i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IQueryable<>)); + if (queryableInterface != null) + { + var queryableElementType = queryableInterface.GenericTypeArguments[0]; + var buildDocumentMethod = + _openBuildDocumentFromQueryableMethod.Value.MakeGenericMethod(queryableElementType); + + dynamic materializedQueryTask = buildDocumentMethod.Invoke(_queryableResourceCollectionDocumentBuilder, + new[] {obj, requestMessage, cancellationToken}); + + return await materializedQueryTask; + } + + var isCollection = false; + var enumerableElementType = GetEnumerableElementType(type); + if (enumerableElementType != null) + { + isCollection = true; + } + + var linkBaseUrl = _baseUrlService.GetBaseUrl(requestMessage); + + if (isCollection) + { + var buildDocumentMethod = + _openBuildDocumentFromEnumerableMethod.Value.MakeGenericMethod(enumerableElementType); + return + (dynamic)buildDocumentMethod.Invoke(_resourceCollectionDocumentBuilder, new[] { obj, linkBaseUrl, new string[] { }, null }); + } + + // Single resource object + return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, null); + } + + private static Type GetEnumerableElementType(Type collectionType) + { + if (collectionType.IsArray) + return collectionType.GetElementType(); + + if (collectionType.IsGenericType && collectionType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return collectionType.GetGenericArguments()[0]; + } + + var enumerableInterface = collectionType.GetInterface(typeof(IEnumerable<>).FullName); + if (enumerableInterface == null) return null; + + var genericArguments = collectionType.GetGenericArguments(); + if (!genericArguments.Any()) return null; + + return genericArguments[0]; + } + } +} diff --git a/JSONAPI/Documents/Builders/IErrorDocumentBuilder.cs b/JSONAPI/Documents/Builders/IErrorDocumentBuilder.cs new file mode 100644 index 00000000..900d6d05 --- /dev/null +++ b/JSONAPI/Documents/Builders/IErrorDocumentBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Net; +using System.Web.Http; + +namespace JSONAPI.Documents.Builders +{ + /// + /// Provides services for building an error document + /// + public interface IErrorDocumentBuilder + { + /// + /// Builds an error document based on an exception + /// + IErrorDocument BuildFromException(Exception exception); + + /// + /// Builds an error document based on an HttpError + /// + IErrorDocument BuildFromHttpError(HttpError httpError, HttpStatusCode statusCode); + } +} \ No newline at end of file diff --git a/JSONAPI/Payload/Builders/IFallbackPayloadBuilder.cs b/JSONAPI/Documents/Builders/IFallbackDocumentBuilder.cs similarity index 50% rename from JSONAPI/Payload/Builders/IFallbackPayloadBuilder.cs rename to JSONAPI/Documents/Builders/IFallbackDocumentBuilder.cs index 98d7c4a7..5004d2fd 100644 --- a/JSONAPI/Payload/Builders/IFallbackPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/IFallbackDocumentBuilder.cs @@ -2,21 +2,21 @@ using System.Threading; using System.Threading.Tasks; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Service to create a payload when the type is unknown at compile-time + /// Service to create a document when the type is unknown at compile-time /// - public interface IFallbackPayloadBuilder + public interface IFallbackDocumentBuilder { /// - /// Builds a JSON API payload based on the given object + /// Builds a JSON API document based on the given object /// /// /// /// /// - /// Thrown when an error occurs when building the payload - Task BuildPayload(object obj, HttpRequestMessage requestMessage, CancellationToken cancellationToken); + /// Thrown when an error occurs when building the document + Task BuildDocument(object obj, HttpRequestMessage requestMessage, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/JSONAPI/Payload/Builders/IQueryableResourceCollectionPayloadBuilder.cs b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs similarity index 53% rename from JSONAPI/Payload/Builders/IQueryableResourceCollectionPayloadBuilder.cs rename to JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs index dfb5fb73..58c3bc32 100644 --- a/JSONAPI/Payload/Builders/IQueryableResourceCollectionPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs @@ -3,21 +3,21 @@ using System.Threading; using System.Threading.Tasks; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// This interface is responsible for building IPayload objects based on IQueryable ObjectContent + /// This interface is responsible for building IResourceCollectionDocument objects based on IQueryable ObjectContent /// - public interface IQueryableResourceCollectionPayloadBuilder + public interface IQueryableResourceCollectionDocumentBuilder { /// - /// Builds a payload object for the given query + /// Builds a document object for the given query /// - /// The query to materialize to build the response payload + /// The query to materialize to build the response document /// The request containing parameters to determine how to sort/filter/paginate the query /// /// /// - Task BuildPayload(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken); + Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken); } } diff --git a/JSONAPI/Payload/Builders/IResourceCollectionPayloadBuilder.cs b/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs similarity index 60% rename from JSONAPI/Payload/Builders/IResourceCollectionPayloadBuilder.cs rename to JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs index b5edbbae..899eb99d 100644 --- a/JSONAPI/Payload/Builders/IResourceCollectionPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Builds a response payload from primary data objects + /// Builds a response document from primary data objects /// - public interface IResourceCollectionPayloadBuilder + public interface IResourceCollectionDocumentBuilder { /// - /// Builds an IResourceCollectionPayload from the given queryable of model objects + /// Builds an IResourceCollectionDocument from the given queryable of model objects /// /// /// The string to prepend to link URLs. @@ -17,6 +17,6 @@ public interface IResourceCollectionPayloadBuilder /// Metadata for the top-level /// /// - IResourceCollectionPayload BuildPayload(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata); + IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata); } } \ No newline at end of file diff --git a/JSONAPI/Payload/Builders/ISingleResourcePayloadBuilder.cs b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs similarity index 57% rename from JSONAPI/Payload/Builders/ISingleResourcePayloadBuilder.cs rename to JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs index 377aefc7..976fd730 100644 --- a/JSONAPI/Payload/Builders/ISingleResourcePayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs @@ -1,18 +1,18 @@ -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Builds a response payload from primary data objects + /// Builds a response document from primary data objects /// - public interface ISingleResourcePayloadBuilder + public interface ISingleResourceDocumentBuilder { /// - /// Builds an ISingleResourcePayload from the given model object + /// Builds an ISingleResourceDocument from the given model object /// /// /// The string to prepend to link URLs. /// A list of dot-separated paths to include in the compound document. /// If this collection is null or empty, no linkage will be included. /// - ISingleResourcePayload BuildPayload(object primaryData, string linkBaseUrl, string[] includePathExpressions); + ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions); } } diff --git a/JSONAPI/Payload/Builders/JsonApiException.cs b/JSONAPI/Documents/Builders/JsonApiException.cs similarity index 92% rename from JSONAPI/Payload/Builders/JsonApiException.cs rename to JSONAPI/Documents/Builders/JsonApiException.cs index b85a0b2e..d702b848 100644 --- a/JSONAPI/Payload/Builders/JsonApiException.cs +++ b/JSONAPI/Documents/Builders/JsonApiException.cs @@ -1,11 +1,11 @@ using System; using System.Net; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Exception that should be thrown by payload builders if an error occurs. The data in - /// this exception will drive the construction of the error object in the response payload, + /// Exception that should be thrown by document builders if an error occurs. The data in + /// this exception will drive the construction of the error object in the response document, /// as well as the HTTP status code. /// public class JsonApiException : Exception diff --git a/JSONAPI/Payload/Builders/RegistryDrivenPayloadBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs similarity index 95% rename from JSONAPI/Payload/Builders/RegistryDrivenPayloadBuilder.cs rename to JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs index 4a71ad21..fd5b0335 100644 --- a/JSONAPI/Payload/Builders/RegistryDrivenPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs @@ -3,22 +3,22 @@ using JSONAPI.Core; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Base class for the main payload builders + /// Base class for the main document builders /// - public abstract class RegistryDrivenPayloadBuilder + public abstract class RegistryDrivenDocumentBuilder { private readonly IResourceTypeRegistry _resourceTypeRegistry; private readonly ILinkConventions _linkConventions; /// - /// Creates a new RegistryDrivenPayloadBuilder + /// Creates a new RegistryDrivenDocumentBuilder /// /// /// - protected RegistryDrivenPayloadBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) + protected RegistryDrivenDocumentBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) { _resourceTypeRegistry = resourceTypeRegistry; _linkConventions = linkConventions; diff --git a/JSONAPI/Payload/Builders/RegistryDrivenResourceCollectionPayloadBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs similarity index 54% rename from JSONAPI/Payload/Builders/RegistryDrivenResourceCollectionPayloadBuilder.cs rename to JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs index aaa24883..2eb68c35 100644 --- a/JSONAPI/Payload/Builders/RegistryDrivenResourceCollectionPayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs @@ -2,24 +2,24 @@ using System.Linq; using JSONAPI.Core; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Builds a payload for a collection of resources that are registered with a resource type registry + /// Builds a document for a collection of resources that are registered with a resource type registry /// - public class RegistryDrivenResourceCollectionPayloadBuilder : RegistryDrivenPayloadBuilder, IResourceCollectionPayloadBuilder + public class RegistryDrivenResourceCollectionDocumentBuilder : RegistryDrivenDocumentBuilder, IResourceCollectionDocumentBuilder { /// - /// Creates a new RegistryDrivenSingleResourcePayloadBuilder + /// Creates a new RegistryDrivenSingleResourceDocumentBuilder /// /// The resource type registry to use to locate the registered type /// Conventions to follow when building links - public RegistryDrivenResourceCollectionPayloadBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) + public RegistryDrivenResourceCollectionDocumentBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) : base(resourceTypeRegistry, linkConventions) { } - public IResourceCollectionPayload BuildPayload(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata) + public IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata) { var idDictionariesByType = new Dictionary>(); var primaryDataResources = @@ -27,8 +27,8 @@ public IResourceCollectionPayload BuildPayload(IEnumerable prima .ToArray(); var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); - var payload = new ResourceCollectionPayload(primaryDataResources, relatedData, metadata); - return payload; + var document = new ResourceCollectionDocument(primaryDataResources, relatedData, metadata); + return document; } } } \ No newline at end of file diff --git a/JSONAPI/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs similarity index 54% rename from JSONAPI/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilder.cs rename to JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs index b1e8b982..21e32a61 100644 --- a/JSONAPI/Payload/Builders/RegistryDrivenSingleResourcePayloadBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs @@ -2,31 +2,31 @@ using System.Linq; using JSONAPI.Core; -namespace JSONAPI.Payload.Builders +namespace JSONAPI.Documents.Builders { /// - /// Builds a payload for a resource that is registered with a resource type registry + /// Builds a document for a resource that is registered with a resource type registry /// - public class RegistryDrivenSingleResourcePayloadBuilder : RegistryDrivenPayloadBuilder, ISingleResourcePayloadBuilder + public class RegistryDrivenSingleResourceDocumentBuilder : RegistryDrivenDocumentBuilder, ISingleResourceDocumentBuilder { /// - /// Creates a new RegistryDrivenSingleResourcePayloadBuilder + /// Creates a new RegistryDrivenSingleResourceDocumentBuilder /// /// The resource type registry to use to locate the registered type /// Conventions to follow when building links - public RegistryDrivenSingleResourcePayloadBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) + public RegistryDrivenSingleResourceDocumentBuilder(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) : base(resourceTypeRegistry, linkConventions) { } - public ISingleResourcePayload BuildPayload(object primaryData, string linkBaseUrl, string[] includePathExpressions) + public ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions) { var idDictionariesByType = new Dictionary>(); var primaryDataResource = CreateResourceObject(primaryData, idDictionariesByType, null, includePathExpressions, linkBaseUrl); var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); - var payload = new SingleResourcePayload(primaryDataResource, relatedData, null); - return payload; + var document = new SingleResourceDocument(primaryDataResource, relatedData, null); + return document; } } } \ No newline at end of file diff --git a/JSONAPI/Payload/DefaultLinkConventions.cs b/JSONAPI/Documents/DefaultLinkConventions.cs similarity index 99% rename from JSONAPI/Payload/DefaultLinkConventions.cs rename to JSONAPI/Documents/DefaultLinkConventions.cs index 5983bc77..6ca5f30e 100644 --- a/JSONAPI/Payload/DefaultLinkConventions.cs +++ b/JSONAPI/Documents/DefaultLinkConventions.cs @@ -1,7 +1,7 @@ using System; using JSONAPI.Core; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Implementation of ILinkConventions that adheres to JSON API recommendations for URL formatting. diff --git a/JSONAPI/Payload/Error.cs b/JSONAPI/Documents/Error.cs similarity index 95% rename from JSONAPI/Payload/Error.cs rename to JSONAPI/Documents/Error.cs index b0509f67..95659008 100644 --- a/JSONAPI/Payload/Error.cs +++ b/JSONAPI/Documents/Error.cs @@ -1,6 +1,6 @@ using System.Net; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Default implementation of IError diff --git a/JSONAPI/Payload/ErrorPayload.cs b/JSONAPI/Documents/ErrorDocument.cs similarity index 61% rename from JSONAPI/Payload/ErrorPayload.cs rename to JSONAPI/Documents/ErrorDocument.cs index 2df7db73..23dc4637 100644 --- a/JSONAPI/Payload/ErrorPayload.cs +++ b/JSONAPI/Documents/ErrorDocument.cs @@ -1,19 +1,19 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// - /// Default implementation of IErrorPayload + /// Default implementation of IErrorDocument /// - public class ErrorPayload : IErrorPayload + public class ErrorDocument : IErrorDocument { public IError[] Errors { get; private set; } public IMetadata Metadata { get; private set; } /// - /// Creates a new ErrorPayload + /// Creates a new ErrorDocument /// /// /// - public ErrorPayload(IError[] errors, IMetadata metadata) + public ErrorDocument(IError[] errors, IMetadata metadata) { Errors = errors; Metadata = metadata; diff --git a/JSONAPI/Payload/ExceptionErrorMetadata.cs b/JSONAPI/Documents/ExceptionErrorMetadata.cs similarity index 97% rename from JSONAPI/Payload/ExceptionErrorMetadata.cs rename to JSONAPI/Documents/ExceptionErrorMetadata.cs index 4c6c5c6a..9af55611 100644 --- a/JSONAPI/Payload/ExceptionErrorMetadata.cs +++ b/JSONAPI/Documents/ExceptionErrorMetadata.cs @@ -1,7 +1,7 @@ using System; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Metadata object for serializing exceptions in a response diff --git a/JSONAPI/Payload/IError.cs b/JSONAPI/Documents/IError.cs similarity index 98% rename from JSONAPI/Payload/IError.cs rename to JSONAPI/Documents/IError.cs index f750dfdb..ea7e37e1 100644 --- a/JSONAPI/Payload/IError.cs +++ b/JSONAPI/Documents/IError.cs @@ -1,6 +1,6 @@ using System.Net; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Represents an error appearing in the `errors` array at the document top-level diff --git a/JSONAPI/Documents/IErrorDocument.cs b/JSONAPI/Documents/IErrorDocument.cs new file mode 100644 index 00000000..78892aa5 --- /dev/null +++ b/JSONAPI/Documents/IErrorDocument.cs @@ -0,0 +1,13 @@ +namespace JSONAPI.Documents +{ + /// + /// Interface for JSON API documents that represent a collection of errors + /// + public interface IErrorDocument : IJsonApiDocument + { + /// + /// The errors to send in this document + /// + IError[] Errors { get; } + } +} \ No newline at end of file diff --git a/JSONAPI/Documents/IJsonApiDocument.cs b/JSONAPI/Documents/IJsonApiDocument.cs new file mode 100644 index 00000000..120d2cd2 --- /dev/null +++ b/JSONAPI/Documents/IJsonApiDocument.cs @@ -0,0 +1,13 @@ +namespace JSONAPI.Documents +{ + /// + /// Base interface for document + /// + public interface IJsonApiDocument + { + /// + /// Metadata for the document as a whole + /// + IMetadata Metadata { get; } + } +} \ No newline at end of file diff --git a/JSONAPI/Payload/ILink.cs b/JSONAPI/Documents/ILink.cs similarity index 97% rename from JSONAPI/Payload/ILink.cs rename to JSONAPI/Documents/ILink.cs index 9198114e..32fab92e 100644 --- a/JSONAPI/Payload/ILink.cs +++ b/JSONAPI/Documents/ILink.cs @@ -1,4 +1,4 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// A link that may be found in a "links object" diff --git a/JSONAPI/Payload/ILinkConventions.cs b/JSONAPI/Documents/ILinkConventions.cs similarity index 95% rename from JSONAPI/Payload/ILinkConventions.cs rename to JSONAPI/Documents/ILinkConventions.cs index 76ecd018..ec369a26 100644 --- a/JSONAPI/Payload/ILinkConventions.cs +++ b/JSONAPI/Documents/ILinkConventions.cs @@ -1,7 +1,6 @@ -using System.Net.Http; -using JSONAPI.Core; +using JSONAPI.Core; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Service to provide formatting of links diff --git a/JSONAPI/Payload/IMetadata.cs b/JSONAPI/Documents/IMetadata.cs similarity index 91% rename from JSONAPI/Payload/IMetadata.cs rename to JSONAPI/Documents/IMetadata.cs index 1e92857c..99e75c7b 100644 --- a/JSONAPI/Payload/IMetadata.cs +++ b/JSONAPI/Documents/IMetadata.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Represents metadata that can be embedded in several places diff --git a/JSONAPI/Payload/IRelationshipObject.cs b/JSONAPI/Documents/IRelationshipObject.cs similarity index 95% rename from JSONAPI/Payload/IRelationshipObject.cs rename to JSONAPI/Documents/IRelationshipObject.cs index a1d1cb59..b65259ec 100644 --- a/JSONAPI/Payload/IRelationshipObject.cs +++ b/JSONAPI/Documents/IRelationshipObject.cs @@ -1,4 +1,4 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Represents a JSON API relationship object diff --git a/JSONAPI/Payload/IResourceCollectionPayload.cs b/JSONAPI/Documents/IResourceCollectionDocument.cs similarity index 56% rename from JSONAPI/Payload/IResourceCollectionPayload.cs rename to JSONAPI/Documents/IResourceCollectionDocument.cs index cddd7ba0..bcc52cf9 100644 --- a/JSONAPI/Payload/IResourceCollectionPayload.cs +++ b/JSONAPI/Documents/IResourceCollectionDocument.cs @@ -1,12 +1,12 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// - /// Interface for JSON API payloads that represent a collection of resources + /// Interface for JSON API documents that represent a collection of resources /// - public interface IResourceCollectionPayload : IJsonApiPayload + public interface IResourceCollectionDocument : IJsonApiDocument { /// - /// The payload's primary data + /// The document's primary data /// IResourceObject[] PrimaryData { get; } diff --git a/JSONAPI/Documents/IResourceIdentifier.cs b/JSONAPI/Documents/IResourceIdentifier.cs new file mode 100644 index 00000000..9a7ae764 --- /dev/null +++ b/JSONAPI/Documents/IResourceIdentifier.cs @@ -0,0 +1,18 @@ +namespace JSONAPI.Documents +{ + /// + /// Type/ID pair that identifies a particular resource + /// + public interface IResourceIdentifier + { + /// + /// The type of resource + /// + string Type { get; } + + /// + /// The ID of the resource + /// + string Id { get; } + } +} \ No newline at end of file diff --git a/JSONAPI/Payload/IResourceLinkage.cs b/JSONAPI/Documents/IResourceLinkage.cs similarity index 91% rename from JSONAPI/Payload/IResourceLinkage.cs rename to JSONAPI/Documents/IResourceLinkage.cs index 7fc2a197..a5cfe06e 100644 --- a/JSONAPI/Payload/IResourceLinkage.cs +++ b/JSONAPI/Documents/IResourceLinkage.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Describes a relationship's linkage diff --git a/JSONAPI/Payload/IResourceObject.cs b/JSONAPI/Documents/IResourceObject.cs similarity index 97% rename from JSONAPI/Payload/IResourceObject.cs rename to JSONAPI/Documents/IResourceObject.cs index 7344352d..6d839e40 100644 --- a/JSONAPI/Payload/IResourceObject.cs +++ b/JSONAPI/Documents/IResourceObject.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Represents a JSON API resource object diff --git a/JSONAPI/Payload/ISingleResourcePayload.cs b/JSONAPI/Documents/ISingleResourceDocument.cs similarity index 57% rename from JSONAPI/Payload/ISingleResourcePayload.cs rename to JSONAPI/Documents/ISingleResourceDocument.cs index a31783c0..684521f0 100644 --- a/JSONAPI/Payload/ISingleResourcePayload.cs +++ b/JSONAPI/Documents/ISingleResourceDocument.cs @@ -1,12 +1,12 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// - /// Interface for JSON API payloads that represent a single resource + /// Interface for JSON API documents that represent a single resource /// - public interface ISingleResourcePayload : IJsonApiPayload + public interface ISingleResourceDocument : IJsonApiDocument { /// - /// The payload's primary data + /// The document's primary data /// IResourceObject PrimaryData { get; } diff --git a/JSONAPI/Payload/RelationshipObject.cs b/JSONAPI/Documents/RelationshipObject.cs similarity index 97% rename from JSONAPI/Payload/RelationshipObject.cs rename to JSONAPI/Documents/RelationshipObject.cs index 089d24b0..4c3d7e14 100644 --- a/JSONAPI/Payload/RelationshipObject.cs +++ b/JSONAPI/Documents/RelationshipObject.cs @@ -1,4 +1,4 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Default implementation for IRelationshipObject diff --git a/JSONAPI/Documents/ResourceCollectionDocument.cs b/JSONAPI/Documents/ResourceCollectionDocument.cs new file mode 100644 index 00000000..daab255b --- /dev/null +++ b/JSONAPI/Documents/ResourceCollectionDocument.cs @@ -0,0 +1,22 @@ +namespace JSONAPI.Documents +{ + /// + /// Default implementation of IResourceCollectionDocument + /// + public class ResourceCollectionDocument : IResourceCollectionDocument + { + public IResourceObject[] PrimaryData { get; private set; } + public IResourceObject[] RelatedData { get; private set; } + public IMetadata Metadata { get; private set; } + + /// + /// Constructs a resource collection document + /// + public ResourceCollectionDocument(IResourceObject[] primaryData, IResourceObject[] relatedData, IMetadata metadata) + { + PrimaryData = primaryData; + RelatedData = relatedData; + Metadata = metadata; + } + } +} diff --git a/JSONAPI/Payload/IResourceIdentifier.cs b/JSONAPI/Documents/ResourceIdentifier.cs similarity index 58% rename from JSONAPI/Payload/IResourceIdentifier.cs rename to JSONAPI/Documents/ResourceIdentifier.cs index 58044491..5f894f1a 100644 --- a/JSONAPI/Payload/IResourceIdentifier.cs +++ b/JSONAPI/Documents/ResourceIdentifier.cs @@ -1,21 +1,5 @@ -namespace JSONAPI.Payload +namespace JSONAPI.Documents { - /// - /// Type/ID pair that identifies a particular resource - /// - public interface IResourceIdentifier - { - /// - /// The type of resource - /// - string Type { get; } - - /// - /// The ID of the resource - /// - string Id { get; } - } - /// /// Default implementation of IResourceIdentifier /// diff --git a/JSONAPI/Payload/ResourceObject.cs b/JSONAPI/Documents/ResourceObject.cs similarity index 97% rename from JSONAPI/Payload/ResourceObject.cs rename to JSONAPI/Documents/ResourceObject.cs index 37aa3878..e1ecd612 100644 --- a/JSONAPI/Payload/ResourceObject.cs +++ b/JSONAPI/Documents/ResourceObject.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Default implementation of IResourceObject diff --git a/JSONAPI/Documents/SingleResourceDocument.cs b/JSONAPI/Documents/SingleResourceDocument.cs new file mode 100644 index 00000000..01def3e0 --- /dev/null +++ b/JSONAPI/Documents/SingleResourceDocument.cs @@ -0,0 +1,24 @@ +namespace JSONAPI.Documents +{ + /// + /// Default implementation of ISingleResourceDocument + /// + public class SingleResourceDocument : ISingleResourceDocument + { + public IResourceObject PrimaryData { get; private set; } + + public IResourceObject[] RelatedData { get; private set; } + + public IMetadata Metadata { get; private set; } + + /// + /// Constructs a single resource document + /// + public SingleResourceDocument(IResourceObject primaryData, IResourceObject[] relatedData, IMetadata metadata) + { + PrimaryData = primaryData; + RelatedData = relatedData; + Metadata = metadata; + } + } +} diff --git a/JSONAPI/Payload/ToManyResourceLinkage.cs b/JSONAPI/Documents/ToManyResourceLinkage.cs similarity index 97% rename from JSONAPI/Payload/ToManyResourceLinkage.cs rename to JSONAPI/Documents/ToManyResourceLinkage.cs index fe41d5ef..fa696e98 100644 --- a/JSONAPI/Payload/ToManyResourceLinkage.cs +++ b/JSONAPI/Documents/ToManyResourceLinkage.cs @@ -1,7 +1,7 @@ using System; using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Describes linkage to a collection of resources diff --git a/JSONAPI/Payload/ToOneResourceLinkage.cs b/JSONAPI/Documents/ToOneResourceLinkage.cs similarity index 96% rename from JSONAPI/Payload/ToOneResourceLinkage.cs rename to JSONAPI/Documents/ToOneResourceLinkage.cs index 304b72e5..27f5c7a9 100644 --- a/JSONAPI/Payload/ToOneResourceLinkage.cs +++ b/JSONAPI/Documents/ToOneResourceLinkage.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace JSONAPI.Payload +namespace JSONAPI.Documents { /// /// Describes linkage to a single resource diff --git a/JSONAPI/Http/IPayloadMaterializer.cs b/JSONAPI/Http/IDocumentMaterializer.cs similarity index 55% rename from JSONAPI/Http/IPayloadMaterializer.cs rename to JSONAPI/Http/IDocumentMaterializer.cs index 81f83d9b..b0fbd6be 100644 --- a/JSONAPI/Http/IPayloadMaterializer.cs +++ b/JSONAPI/Http/IDocumentMaterializer.cs @@ -1,50 +1,50 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Http { /// /// This service provides the glue between JSONAPI.NET and your persistence layer. /// - public interface IPayloadMaterializer where T : class + public interface IDocumentMaterializer where T : class { /// - /// Returns a payload containing records that are filtered, sorted, + /// Returns a document containing records that are filtered, sorted, /// and paginated according to query parameters present in the provided request. /// - Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken); + Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken); /// - /// Returns a payload with the resource identified by the given ID. + /// Returns a document with the resource identified by the given ID. /// - Task GetRecordById(string id, HttpRequestMessage request, + Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken); /// /// Gets the resource(s) related to the resource identified by the given ID /// - Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, + Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken); /// - /// Creates a record corresponding to the data in the request payload, and returns a payload + /// Creates a record corresponding to the data in the request document, and returns a document /// corresponding to the created record. /// - Task CreateRecord(ISingleResourcePayload requestPayload, HttpRequestMessage request, + Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken); /// - /// Updates the record corresponding to the data in the request payload, and returns a payload + /// Updates the record corresponding to the data in the request document, and returns a document /// corresponding to the updated record. /// - Task UpdateRecord(string id, ISingleResourcePayload requestPayload, + Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken); /// /// Deletes the record corresponding to the given id. /// - Task DeleteRecord(string id, CancellationToken cancellationToken); + Task DeleteRecord(string id, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/JSONAPI/Http/JsonApiController.cs b/JSONAPI/Http/JsonApiController.cs index d44c6dfb..a69db870 100644 --- a/JSONAPI/Http/JsonApiController.cs +++ b/JSONAPI/Http/JsonApiController.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; using System.Web.Http; -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Http { @@ -11,41 +11,41 @@ namespace JSONAPI.Http /// public class JsonApiController : ApiController where T : class { - private readonly IPayloadMaterializer _payloadMaterializer; + private readonly IDocumentMaterializer _documentMaterializer; /// /// Creates a new ApiController /// - /// - public JsonApiController(IPayloadMaterializer payloadMaterializer) + /// + public JsonApiController(IDocumentMaterializer documentMaterializer) { - _payloadMaterializer = payloadMaterializer; + _documentMaterializer = documentMaterializer; } /// - /// Returns a payload corresponding to a set of records of this type. + /// Returns a document corresponding to a set of records of this type. /// /// public virtual async Task Get(CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.GetRecords(Request, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.GetRecords(Request, cancellationToken); + return Ok(document); } /// - /// Returns a payload corresponding to the single record matching the ID. + /// Returns a document corresponding to the single record matching the ID. /// /// /// /// public virtual async Task Get(string id, CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.GetRecordById(id, Request, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.GetRecordById(id, Request, cancellationToken); + return Ok(document); } /// - /// Returns a payload corresponding to the resource(s) related to the resource identified by the ID, + /// Returns a document corresponding to the resource(s) related to the resource identified by the ID, /// and the relationship name. /// /// @@ -54,34 +54,34 @@ public virtual async Task Get(string id, CancellationToken ca /// public virtual async Task GetRelatedResource(string id, string relationshipName, CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.GetRelated(id, relationshipName, Request, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.GetRelated(id, relationshipName, Request, cancellationToken); + return Ok(document); } /// - /// Creates a new record corresponding to the data in the request payload. + /// Creates a new record corresponding to the data in the request document. /// - /// + /// /// /// - public virtual async Task Post([FromBody]ISingleResourcePayload requestPayload, CancellationToken cancellationToken) + public virtual async Task Post([FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.CreateRecord(requestPayload, Request, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.CreateRecord(requestDocument, Request, cancellationToken); + return Ok(document); } /// /// Updates the record with the given ID with data from the request payloaad. /// /// - /// + /// /// /// - public virtual async Task Patch(string id, [FromBody]ISingleResourcePayload requestPayload, CancellationToken cancellationToken) + public virtual async Task Patch(string id, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.UpdateRecord(id, requestPayload, Request, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.UpdateRecord(id, requestDocument, Request, cancellationToken); + return Ok(document); } /// @@ -91,8 +91,8 @@ public virtual async Task Patch(string id, [FromBody]ISingleR /// public virtual async Task Delete(string id, CancellationToken cancellationToken) { - var payload = await _payloadMaterializer.DeleteRecord(id, cancellationToken); - return Ok(payload); + var document = await _documentMaterializer.DeleteRecord(id, cancellationToken); + return Ok(document); } } } diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 72292309..a9e4da6f 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -69,7 +69,7 @@ - + @@ -97,73 +97,72 @@ + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JSONAPI/Json/BasicMetadata.cs b/JSONAPI/Json/BasicMetadata.cs index a88c3b2e..12d5a29e 100644 --- a/JSONAPI/Json/BasicMetadata.cs +++ b/JSONAPI/Json/BasicMetadata.cs @@ -1,4 +1,4 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json.Linq; namespace JSONAPI.Json diff --git a/JSONAPI/Json/DeserializationException.cs b/JSONAPI/Json/DeserializationException.cs index 52cc75e7..f6b732d0 100644 --- a/JSONAPI/Json/DeserializationException.cs +++ b/JSONAPI/Json/DeserializationException.cs @@ -3,8 +3,8 @@ namespace JSONAPI.Json { /// - /// An exception that may be thrown by payload serializers during deserialization - /// in response to a JSON API-noncompliant payload being submitted by the client. + /// An exception that may be thrown by document formatters during deserialization + /// in response to a JSON API-noncompliant document being submitted by the client. /// public class DeserializationException : Exception { diff --git a/JSONAPI/Json/ErrorDocumentFormatter.cs b/JSONAPI/Json/ErrorDocumentFormatter.cs new file mode 100644 index 00000000..a5ef94f4 --- /dev/null +++ b/JSONAPI/Json/ErrorDocumentFormatter.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using JSONAPI.Documents; +using Newtonsoft.Json; + +namespace JSONAPI.Json +{ + /// + /// Default implementation of IErrorDocumentFormatter + /// + public class ErrorDocumentFormatter : IErrorDocumentFormatter + { + private readonly IErrorFormatter _errorFormatter; + private readonly IMetadataFormatter _metadataFormatter; + + /// + /// Creates a new ErrorDocumentFormatter + /// + /// + /// + public ErrorDocumentFormatter(IErrorFormatter errorFormatter, IMetadataFormatter metadataFormatter) + { + _errorFormatter = errorFormatter; + _metadataFormatter = metadataFormatter; + } + + public Task Serialize(IErrorDocument document, JsonWriter writer) + { + writer.WriteStartObject(); + writer.WritePropertyName("errors"); + writer.WriteStartArray(); + foreach (var error in document.Errors) + { + _errorFormatter.Serialize(error, writer); + } + writer.WriteEndArray(); + + if (document.Metadata != null) + { + writer.WritePropertyName("meta"); + _metadataFormatter.Serialize(document.Metadata, writer); + } + + writer.WriteEndObject(); + + return Task.FromResult(0); + } + + public Task Deserialize(JsonReader reader, string currentPath) + { + // The client should never be sending us errors. + throw new NotSupportedException(); + } + } +} diff --git a/JSONAPI/Json/ErrorSerializer.cs b/JSONAPI/Json/ErrorFormatter.cs similarity index 77% rename from JSONAPI/Json/ErrorSerializer.cs rename to JSONAPI/Json/ErrorFormatter.cs index 42f46fae..d1e33337 100644 --- a/JSONAPI/Json/ErrorSerializer.cs +++ b/JSONAPI/Json/ErrorFormatter.cs @@ -1,28 +1,28 @@ using System; using System.Net; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; namespace JSONAPI.Json { /// - /// Default implementation of IErrorSerializer + /// Default implementation of IErrorFormatter /// - public class ErrorSerializer : IErrorSerializer + public class ErrorFormatter : IErrorFormatter { - private readonly ILinkSerializer _linkSerializer; - private readonly IMetadataSerializer _metadataSerializer; + private readonly ILinkFormatter _linkFormatter; + private readonly IMetadataFormatter _metadataFormatter; /// - /// Creates a new ErrorSerializer + /// Creates a new errorFormatter /// - /// - /// - public ErrorSerializer(ILinkSerializer linkSerializer, IMetadataSerializer metadataSerializer) + /// + /// + public ErrorFormatter(ILinkFormatter linkFormatter, IMetadataFormatter metadataFormatter) { - _linkSerializer = linkSerializer; - _metadataSerializer = metadataSerializer; + _linkFormatter = linkFormatter; + _metadataFormatter = metadataFormatter; } public Task Serialize(IError error, JsonWriter writer) @@ -40,7 +40,7 @@ public Task Serialize(IError error, JsonWriter writer) writer.WritePropertyName("links"); writer.WriteStartObject(); writer.WritePropertyName("about"); - _linkSerializer.Serialize(error.AboutLink, writer); + _linkFormatter.Serialize(error.AboutLink, writer); writer.WriteEndObject(); } @@ -88,7 +88,7 @@ public Task Serialize(IError error, JsonWriter writer) if (error.Metadata != null) { writer.WritePropertyName("meta"); - error.Metadata.MetaObject.WriteTo(writer); + _metadataFormatter.Serialize(error.Metadata, writer); } writer.WriteEndObject(); diff --git a/JSONAPI/Json/ErrorPayloadSerializer.cs b/JSONAPI/Json/ErrorPayloadSerializer.cs deleted file mode 100644 index a9ec2fed..00000000 --- a/JSONAPI/Json/ErrorPayloadSerializer.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading.Tasks; -using JSONAPI.Payload; -using Newtonsoft.Json; - -namespace JSONAPI.Json -{ - /// - /// Default implementation of IErrorPayloadSerializer - /// - public class ErrorPayloadSerializer : IErrorPayloadSerializer - { - private readonly IErrorSerializer _errorSerializer; - private readonly IMetadataSerializer _metadataSerializer; - - /// - /// Creates a new ErrorPayloadSerializer - /// - /// - /// - public ErrorPayloadSerializer(IErrorSerializer errorSerializer, IMetadataSerializer metadataSerializer) - { - _errorSerializer = errorSerializer; - _metadataSerializer = metadataSerializer; - } - - public Task Serialize(IErrorPayload payload, JsonWriter writer) - { - writer.WriteStartObject(); - writer.WritePropertyName("errors"); - writer.WriteStartArray(); - foreach (var error in payload.Errors) - { - _errorSerializer.Serialize(error, writer); - } - writer.WriteEndArray(); - - if (payload.Metadata != null) - { - writer.WritePropertyName("meta"); - _metadataSerializer.Serialize(payload.Metadata, writer); - } - - writer.WriteEndObject(); - - return Task.FromResult(0); - } - - public Task Deserialize(JsonReader reader, string currentPath) - { - // The client should never be sending us errors. - throw new NotSupportedException(); - } - } -} diff --git a/JSONAPI/Json/IErrorDocumentFormatter.cs b/JSONAPI/Json/IErrorDocumentFormatter.cs new file mode 100644 index 00000000..01efeb71 --- /dev/null +++ b/JSONAPI/Json/IErrorDocumentFormatter.cs @@ -0,0 +1,11 @@ +using JSONAPI.Documents; + +namespace JSONAPI.Json +{ + /// + /// Service responsible for serializing IErrorDocument instances + /// + public interface IErrorDocumentFormatter : IJsonApiFormatter + { + } +} \ No newline at end of file diff --git a/JSONAPI/Json/IErrorSerializer.cs b/JSONAPI/Json/IErrorFormatter.cs similarity index 59% rename from JSONAPI/Json/IErrorSerializer.cs rename to JSONAPI/Json/IErrorFormatter.cs index 017b996f..9417b053 100644 --- a/JSONAPI/Json/IErrorSerializer.cs +++ b/JSONAPI/Json/IErrorFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing IError instances /// - public interface IErrorSerializer : IJsonApiSerializer + public interface IErrorFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/IErrorPayloadSerializer.cs b/JSONAPI/Json/IErrorPayloadSerializer.cs deleted file mode 100644 index 15997c18..00000000 --- a/JSONAPI/Json/IErrorPayloadSerializer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using JSONAPI.Payload; - -namespace JSONAPI.Json -{ - /// - /// Service responsible for serializing IErrorPayload instances - /// - public interface IErrorPayloadSerializer : IJsonApiSerializer - { - } -} \ No newline at end of file diff --git a/JSONAPI/Json/IJsonApiSerializer.cs b/JSONAPI/Json/IJsonApiFormatter.cs similarity index 86% rename from JSONAPI/Json/IJsonApiSerializer.cs rename to JSONAPI/Json/IJsonApiFormatter.cs index f22afbbc..4ddae298 100644 --- a/JSONAPI/Json/IJsonApiSerializer.cs +++ b/JSONAPI/Json/IJsonApiFormatter.cs @@ -4,10 +4,10 @@ namespace JSONAPI.Json { /// - /// Interface responsible for serializing JSON API components + /// Interface responsible for serializing and deserializing JSON API document components /// - /// The type of component this service can serialize - public interface IJsonApiSerializer + /// The type of component this service can format + public interface IJsonApiFormatter { /// /// Serializes the given component diff --git a/JSONAPI/Json/ILinkSerializer.cs b/JSONAPI/Json/ILinkFormatter.cs similarity index 59% rename from JSONAPI/Json/ILinkSerializer.cs rename to JSONAPI/Json/ILinkFormatter.cs index 0ea41e93..93a8e5c3 100644 --- a/JSONAPI/Json/ILinkSerializer.cs +++ b/JSONAPI/Json/ILinkFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing ILink instances /// - public interface ILinkSerializer : IJsonApiSerializer + public interface ILinkFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/IMetadataSerializer.cs b/JSONAPI/Json/IMetadataFormatter.cs similarity index 58% rename from JSONAPI/Json/IMetadataSerializer.cs rename to JSONAPI/Json/IMetadataFormatter.cs index 53707f7d..fe6d61c3 100644 --- a/JSONAPI/Json/IMetadataSerializer.cs +++ b/JSONAPI/Json/IMetadataFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing IMetadata instances /// - public interface IMetadataSerializer : IJsonApiSerializer + public interface IMetadataFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/IRelationshipObjectSerializer.cs b/JSONAPI/Json/IRelationshipObjectFormatter.cs similarity index 55% rename from JSONAPI/Json/IRelationshipObjectSerializer.cs rename to JSONAPI/Json/IRelationshipObjectFormatter.cs index 8ef8ec7f..8762a1d9 100644 --- a/JSONAPI/Json/IRelationshipObjectSerializer.cs +++ b/JSONAPI/Json/IRelationshipObjectFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing IRelationshipObject instances /// - public interface IRelationshipObjectSerializer : IJsonApiSerializer + public interface IRelationshipObjectFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/IResourceCollectionDocumentFormatter.cs b/JSONAPI/Json/IResourceCollectionDocumentFormatter.cs new file mode 100644 index 00000000..03a97970 --- /dev/null +++ b/JSONAPI/Json/IResourceCollectionDocumentFormatter.cs @@ -0,0 +1,11 @@ +using JSONAPI.Documents; + +namespace JSONAPI.Json +{ + /// + /// Service responsible for formatting IResourceCollectionDocument instances + /// + public interface IResourceCollectionDocumentFormatter : IJsonApiFormatter + { + } +} \ No newline at end of file diff --git a/JSONAPI/Json/IResourceCollectionPayloadSerializer.cs b/JSONAPI/Json/IResourceCollectionPayloadSerializer.cs deleted file mode 100644 index d488be4f..00000000 --- a/JSONAPI/Json/IResourceCollectionPayloadSerializer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using JSONAPI.Payload; - -namespace JSONAPI.Json -{ - /// - /// Service responsible for serializing ISingleResourcePayload instances - /// - public interface IResourceCollectionPayloadSerializer : IJsonApiSerializer - { - } -} \ No newline at end of file diff --git a/JSONAPI/Json/IResourceLinkageSerializer.cs b/JSONAPI/Json/IResourceLinkageFormatter.cs similarity index 57% rename from JSONAPI/Json/IResourceLinkageSerializer.cs rename to JSONAPI/Json/IResourceLinkageFormatter.cs index 0532b18d..eb4d0d6b 100644 --- a/JSONAPI/Json/IResourceLinkageSerializer.cs +++ b/JSONAPI/Json/IResourceLinkageFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing IResourceLinkage instances /// - public interface IResourceLinkageSerializer : IJsonApiSerializer + public interface IResourceLinkageFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/IResourceObjectSerializer.cs b/JSONAPI/Json/IResourceObjectFormatter.cs similarity index 57% rename from JSONAPI/Json/IResourceObjectSerializer.cs rename to JSONAPI/Json/IResourceObjectFormatter.cs index 26955c5b..c3be78f0 100644 --- a/JSONAPI/Json/IResourceObjectSerializer.cs +++ b/JSONAPI/Json/IResourceObjectFormatter.cs @@ -1,11 +1,11 @@ -using JSONAPI.Payload; +using JSONAPI.Documents; namespace JSONAPI.Json { /// /// Service responsible for serializing IResourceObject instances /// - public interface IResourceObjectSerializer : IJsonApiSerializer + public interface IResourceObjectFormatter : IJsonApiFormatter { } } \ No newline at end of file diff --git a/JSONAPI/Json/ISingleResourceDocumentFormatter.cs b/JSONAPI/Json/ISingleResourceDocumentFormatter.cs new file mode 100644 index 00000000..0d9174b8 --- /dev/null +++ b/JSONAPI/Json/ISingleResourceDocumentFormatter.cs @@ -0,0 +1,11 @@ +using JSONAPI.Documents; + +namespace JSONAPI.Json +{ + /// + /// Service responsible for formatting ISingleResourceDocument instances + /// + public interface ISingleResourceDocumentFormatter : IJsonApiFormatter + { + } +} \ No newline at end of file diff --git a/JSONAPI/Json/ISingleResourcePayloadSerializer.cs b/JSONAPI/Json/ISingleResourcePayloadSerializer.cs deleted file mode 100644 index 2d9ccdb7..00000000 --- a/JSONAPI/Json/ISingleResourcePayloadSerializer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using JSONAPI.Payload; - -namespace JSONAPI.Json -{ - /// - /// Service responsible for serializing ISingleResourcePayload instances - /// - public interface ISingleResourcePayloadSerializer : IJsonApiSerializer - { - } -} \ No newline at end of file diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 2e181dce..67d7c1f2 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -7,8 +7,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using System.Web.Http; -using JSONAPI.Payload; -using JSONAPI.Payload.Builders; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; using Newtonsoft.Json; namespace JSONAPI.Json @@ -18,23 +18,23 @@ namespace JSONAPI.Json /// public class JsonApiFormatter : JsonMediaTypeFormatter { - private readonly ISingleResourcePayloadSerializer _singleResourcePayloadSerializer; - private readonly IResourceCollectionPayloadSerializer _resourceCollectionPayloadSerializer; - private readonly IErrorPayloadSerializer _errorPayloadSerializer; - private readonly IErrorPayloadBuilder _errorPayloadBuilder; + private readonly ISingleResourceDocumentFormatter _singleResourceDocumentFormatter; + private readonly IResourceCollectionDocumentFormatter _resourceCollectionDocumentFormatter; + private readonly IErrorDocumentFormatter _errorDocumentFormatter; + private readonly IErrorDocumentBuilder _errorDocumentBuilder; /// /// Creates a new JsonApiFormatter /// - public JsonApiFormatter(ISingleResourcePayloadSerializer singleResourcePayloadSerializer, - IResourceCollectionPayloadSerializer resourceCollectionPayloadSerializer, - IErrorPayloadSerializer errorPayloadSerializer, - IErrorPayloadBuilder errorPayloadBuilder) + public JsonApiFormatter(ISingleResourceDocumentFormatter singleResourceDocumentFormatter, + IResourceCollectionDocumentFormatter resourceCollectionDocumentFormatter, + IErrorDocumentFormatter errorDocumentFormatter, + IErrorDocumentBuilder errorDocumentBuilder) { - _singleResourcePayloadSerializer = singleResourcePayloadSerializer; - _resourceCollectionPayloadSerializer = resourceCollectionPayloadSerializer; - _errorPayloadSerializer = errorPayloadSerializer; - _errorPayloadBuilder = errorPayloadBuilder; + _singleResourceDocumentFormatter = singleResourceDocumentFormatter; + _resourceCollectionDocumentFormatter = resourceCollectionDocumentFormatter; + _errorDocumentFormatter = errorDocumentFormatter; + _errorDocumentBuilder = errorDocumentBuilder; SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json")); @@ -52,35 +52,35 @@ public override bool CanWriteType(Type t) public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { - if (type == typeof(IJsonApiPayload) && value == null) + if (type == typeof(IJsonApiDocument) && value == null) return Task.FromResult(0); var contentHeaders = content == null ? null : content.Headers; var effectiveEncoding = SelectCharacterEncoding(contentHeaders); var writer = CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); - var singleResourcePayload = value as ISingleResourcePayload; - var resourceCollectionPayload = value as IResourceCollectionPayload; - var errorPayload = value as IErrorPayload; - if (singleResourcePayload != null) + var singleResourceDocument = value as ISingleResourceDocument; + var resourceCollectionDocument = value as IResourceCollectionDocument; + var errorDocument = value as IErrorDocument; + if (singleResourceDocument != null) { - _singleResourcePayloadSerializer.Serialize(singleResourcePayload, writer); + _singleResourceDocumentFormatter.Serialize(singleResourceDocument, writer); } - else if (resourceCollectionPayload != null) + else if (resourceCollectionDocument != null) { - _resourceCollectionPayloadSerializer.Serialize(resourceCollectionPayload, writer); + _resourceCollectionDocumentFormatter.Serialize(resourceCollectionDocument, writer); } - else if (errorPayload != null) + else if (errorDocument != null) { - _errorPayloadSerializer.Serialize(errorPayload, writer); + _errorDocumentFormatter.Serialize(errorDocument, writer); } else { var error = value as HttpError; if (error != null) { - var httpErrorPayload = _errorPayloadBuilder.BuildFromHttpError(error, HttpStatusCode.InternalServerError); - _errorPayloadSerializer.Serialize(httpErrorPayload, writer); + var httpErrorDocument = _errorDocumentBuilder.BuildFromHttpError(error, HttpStatusCode.InternalServerError); + _errorDocumentFormatter.Serialize(httpErrorDocument, writer); } else { @@ -103,10 +103,10 @@ public override async Task ReadFromStreamAsync(Type type, Stream readStr reader.Read(); - if (typeof(ISingleResourcePayload).IsAssignableFrom(type)) - return await _singleResourcePayloadSerializer.Deserialize(reader, ""); - if (typeof(IResourceCollectionPayload).IsAssignableFrom(type)) - return await _resourceCollectionPayloadSerializer.Deserialize(reader, ""); + if (typeof(ISingleResourceDocument).IsAssignableFrom(type)) + return await _singleResourceDocumentFormatter.Deserialize(reader, ""); + if (typeof(IResourceCollectionDocument).IsAssignableFrom(type)) + return await _resourceCollectionDocumentFormatter.Deserialize(reader, ""); throw new Exception(string.Format("The type {0} is not supported for deserialization.", type.Name)); } diff --git a/JSONAPI/Json/LinkSerializer.cs b/JSONAPI/Json/LinkFormatter.cs similarity index 68% rename from JSONAPI/Json/LinkSerializer.cs rename to JSONAPI/Json/LinkFormatter.cs index 8b3a8db9..7c88dae3 100644 --- a/JSONAPI/Json/LinkSerializer.cs +++ b/JSONAPI/Json/LinkFormatter.cs @@ -1,26 +1,26 @@ using System; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; namespace JSONAPI.Json { /// - /// Default implementation of ILinkSerializer + /// Default implementation of ILinkFormatter /// - public class LinkSerializer : ILinkSerializer + public class LinkFormatter : ILinkFormatter { - private readonly IMetadataSerializer _metadataSerializer; + private readonly IMetadataFormatter _metadataFormatter; private const string HrefKeyName = "href"; private const string MetaKeyName = "meta"; /// - /// Constructs a LinkSerializer + /// Constructs a LinkFormatter /// - /// - public LinkSerializer(IMetadataSerializer metadataSerializer) + /// + public LinkFormatter(IMetadataFormatter metadataFormatter) { - _metadataSerializer = metadataSerializer; + _metadataFormatter = metadataFormatter; } public Task Serialize(ILink link, JsonWriter writer) @@ -35,7 +35,7 @@ public Task Serialize(ILink link, JsonWriter writer) writer.WritePropertyName(HrefKeyName); writer.WriteValue(link.Href); writer.WritePropertyName(MetaKeyName); - _metadataSerializer.Serialize(link.Metadata, writer); + _metadataFormatter.Serialize(link.Metadata, writer); writer.WriteEndObject(); } return Task.FromResult(0); diff --git a/JSONAPI/Json/MetadataSerializer.cs b/JSONAPI/Json/MetadataFormatter.cs similarity index 90% rename from JSONAPI/Json/MetadataSerializer.cs rename to JSONAPI/Json/MetadataFormatter.cs index 9183ffd6..a693fdf7 100644 --- a/JSONAPI/Json/MetadataSerializer.cs +++ b/JSONAPI/Json/MetadataFormatter.cs @@ -1,14 +1,14 @@ using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JSONAPI.Json { /// - /// Default implementation of IMetadataSerializer + /// Default implementation of IMetadataFormatter /// - public class MetadataSerializer : IMetadataSerializer + public class MetadataFormatter : IMetadataFormatter { public Task Serialize(IMetadata metadata, JsonWriter writer) { diff --git a/JSONAPI/Json/RelationAggregator.cs b/JSONAPI/Json/RelationAggregator.cs deleted file mode 100644 index 1317124e..00000000 --- a/JSONAPI/Json/RelationAggregator.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace JSONAPI.Json -{ - public class RelationAggregator - { - private Type itemType = null; - private ISet rootItems = null; - internal Dictionary> Appendices; - - public RelationAggregator() - { - this.Appendices = new Dictionary>(); - } - - public void AddPrimary(Type type, IEnumerable items) - { - if (itemType == null) itemType = type; - if (rootItems == null) - rootItems = new HashSet(); - rootItems.UnionWith(items); - } - public void AddPrimary(Type type, object item) - { - if (itemType == null) itemType = type; - if (rootItems == null) - rootItems = new HashSet(); - rootItems.Add(item); - } - - public void Add(Type type, IEnumerable items) - { - // Exclude items that are already included in the root! - items = items.Except(this.rootItems); - if (items.Count() <= 0) return; - - if (!this.Appendices.ContainsKey(type)) - //TODO: Can we make a strongly-typed collection here somehow, since we know the type? - this.Appendices[type] = new HashSet(); - - this.Appendices[type].UnionWith(items); - /* Assuming the above is faster than this... - foreach (object item in items) - { - currentPayload.Appendices[prop.GetType()].Add(item); - } - */ - } - - public void Add(Type type, object item) - { - // Exclude items that are already included in the root! - if (item.GetType() == itemType) - { - if (this.rootItems.Contains(item)) return; - } - - if (!this.Appendices.ContainsKey(type)) - this.Appendices[type] = new HashSet(); - - this.Appendices[type].Add(item); - } - } -} diff --git a/JSONAPI/Json/RelationshipObjectSerializer.cs b/JSONAPI/Json/RelationshipObjectFormatter.cs similarity index 68% rename from JSONAPI/Json/RelationshipObjectSerializer.cs rename to JSONAPI/Json/RelationshipObjectFormatter.cs index 5d0aad09..ce81647f 100644 --- a/JSONAPI/Json/RelationshipObjectSerializer.cs +++ b/JSONAPI/Json/RelationshipObjectFormatter.cs @@ -1,14 +1,14 @@ using System; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; namespace JSONAPI.Json { /// - /// Default implementation of IRelationshipObjectSerializer + /// Default implementation of IRelationshipObjectFormatter /// - public class RelationshipObjectSerializer : IRelationshipObjectSerializer + public class RelationshipObjectFormatter : IRelationshipObjectFormatter { private const string LinksKeyName = "links"; private const string SelfLinkKeyName = "self"; @@ -16,18 +16,18 @@ public class RelationshipObjectSerializer : IRelationshipObjectSerializer private const string LinkageKeyName = "data"; private const string MetaKeyName = "meta"; - private readonly ILinkSerializer _linkSerializer; - private readonly IResourceLinkageSerializer _resourceLinkageSerializer; - private readonly IMetadataSerializer _metadataSerializer; + private readonly ILinkFormatter _linkFormatter; + private readonly IResourceLinkageFormatter _resourceLinkageFormatter; + private readonly IMetadataFormatter _metadataFormatter; /// - /// Creates a new RelationshipObjectSerializer + /// Creates a new RelationshipObjectFormatter /// - public RelationshipObjectSerializer(ILinkSerializer linkSerializer, IResourceLinkageSerializer resourceLinkageSerializer, IMetadataSerializer metadataSerializer) + public RelationshipObjectFormatter(ILinkFormatter linkFormatter, IResourceLinkageFormatter resourceLinkageFormatter, IMetadataFormatter metadataFormatter) { - _linkSerializer = linkSerializer; - _resourceLinkageSerializer = resourceLinkageSerializer; - _metadataSerializer = metadataSerializer; + _linkFormatter = linkFormatter; + _resourceLinkageFormatter = resourceLinkageFormatter; + _metadataFormatter = metadataFormatter; } public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) @@ -48,12 +48,12 @@ public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) if (relationshipObject.SelfLink != null) { writer.WritePropertyName(SelfLinkKeyName); - _linkSerializer.Serialize(relationshipObject.SelfLink, writer); + _linkFormatter.Serialize(relationshipObject.SelfLink, writer); } if (relationshipObject.RelatedResourceLink != null) { writer.WritePropertyName(RelatedLinkKeyName); - _linkSerializer.Serialize(relationshipObject.RelatedResourceLink, writer); + _linkFormatter.Serialize(relationshipObject.RelatedResourceLink, writer); } writer.WriteEndObject(); @@ -62,13 +62,13 @@ public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) if (relationshipObject.Linkage != null) { writer.WritePropertyName(LinkageKeyName); - _resourceLinkageSerializer.Serialize(relationshipObject.Linkage, writer); + _resourceLinkageFormatter.Serialize(relationshipObject.Linkage, writer); } if (relationshipObject.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataSerializer.Serialize(relationshipObject.Metadata, writer); + _metadataFormatter.Serialize(relationshipObject.Metadata, writer); } writer.WriteEndObject(); @@ -94,10 +94,10 @@ public async Task Deserialize(JsonReader reader, string cur switch (propertyName) { case LinkageKeyName: - linkage = await _resourceLinkageSerializer.Deserialize(reader, currentPath + "/" + LinkageKeyName); + linkage = await _resourceLinkageFormatter.Deserialize(reader, currentPath + "/" + LinkageKeyName); break; case MetaKeyName: - metadata = await _metadataSerializer.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; } } diff --git a/JSONAPI/Json/ResourceCollectionPayloadSerializer.cs b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs similarity index 61% rename from JSONAPI/Json/ResourceCollectionPayloadSerializer.cs rename to JSONAPI/Json/ResourceCollectionDocumentFormatter.cs index 76ddf544..db2d40e9 100644 --- a/JSONAPI/Json/ResourceCollectionPayloadSerializer.cs +++ b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs @@ -1,61 +1,61 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; namespace JSONAPI.Json { /// - /// Default implementation of IResourceCollectionPayloadSerializer + /// Default implementation of IResourceCollectionDocumentFormatter /// - public class ResourceCollectionPayloadSerializer : IResourceCollectionPayloadSerializer + public class ResourceCollectionDocumentFormatter : IResourceCollectionDocumentFormatter { - private readonly IResourceObjectSerializer _resourceObjectSerializer; - private readonly IMetadataSerializer _metadataSerializer; + private readonly IResourceObjectFormatter _resourceObjectFormatter; + private readonly IMetadataFormatter _metadataFormatter; private const string PrimaryDataKeyName = "data"; private const string RelatedDataKeyName = "included"; private const string MetaKeyName = "meta"; /// - /// Creates a SingleResourcePayloadSerializer + /// Creates a SingleResourceDocumentFormatter /// - /// - /// - public ResourceCollectionPayloadSerializer(IResourceObjectSerializer resourceObjectSerializer, IMetadataSerializer metadataSerializer) + /// + /// + public ResourceCollectionDocumentFormatter(IResourceObjectFormatter resourceObjectFormatter, IMetadataFormatter metadataFormatter) { - _resourceObjectSerializer = resourceObjectSerializer; - _metadataSerializer = metadataSerializer; + _resourceObjectFormatter = resourceObjectFormatter; + _metadataFormatter = metadataFormatter; } - public Task Serialize(IResourceCollectionPayload payload, JsonWriter writer) + public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) { writer.WriteStartObject(); writer.WritePropertyName(PrimaryDataKeyName); writer.WriteStartArray(); - foreach (var resourceObject in payload.PrimaryData) + foreach (var resourceObject in document.PrimaryData) { - _resourceObjectSerializer.Serialize(resourceObject, writer); + _resourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); - if (payload.RelatedData != null && payload.RelatedData.Any()) + if (document.RelatedData != null && document.RelatedData.Any()) { writer.WritePropertyName(RelatedDataKeyName); writer.WriteStartArray(); - foreach (var resourceObject in payload.RelatedData) + foreach (var resourceObject in document.RelatedData) { - _resourceObjectSerializer.Serialize(resourceObject, writer); + _resourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); } - if (payload.Metadata != null) + if (document.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataSerializer.Serialize(payload.Metadata, writer); + _metadataFormatter.Serialize(document.Metadata, writer); } writer.WriteEndObject(); @@ -65,7 +65,7 @@ public Task Serialize(IResourceCollectionPayload payload, JsonWriter writer) return Task.FromResult(0); } - public async Task Deserialize(JsonReader reader, string currentPath) + public async Task Deserialize(JsonReader reader, string currentPath) { if (reader.TokenType != JsonToken.StartObject) throw new JsonSerializationException("Document root is not an object!"); @@ -91,7 +91,7 @@ public async Task Deserialize(JsonReader reader, str primaryData = await DeserializePrimaryData(reader, currentPath + "/" + PrimaryDataKeyName); break; case MetaKeyName: - metadata = await _metadataSerializer.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; default: reader.Skip(); @@ -99,7 +99,7 @@ public async Task Deserialize(JsonReader reader, str } } - return new ResourceCollectionPayload(primaryData ?? new IResourceObject[] { }, new IResourceObject[] { }, metadata); + return new ResourceCollectionDocument(primaryData ?? new IResourceObject[] { }, new IResourceObject[] { }, metadata); } private async Task DeserializePrimaryData(JsonReader reader, string currentPath) @@ -115,7 +115,7 @@ private async Task DeserializePrimaryData(JsonReader reader, if (reader.TokenType == JsonToken.EndArray) break; - var resourceObject = await _resourceObjectSerializer.Deserialize(reader, currentPath + "/" + index); + var resourceObject = await _resourceObjectFormatter.Deserialize(reader, currentPath + "/" + index); primaryData.Add(resourceObject); index++; diff --git a/JSONAPI/Json/ResourceLinkageSerializer.cs b/JSONAPI/Json/ResourceLinkageFormatter.cs similarity index 95% rename from JSONAPI/Json/ResourceLinkageSerializer.cs rename to JSONAPI/Json/ResourceLinkageFormatter.cs index df58c6d9..07c4e9f4 100644 --- a/JSONAPI/Json/ResourceLinkageSerializer.cs +++ b/JSONAPI/Json/ResourceLinkageFormatter.cs @@ -1,15 +1,15 @@ using System.Linq; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JSONAPI.Json { /// - /// Default implementation of IResourceLinkageSerializer + /// Default implementation of IResourceLinkageFormatter /// - public class ResourceLinkageSerializer : IResourceLinkageSerializer + public class ResourceLinkageFormatter : IResourceLinkageFormatter { public Task Serialize(IResourceLinkage linkage, JsonWriter writer) { diff --git a/JSONAPI/Json/ResourceObjectSerializer.cs b/JSONAPI/Json/ResourceObjectFormatter.cs similarity index 81% rename from JSONAPI/Json/ResourceObjectSerializer.cs rename to JSONAPI/Json/ResourceObjectFormatter.cs index 385ecf00..6ad50171 100644 --- a/JSONAPI/Json/ResourceObjectSerializer.cs +++ b/JSONAPI/Json/ResourceObjectFormatter.cs @@ -1,20 +1,20 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JSONAPI.Json { /// - /// Default implementation of IResourceObjectSerializer + /// Default implementation of IResourceObjectFormatter /// - public class ResourceObjectSerializer : IResourceObjectSerializer + public class ResourceObjectFormatter : IResourceObjectFormatter { - private readonly IRelationshipObjectSerializer _relationshipObjectSerializer; - private readonly ILinkSerializer _linkSerializer; - private readonly IMetadataSerializer _metadataSerializer; + private readonly IRelationshipObjectFormatter _relationshipObjectFormatter; + private readonly ILinkFormatter _linkFormatter; + private readonly IMetadataFormatter _metadataFormatter; private const string TypeKeyName = "type"; private const string IdKeyName = "id"; private const string AttributesKeyName = "attributes"; @@ -24,16 +24,16 @@ public class ResourceObjectSerializer : IResourceObjectSerializer private const string SelfLinkKeyName = "self"; /// - /// Constructs a new ResourceObjectSerializer + /// Constructs a new resourceObjectFormatter /// - /// The serializer to use for relationship objects - /// The serializer to use for links - /// The serializer to use for metadata - public ResourceObjectSerializer(IRelationshipObjectSerializer relationshipObjectSerializer, ILinkSerializer linkSerializer, IMetadataSerializer metadataSerializer) + /// The formatter to use for relationship objects + /// The formatter to use for links + /// The formatter to use for metadata + public ResourceObjectFormatter(IRelationshipObjectFormatter relationshipObjectFormatter, ILinkFormatter linkFormatter, IMetadataFormatter metadataFormatter) { - _relationshipObjectSerializer = relationshipObjectSerializer; - _linkSerializer = linkSerializer; - _metadataSerializer = metadataSerializer; + _relationshipObjectFormatter = relationshipObjectFormatter; + _linkFormatter = linkFormatter; + _metadataFormatter = metadataFormatter; } public Task Serialize(IResourceObject resourceObject, JsonWriter writer) @@ -82,7 +82,7 @@ public Task Serialize(IResourceObject resourceObject, JsonWriter writer) { if (relationship.Value == null) continue; writer.WritePropertyName(relationship.Key); - _relationshipObjectSerializer.Serialize(relationship.Value, writer); + _relationshipObjectFormatter.Serialize(relationship.Value, writer); } writer.WriteEndObject(); } @@ -93,14 +93,14 @@ public Task Serialize(IResourceObject resourceObject, JsonWriter writer) writer.WritePropertyName(LinksKeyName); writer.WriteStartObject(); writer.WritePropertyName(SelfLinkKeyName); - _linkSerializer.Serialize(resourceObject.SelfLink, writer); + _linkFormatter.Serialize(resourceObject.SelfLink, writer); writer.WriteEndObject(); } if (resourceObject.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataSerializer.Serialize(resourceObject.Metadata, writer); + _metadataFormatter.Serialize(resourceObject.Metadata, writer); } writer.WriteEndObject(); @@ -136,7 +136,7 @@ public async Task Deserialize(JsonReader reader, string current id = (string) reader.Value; break; case MetaKeyName: - metadata = await _metadataSerializer.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; case AttributesKeyName: attributes = DeserializeAttributes(reader, currentPath + "/" + AttributesKeyName); @@ -196,7 +196,7 @@ private async Task> DeserializeRelation var relationshipName = (string)reader.Value; reader.Read(); - var relationship = await _relationshipObjectSerializer.Deserialize(reader, currentPath + "/" + relationshipName); + var relationship = await _relationshipObjectFormatter.Deserialize(reader, currentPath + "/" + relationshipName); relationships.Add(relationshipName, relationship); } diff --git a/JSONAPI/Json/SingleResourcePayloadSerializer.cs b/JSONAPI/Json/SingleResourceDocumentFormatter.cs similarity index 59% rename from JSONAPI/Json/SingleResourcePayloadSerializer.cs rename to JSONAPI/Json/SingleResourceDocumentFormatter.cs index 619402b4..e0414c11 100644 --- a/JSONAPI/Json/SingleResourcePayloadSerializer.cs +++ b/JSONAPI/Json/SingleResourceDocumentFormatter.cs @@ -1,55 +1,55 @@ using System.Linq; using System.Threading.Tasks; -using JSONAPI.Payload; +using JSONAPI.Documents; using Newtonsoft.Json; namespace JSONAPI.Json { /// - /// Default implementation of IPayloadSerializer + /// Default implementation of ISingleResourceDocumentFormatter /// - public class SingleResourcePayloadSerializer : ISingleResourcePayloadSerializer + public class SingleResourceDocumentFormatter : ISingleResourceDocumentFormatter { - private readonly IResourceObjectSerializer _resourceObjectSerializer; - private readonly IMetadataSerializer _metadataSerializer; + private readonly IResourceObjectFormatter _resourceObjectFormatter; + private readonly IMetadataFormatter _metadataFormatter; private const string PrimaryDataKeyName = "data"; private const string RelatedDataKeyName = "included"; private const string MetaKeyName = "meta"; /// - /// Creates a SingleResourcePayloadSerializer + /// Creates a SingleResourceDocumentFormatter /// - /// - /// - public SingleResourcePayloadSerializer(IResourceObjectSerializer resourceObjectSerializer, IMetadataSerializer metadataSerializer) + /// + /// + public SingleResourceDocumentFormatter(IResourceObjectFormatter resourceObjectFormatter, IMetadataFormatter metadataFormatter) { - _resourceObjectSerializer = resourceObjectSerializer; - _metadataSerializer = metadataSerializer; + _resourceObjectFormatter = resourceObjectFormatter; + _metadataFormatter = metadataFormatter; } - public Task Serialize(ISingleResourcePayload payload, JsonWriter writer) + public Task Serialize(ISingleResourceDocument document, JsonWriter writer) { writer.WriteStartObject(); writer.WritePropertyName(PrimaryDataKeyName); - _resourceObjectSerializer.Serialize(payload.PrimaryData, writer); + _resourceObjectFormatter.Serialize(document.PrimaryData, writer); - if (payload.RelatedData != null && payload.RelatedData.Any()) + if (document.RelatedData != null && document.RelatedData.Any()) { writer.WritePropertyName(RelatedDataKeyName); writer.WriteStartArray(); - foreach (var resourceObject in payload.RelatedData) + foreach (var resourceObject in document.RelatedData) { - _resourceObjectSerializer.Serialize(resourceObject, writer); + _resourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); } - if (payload.Metadata != null) + if (document.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataSerializer.Serialize(payload.Metadata, writer); + _metadataFormatter.Serialize(document.Metadata, writer); } writer.WriteEndObject(); @@ -57,7 +57,7 @@ public Task Serialize(ISingleResourcePayload payload, JsonWriter writer) return Task.FromResult(0); } - public async Task Deserialize(JsonReader reader, string currentPath) + public async Task Deserialize(JsonReader reader, string currentPath) { if (reader.TokenType != JsonToken.StartObject) throw new DeserializationException("Invalid document root", "Document root is not an object!", currentPath); @@ -83,7 +83,7 @@ public async Task Deserialize(JsonReader reader, string primaryData = await DeserializePrimaryData(reader, currentPath + "/" + PrimaryDataKeyName); break; case MetaKeyName: - metadata = await _metadataSerializer.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; default: reader.Skip(); @@ -91,14 +91,14 @@ public async Task Deserialize(JsonReader reader, string } } - return new SingleResourcePayload(primaryData, new IResourceObject[] { }, metadata); + return new SingleResourceDocument(primaryData, new IResourceObject[] { }, metadata); } private async Task DeserializePrimaryData(JsonReader reader, string currentPath) { if (reader.TokenType == JsonToken.Null) return null; - var primaryData = await _resourceObjectSerializer.Deserialize(reader, currentPath); + var primaryData = await _resourceObjectFormatter.Deserialize(reader, currentPath); return primaryData; } } diff --git a/JSONAPI/Payload/Builders/FallbackPayloadBuilder.cs b/JSONAPI/Payload/Builders/FallbackPayloadBuilder.cs deleted file mode 100644 index b73a49c5..00000000 --- a/JSONAPI/Payload/Builders/FallbackPayloadBuilder.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using JSONAPI.Http; - -namespace JSONAPI.Payload.Builders -{ - /// - /// Default implementation of IFallbackPayloadBuilder - /// - public class FallbackPayloadBuilder : IFallbackPayloadBuilder - { - private readonly ISingleResourcePayloadBuilder _singleResourcePayloadBuilder; - private readonly IQueryableResourceCollectionPayloadBuilder _queryableResourceCollectionPayloadBuilder; - private readonly IResourceCollectionPayloadBuilder _resourceCollectionPayloadBuilder; - private readonly IBaseUrlService _baseUrlService; - private readonly Lazy _openBuildPayloadFromQueryableMethod; - private readonly Lazy _openBuildPayloadFromEnumerableMethod; - - /// - /// Creates a new FallbackPayloadBuilder - /// - /// - /// - /// - /// - public FallbackPayloadBuilder(ISingleResourcePayloadBuilder singleResourcePayloadBuilder, - IQueryableResourceCollectionPayloadBuilder queryableResourceCollectionPayloadBuilder, - IResourceCollectionPayloadBuilder resourceCollectionPayloadBuilder, - IBaseUrlService baseUrlService) - { - _singleResourcePayloadBuilder = singleResourcePayloadBuilder; - _queryableResourceCollectionPayloadBuilder = queryableResourceCollectionPayloadBuilder; - _resourceCollectionPayloadBuilder = resourceCollectionPayloadBuilder; - _baseUrlService = baseUrlService; - - _openBuildPayloadFromQueryableMethod = - new Lazy( - () => _queryableResourceCollectionPayloadBuilder.GetType() - .GetMethod("BuildPayload", BindingFlags.Instance | BindingFlags.Public)); - - _openBuildPayloadFromEnumerableMethod = - new Lazy( - () => _resourceCollectionPayloadBuilder.GetType() - .GetMethod("BuildPayload", BindingFlags.Instance | BindingFlags.Public)); - } - - public async Task BuildPayload(object obj, HttpRequestMessage requestMessage, - CancellationToken cancellationToken) - { - var type = obj.GetType(); - - var queryableInterfaces = type.GetInterfaces(); - var queryableInterface = - queryableInterfaces.FirstOrDefault( - i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IQueryable<>)); - if (queryableInterface != null) - { - var queryableElementType = queryableInterface.GenericTypeArguments[0]; - var buildPayloadMethod = - _openBuildPayloadFromQueryableMethod.Value.MakeGenericMethod(queryableElementType); - - dynamic materializedQueryTask = buildPayloadMethod.Invoke(_queryableResourceCollectionPayloadBuilder, - new[] {obj, requestMessage, cancellationToken}); - - return await materializedQueryTask; - } - - var isCollection = false; - var enumerableElementType = GetEnumerableElementType(type); - if (enumerableElementType != null) - { - isCollection = true; - } - - var linkBaseUrl = _baseUrlService.GetBaseUrl(requestMessage); - - if (isCollection) - { - var buildPayloadMethod = - _openBuildPayloadFromEnumerableMethod.Value.MakeGenericMethod(enumerableElementType); - return - (dynamic)buildPayloadMethod.Invoke(_resourceCollectionPayloadBuilder, new[] { obj, linkBaseUrl, new string[] { }, null }); - } - - // Single resource object - return _singleResourcePayloadBuilder.BuildPayload(obj, linkBaseUrl, null); - } - - private static Type GetEnumerableElementType(Type collectionType) - { - if (collectionType.IsArray) - return collectionType.GetElementType(); - - if (collectionType.IsGenericType && collectionType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - return collectionType.GetGenericArguments()[0]; - } - - var enumerableInterface = collectionType.GetInterface(typeof(IEnumerable<>).FullName); - if (enumerableInterface == null) return null; - - var genericArguments = collectionType.GetGenericArguments(); - if (!genericArguments.Any()) return null; - - return genericArguments[0]; - } - } -} diff --git a/JSONAPI/Payload/Builders/IErrorPayloadBuilder.cs b/JSONAPI/Payload/Builders/IErrorPayloadBuilder.cs deleted file mode 100644 index c1da50e9..00000000 --- a/JSONAPI/Payload/Builders/IErrorPayloadBuilder.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Net; -using System.Web.Http; - -namespace JSONAPI.Payload.Builders -{ - /// - /// Provides services for building an error payload - /// - public interface IErrorPayloadBuilder - { - /// - /// Builds an error payload based on an exception - /// - /// - /// - IErrorPayload BuildFromException(Exception exception); - - /// - /// Builds an error payload based on an HttpError - /// - /// - /// - /// - IErrorPayload BuildFromHttpError(HttpError httpError, HttpStatusCode statusCode); - } -} \ No newline at end of file diff --git a/JSONAPI/Payload/IErrorPayload.cs b/JSONAPI/Payload/IErrorPayload.cs deleted file mode 100644 index c5b40ecf..00000000 --- a/JSONAPI/Payload/IErrorPayload.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JSONAPI.Payload -{ - /// - /// Interface for JSON API payloads that represent a collection of errors - /// - public interface IErrorPayload : IJsonApiPayload - { - /// - /// The errors to send in this payload - /// - IError[] Errors { get; } - } -} \ No newline at end of file diff --git a/JSONAPI/Payload/IJsonApiPayload.cs b/JSONAPI/Payload/IJsonApiPayload.cs deleted file mode 100644 index 5462472b..00000000 --- a/JSONAPI/Payload/IJsonApiPayload.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JSONAPI.Payload -{ - /// - /// Base interface for payloads - /// - public interface IJsonApiPayload - { - /// - /// Metadata for the payload as a whole - /// - IMetadata Metadata { get; } - } -} \ No newline at end of file diff --git a/JSONAPI/Payload/PayloadReaderException.cs b/JSONAPI/Payload/PayloadReaderException.cs deleted file mode 100644 index 0c0be714..00000000 --- a/JSONAPI/Payload/PayloadReaderException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace JSONAPI.Payload -{ - /// - /// Exception thrown by an IPayloadReader when the payload is semantically incorrect. - /// - public class PayloadReaderException : Exception - { - /// - /// Creates a new PayloadReaderException - /// - /// - public PayloadReaderException(string message) - : base(message) - { - - } - } -} \ No newline at end of file diff --git a/JSONAPI/Payload/ResourceCollectionPayload.cs b/JSONAPI/Payload/ResourceCollectionPayload.cs deleted file mode 100644 index e02268de..00000000 --- a/JSONAPI/Payload/ResourceCollectionPayload.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace JSONAPI.Payload -{ - /// - /// Default implementation of IPayload - /// - public class ResourceCollectionPayload : IResourceCollectionPayload - { - public IResourceObject[] PrimaryData { get; private set; } - public IResourceObject[] RelatedData { get; private set; } - public IMetadata Metadata { get; private set; } - - /// - /// Constructs a resource collection payload - /// - /// - /// - /// - public ResourceCollectionPayload(IResourceObject[] primaryData, IResourceObject[] relatedData, IMetadata metadata) - { - PrimaryData = primaryData; - RelatedData = relatedData; - Metadata = metadata; - } - } -} diff --git a/JSONAPI/Payload/SingleResourcePayload.cs b/JSONAPI/Payload/SingleResourcePayload.cs deleted file mode 100644 index e4162cc7..00000000 --- a/JSONAPI/Payload/SingleResourcePayload.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace JSONAPI.Payload -{ - /// - /// Default implementation of IPayload - /// - public class SingleResourcePayload : ISingleResourcePayload - { - public IResourceObject PrimaryData { get; private set; } - - public IResourceObject[] RelatedData { get; private set; } - - public IMetadata Metadata { get; private set; } - - /// - /// Constructs a single resource payload - /// - /// - /// - /// - public SingleResourcePayload(IResourceObject primaryData, IResourceObject[] relatedData, IMetadata metadata) - { - PrimaryData = primaryData; - RelatedData = relatedData; - Metadata = metadata; - } - } -} From e27868b658d3cdf8048f4b9d03c8fd9b753a9822 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 15:28:11 -0400 Subject: [PATCH 104/186] make sure TodoMVC sample works. --- JSONAPI.TodoMVC.API/Startup.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index dec3d4fd..91774036 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -1,5 +1,8 @@ -using System.Web.Http; +using System.Data.Entity; +using System.Reflection; +using System.Web.Http; using Autofac; +using Autofac.Integration.WebApi; using JSONAPI.Autofac; using JSONAPI.Autofac.EntityFramework; using JSONAPI.Core; @@ -35,6 +38,9 @@ private static HttpConfiguration GetWebApiConfiguration() containerBuilder.RegisterModule(efModule); containerBuilder.RegisterGeneric(typeof(EntityFrameworkDocumentMaterializer<>)) .AsImplementedInterfaces(); + containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + containerBuilder.RegisterType().As(); + var container = containerBuilder.Build(); httpConfig.UseJsonApiWithAutofac(container); From f3ddb883f36c160fd1ee150b4fc79ffc50bee8af Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 15:56:56 -0400 Subject: [PATCH 105/186] Move namespace of queryable transformers --- .../JsonApiAutofacEntityFrameworkModule.cs | 1 + JSONAPI.Autofac/JsonApiAutofacModule.cs | 1 + .../AsynchronousEnumerationTransformer.cs | 1 + .../DefaultFilteringTransformerTests.cs | 1 + .../DefaultPaginationTransformerTests.cs | 1 + .../DefaultSortingTransformerTests.cs | 1 + .../QueryableTransformerTestsBase.cs | 1 + JSONAPI/Core/JsonApiConfiguration.cs | 1 + ...eryableResourceCollectionDocumentBuilder.cs | 1 + JSONAPI/JSONAPI.csproj | 18 +++++++++--------- .../DefaultFilteringTransformer.cs | 3 ++- .../DefaultPaginationTransformResult.cs | 3 ++- .../DefaultPaginationTransformer.cs | 3 ++- .../DefaultSortingTransformer.cs | 3 ++- .../IQueryableEnumerationTransformer.cs | 2 +- .../IQueryableFilteringTransformer.cs | 2 +- .../IQueryablePaginationTransformer.cs | 2 +- .../IQueryableSortingTransformer.cs | 2 +- .../SynchronousEnumerationTransformer.cs | 2 +- 19 files changed, 31 insertions(+), 18 deletions(-) rename JSONAPI/{ActionFilters => QueryableTransformers}/DefaultFilteringTransformer.cs (99%) rename JSONAPI/{ActionFilters => QueryableTransformers}/DefaultPaginationTransformResult.cs (86%) rename JSONAPI/{ActionFilters => QueryableTransformers}/DefaultPaginationTransformer.cs (98%) rename JSONAPI/{ActionFilters => QueryableTransformers}/DefaultSortingTransformer.cs (98%) rename JSONAPI/{ActionFilters => QueryableTransformers}/IQueryableEnumerationTransformer.cs (95%) rename JSONAPI/{ActionFilters => QueryableTransformers}/IQueryableFilteringTransformer.cs (94%) rename JSONAPI/{ActionFilters => QueryableTransformers}/IQueryablePaginationTransformer.cs (97%) rename JSONAPI/{ActionFilters => QueryableTransformers}/IQueryableSortingTransformer.cs (94%) rename JSONAPI/{ActionFilters => QueryableTransformers}/SynchronousEnumerationTransformer.cs (91%) diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs index 094a53bd..f0a940fa 100644 --- a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs @@ -3,6 +3,7 @@ using JSONAPI.EntityFramework; using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.EntityFramework.Http; +using JSONAPI.QueryableTransformers; namespace JSONAPI.Autofac.EntityFramework { diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index ea391788..8db5a9b7 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -7,6 +7,7 @@ using JSONAPI.Documents.Builders; using JSONAPI.Http; using JSONAPI.Json; +using JSONAPI.QueryableTransformers; namespace JSONAPI.Autofac { diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs index 55a74832..3a8c8588 100644 --- a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using JSONAPI.ActionFilters; +using JSONAPI.QueryableTransformers; namespace JSONAPI.EntityFramework.ActionFilters { diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 46b2c3e2..e75850d5 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -7,6 +7,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.QueryableTransformers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs index c8587bc8..8cb1e0af 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -8,6 +8,7 @@ using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 0bf0f9fe..197fc613 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -6,6 +6,7 @@ using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters diff --git a/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs index 6a621587..adba3c0a 100644 --- a/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs +++ b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Net.Http; using JSONAPI.ActionFilters; +using JSONAPI.QueryableTransformers; namespace JSONAPI.Tests.ActionFilters { diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs index 0f8ee2b1..71c63604 100644 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -4,6 +4,7 @@ using JSONAPI.Documents.Builders; using JSONAPI.Http; using JSONAPI.Json; +using JSONAPI.QueryableTransformers; namespace JSONAPI.Core { diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index 30a6849f..f638f1ba 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using JSONAPI.ActionFilters; using JSONAPI.Http; +using JSONAPI.QueryableTransformers; namespace JSONAPI.Documents.Builders { diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index a9e4da6f..13eb023f 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -66,17 +66,17 @@ - - - + + + - - - - - + + + + + - + diff --git a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs similarity index 99% rename from JSONAPI/ActionFilters/DefaultFilteringTransformer.cs rename to JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index 0624da63..0c8ac0fa 100644 --- a/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -5,9 +5,10 @@ using System.Net.Http; using System.Reflection; using System.Web.Http; +using JSONAPI.ActionFilters; using JSONAPI.Core; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// This transformer filters an IQueryable based on query-string values. diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs b/JSONAPI/QueryableTransformers/DefaultPaginationTransformResult.cs similarity index 86% rename from JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs rename to JSONAPI/QueryableTransformers/DefaultPaginationTransformResult.cs index 253854cd..5c68886c 100644 --- a/JSONAPI/ActionFilters/DefaultPaginationTransformResult.cs +++ b/JSONAPI/QueryableTransformers/DefaultPaginationTransformResult.cs @@ -1,6 +1,7 @@ using System.Linq; +using JSONAPI.ActionFilters; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Default implementation of IPaginationTransformResult`1 diff --git a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs b/JSONAPI/QueryableTransformers/DefaultPaginationTransformer.cs similarity index 98% rename from JSONAPI/ActionFilters/DefaultPaginationTransformer.cs rename to JSONAPI/QueryableTransformers/DefaultPaginationTransformer.cs index de610f75..21c87754 100644 --- a/JSONAPI/ActionFilters/DefaultPaginationTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultPaginationTransformer.cs @@ -1,9 +1,10 @@ using System; using System.Linq; using System.Net.Http; +using JSONAPI.ActionFilters; using JSONAPI.Documents.Builders; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Performs pagination diff --git a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs similarity index 98% rename from JSONAPI/ActionFilters/DefaultSortingTransformer.cs rename to JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs index a31be9bf..32b012c1 100644 --- a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs @@ -4,10 +4,11 @@ using System.Linq.Expressions; using System.Net.Http; using System.Reflection; +using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// This transform sorts an IQueryable according to query parameters. diff --git a/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs b/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs similarity index 95% rename from JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs rename to JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs index 5fd785fd..0bcf07d3 100644 --- a/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Provides a service to asynchronously materialize the results of an IQueryable into diff --git a/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs b/JSONAPI/QueryableTransformers/IQueryableFilteringTransformer.cs similarity index 94% rename from JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs rename to JSONAPI/QueryableTransformers/IQueryableFilteringTransformer.cs index 845644e7..fdb5f770 100644 --- a/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryableFilteringTransformer.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Net.Http; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Service for filtering an IQueryable according to an HTTP request. diff --git a/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs b/JSONAPI/QueryableTransformers/IQueryablePaginationTransformer.cs similarity index 97% rename from JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs rename to JSONAPI/QueryableTransformers/IQueryablePaginationTransformer.cs index cabe302a..01ff8e1d 100644 --- a/JSONAPI/ActionFilters/IQueryablePaginationTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryablePaginationTransformer.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Net.Http; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Provides a service to provide a page of data based on information from the request. diff --git a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs b/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs similarity index 94% rename from JSONAPI/ActionFilters/IQueryableSortingTransformer.cs rename to JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs index ced08b89..64e06fbf 100644 --- a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Net.Http; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Service for sorting an IQueryable according to an HTTP request. diff --git a/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs b/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs similarity index 91% rename from JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs rename to JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs index ea084f44..38053221 100644 --- a/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs +++ b/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace JSONAPI.ActionFilters +namespace JSONAPI.QueryableTransformers { /// /// Synchronously enumerates an IQueryable From edcb95972dfd5a1c9835078d82d76d52bb128162 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 17:05:34 -0400 Subject: [PATCH 106/186] ensure not found works for fetching resource by ID also merged related resource tests into the FetchingResources test class. --- .../Acceptance/FetchingResourcesTests.cs | 48 +++++++++++++++++++ .../Get_related_to_many_response.json | 0 .../Get_related_to_one_response.json | 0 .../Get_resource_by_id_that_doesnt_exist.json | 10 ++++ .../Acceptance/RelatedResourcesTests.cs | 46 ------------------ .../JSONAPI.EntityFramework.Tests.csproj | 8 ++-- .../EntityFrameworkDocumentMaterializer.cs | 3 ++ 7 files changed, 65 insertions(+), 50 deletions(-) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{RelatedResources/Responses => FetchingResources}/Get_related_to_many_response.json (100%) rename JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/{RelatedResources/Responses => FetchingResources}/Get_related_to_one_response.json (100%) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/RelatedResourcesTests.cs diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs index 18e4861e..f648e7bd 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -55,6 +55,22 @@ public async Task GetById() } } + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_resource_by_id_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); + } + } + [TestMethod] [DeploymentItem(@"Acceptance\Data\UserGroup.csv", @"Acceptance\Data")] public async Task Get_dasherized_resource() @@ -66,5 +82,37 @@ public async Task Get_dasherized_resource() await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_dasherized_resource.json", HttpStatusCode.OK); } } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_related_to_many() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/comments"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_related_to_many_response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_related_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/author"); + + await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_response.json", HttpStatusCode.OK); + } + } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/RelatedResources/Responses/Get_related_to_many_response.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/RelatedResources/Responses/Get_related_to_many_response.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/RelatedResources/Responses/Get_related_to_one_response.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/RelatedResources/Responses/Get_related_to_one_response.json rename to JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json new file mode 100644 index 00000000..c5ad734d --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`" + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/RelatedResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/RelatedResourcesTests.cs deleted file mode 100644 index 9fbba826..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/RelatedResourcesTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class RelatedResourcesTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_many() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/201/comments"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\RelatedResources\Responses\Get_related_to_many_response.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_one() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/201/author"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\RelatedResources\Responses\Get_related_to_one_response.json", HttpStatusCode.OK); - } - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 90264dd1..4b6f9262 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -113,12 +113,11 @@ + - - @@ -188,8 +187,8 @@ - - + + Always @@ -204,6 +203,7 @@ + Designer diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 00b184e5..30901ee0 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -68,6 +68,9 @@ public virtual async Task GetRecordById(string id, Http var apiBaseUrl = GetBaseUrlFromRequest(request); var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); var singleResource = await FilterById(id, registration).FirstOrDefaultAsync(cancellationToken); + if (singleResource == null) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`", + registration.ResourceTypeName, id)); return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null); } From dae593ff26e26ac1b900f7e111a643e7ff3473a5 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 17:11:48 -0400 Subject: [PATCH 107/186] return 404 for to-one related resources to non-existant resource --- .../Acceptance/FetchingResourcesTests.cs | 18 ++++++++++++++++++ ..._to_one_for_resource_that_doesnt_exist.json | 10 ++++++++++ .../Get_resource_by_id_that_doesnt_exist.json | 2 +- .../JSONAPI.EntityFramework.Tests.csproj | 1 + .../EntityFrameworkDocumentMaterializer.cs | 5 ++++- 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs index f648e7bd..fa34cb98 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -114,5 +114,23 @@ public async Task Get_related_to_one() await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_response.json", HttpStatusCode.OK); } } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_related_to_one_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/author"); + + await AssertResponseContent(response, + @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json new file mode 100644 index 00000000..3921d080 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json index c5ad734d..3921d080 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "404", "title": "Resource not found", - "detail": "No resource of type `posts` exists with id `3000`" + "detail": "No resource of type `posts` exists with id `3000`." } ] } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 4b6f9262..96bbdd83 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -204,6 +204,7 @@ + Designer diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 30901ee0..c7ab8d80 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -69,7 +69,7 @@ public virtual async Task GetRecordById(string id, Http var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); var singleResource = await FilterById(id, registration).FirstOrDefaultAsync(cancellationToken); if (singleResource == null) - throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`", + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", registration.ResourceTypeName, id)); return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null); } @@ -172,6 +172,9 @@ protected async Task GetRelatedToOne(string i var primaryEntityQuery = FilterById(id, primaryEntityRegistration); var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); + if (relatedResource == null) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", + primaryEntityRegistration.ResourceTypeName, id)); return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null); } From e1a78d28ebe103a7a130a0d496a4860f4e5c0fcc Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 17:19:45 -0400 Subject: [PATCH 108/186] return 404 for to-many related actions where primary resource doesn't exist --- .../Acceptance/FetchingResourcesTests.cs | 18 ++++++++++++++++++ ...to_many_for_resource_that_doesnt_exist.json | 10 ++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 1 + .../EntityFrameworkDocumentMaterializer.cs | 7 +++++++ 4 files changed, 36 insertions(+) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs index fa34cb98..4d95d11c 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -132,5 +132,23 @@ await AssertResponseContent(response, HttpStatusCode.NotFound, true); } } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_related_to_many_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/tags"); + + await AssertResponseContent(response, + @"Acceptance\Fixtures\FetchingResources\Get_related_to_many_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json new file mode 100644 index 00000000..3921d080 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 96bbdd83..9e89ebbd 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -205,6 +205,7 @@ + Designer diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index c7ab8d80..749e4eee 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -154,6 +154,13 @@ protected async Task GetRelatedToMany(str var lambda = Expression.Lambda>>(accessorExpr, param); var primaryEntityQuery = FilterById(id, primaryEntityRegistration); + + // We have to see if the resource even exists, so we can throw a 404 if it doesn't + var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); + if (relatedResource == null) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", + primaryEntityRegistration.ResourceTypeName, id)); + var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda); return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, cancellationToken); From b4fb7d21d4ef2e4d452f121f8aacbe9be72ba4b5 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 22:39:03 -0400 Subject: [PATCH 109/186] provide serialization hooks for RelationshipObjectFormatter children --- JSONAPI/Json/RelationshipObjectFormatter.cs | 28 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/JSONAPI/Json/RelationshipObjectFormatter.cs b/JSONAPI/Json/RelationshipObjectFormatter.cs index ce81647f..e598894c 100644 --- a/JSONAPI/Json/RelationshipObjectFormatter.cs +++ b/JSONAPI/Json/RelationshipObjectFormatter.cs @@ -39,7 +39,19 @@ public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) LinksKeyName, LinkageKeyName, MetaKeyName)); writer.WriteStartObject(); + SerializeLinks(relationshipObject, writer); + SerializeLinkage(relationshipObject, writer); + SerializeMetadata(relationshipObject, writer); + writer.WriteEndObject(); + return Task.FromResult(0); + } + + /// + /// Serializes the relationship object's links. + /// + protected virtual void SerializeLinks(IRelationshipObject relationshipObject, JsonWriter writer) + { if (relationshipObject.SelfLink != null || relationshipObject.RelatedResourceLink != null) { writer.WritePropertyName(LinksKeyName); @@ -58,22 +70,30 @@ public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) writer.WriteEndObject(); } + } + /// + /// Serializes the relationship object's linkage. + /// + protected virtual void SerializeLinkage(IRelationshipObject relationshipObject, JsonWriter writer) + { if (relationshipObject.Linkage != null) { writer.WritePropertyName(LinkageKeyName); _resourceLinkageFormatter.Serialize(relationshipObject.Linkage, writer); } + } + /// + /// Serializes the relationship object's metadata. + /// + protected virtual void SerializeMetadata(IRelationshipObject relationshipObject, JsonWriter writer) + { if (relationshipObject.Metadata != null) { writer.WritePropertyName(MetaKeyName); _metadataFormatter.Serialize(relationshipObject.Metadata, writer); } - - writer.WriteEndObject(); - - return Task.FromResult(0); } public async Task Deserialize(JsonReader reader, string currentPath) From bc083dfc9fb3da637e34e9af9d80c648cb1063c8 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 1 Jul 2015 23:05:25 -0400 Subject: [PATCH 110/186] don't crash when requesting related resource for non-existent relationship --- .../Acceptance/FetchingResourcesTests.cs | 18 ++++++++++++++++++ ...rce_for_relationship_that_doesnt_exist.json | 10 ++++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 1 + .../EntityFrameworkDocumentMaterializer.cs | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs index 4d95d11c..33bc8d57 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -150,5 +150,23 @@ await AssertResponseContent(response, HttpStatusCode.NotFound, true); } } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + public async Task Get_related_resource_for_relationship_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/bananas"); + + await AssertResponseContent(response, + @"Acceptance\Fixtures\FetchingResources\Get_related_resource_for_relationship_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json new file mode 100644 index 00000000..ec74d25a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No relationship `bananas` exists for the resource with type `posts` and id `201`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 9e89ebbd..8e53e12c 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -206,6 +206,7 @@ + Designer diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 749e4eee..15ea8beb 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -79,6 +79,9 @@ public virtual async Task GetRelated(string id, string relatio { var registration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); var relationship = (ResourceTypeRelationship) registration.GetFieldByName(relationshipKey); + if (relationship == null) + throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", + relationshipKey, registration.ResourceTypeName, id)); if (relationship.IsToMany) { From 2e54d6a40682b0175ec796cb4d1f5a008b590956 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 4 Jul 2015 19:06:54 -0400 Subject: [PATCH 111/186] make GetBaseUrl method virtual to facilitate inheritance --- JSONAPI/Http/BaseUrlService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/Http/BaseUrlService.cs b/JSONAPI/Http/BaseUrlService.cs index cedebe99..174e83e5 100644 --- a/JSONAPI/Http/BaseUrlService.cs +++ b/JSONAPI/Http/BaseUrlService.cs @@ -8,7 +8,7 @@ namespace JSONAPI.Http /// public class BaseUrlService : IBaseUrlService { - public string GetBaseUrl(HttpRequestMessage requestMessage) + public virtual string GetBaseUrl(HttpRequestMessage requestMessage) { return new Uri(requestMessage.RequestUri.AbsoluteUri.Replace(requestMessage.RequestUri.PathAndQuery, From b2a5213693f809dae1dfd1f7e653523465f61b30 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 4 Jul 2015 19:32:02 -0400 Subject: [PATCH 112/186] allow specifying includes and metadata for queryable builder --- .../Builders/FallbackDocumentBuilderTests.cs | 4 +-- ...ryableResourceCollectionDocumentBuilder.cs | 33 +++++++++++-------- .../Builders/FallbackDocumentBuilder.cs | 2 +- ...ryableResourceCollectionDocumentBuilder.cs | 7 ++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs index 9ba6af5f..65735175 100644 --- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -75,8 +75,8 @@ public async Task Creates_resource_collection_document_for_queryables() var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); mockQueryableDocumentBuilder - .Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token)) - .Returns(() => Task.FromResult(mockDocument.Object)); + .Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token, null)) + .Returns(Task.FromResult(mockDocument.Object)); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index f638f1ba..c0e27b34 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using JSONAPI.ActionFilters; using JSONAPI.Http; +using JSONAPI.Json; using JSONAPI.QueryableTransformers; namespace JSONAPI.Documents.Builders @@ -39,24 +41,29 @@ public DefaultQueryableResourceCollectionDocumentBuilder( _baseUrlService = baseUrlService; } - public async Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken) + public async Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken, + string[] includes = null) { - if (_filteringTransformer != null) - query = _filteringTransformer.Filter(query, request); + var filteredQuery = _filteringTransformer.Filter(query, request); + var sortedQuery = _sortingTransformer.Sort(filteredQuery, request); - if (_sortingTransformer != null) - query = _sortingTransformer.Sort(query, request); - - if (_paginationTransformer != null) - { - var paginationResults = _paginationTransformer.ApplyPagination(query, request); - query = paginationResults.PagedQuery; - } + var paginationResults = _paginationTransformer.ApplyPagination(sortedQuery, request); + query = paginationResults.PagedQuery; var linkBaseUrl = _baseUrlService.GetBaseUrl(request); var results = await _enumerationTransformer.Enumerate(query, cancellationToken); - return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, null, null); + var metadata = await GetDocumentMetadata(query, filteredQuery, sortedQuery, paginationResults, cancellationToken); + return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, includes, metadata); + } + + /// + /// Returns the metadata that should be sent with this document. + /// + protected virtual Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery, + IPaginationTransformResult paginationResult, CancellationToken cancellationToken) + { + return Task.FromResult((IMetadata)null); } } } diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs index cec5ccd1..fd212875 100644 --- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -61,7 +61,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage _openBuildDocumentFromQueryableMethod.Value.MakeGenericMethod(queryableElementType); dynamic materializedQueryTask = buildDocumentMethod.Invoke(_queryableResourceCollectionDocumentBuilder, - new[] {obj, requestMessage, cancellationToken}); + new[] { obj, requestMessage, cancellationToken, null }); return await materializedQueryTask; } diff --git a/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs index 58c3bc32..c1040d49 100644 --- a/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -16,8 +17,10 @@ public interface IQueryableResourceCollectionDocumentBuilder /// The query to materialize to build the response document /// The request containing parameters to determine how to sort/filter/paginate the query /// + /// The set of paths to include in the compound document /// /// - Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken); + Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken, + string[] includePaths = null); } } From 7dc407ef5bcba5aa24eaa5d44305cc73606e7a5e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 7 Jul 2015 13:29:29 -0400 Subject: [PATCH 113/186] don't fail on null to-many relationship --- .../Builders/RegistryDrivenDocumentBuilder.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs index fd5b0335..2995d18a 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs @@ -76,29 +76,31 @@ protected ResourceObject CreateResourceObject(object modelObject, IDictionary)modelRelationship.Property.GetValue(modelObject); - - var identifiers = new List(); - foreach (var relatedResource in propertyValue) + if (propertyValue != null) { - var identifier = GetResourceIdentifierForResource(relatedResource); - identifiers.Add(identifier); - - IDictionary idDictionary; - if (!idDictionariesByType.TryGetValue(identifier.Type, out idDictionary)) + var identifiers = new List(); + foreach (var relatedResource in propertyValue) { - idDictionary = new Dictionary(); - idDictionariesByType[identifier.Type] = idDictionary; - } - - ResourceObject relatedResourceObject; - if (!idDictionary.TryGetValue(identifier.Id, out relatedResourceObject)) - { - relatedResourceObject = CreateResourceObject(relatedResource, idDictionariesByType, - childPath, includePathExpressions, linkBaseUrl); - idDictionary[identifier.Id] = relatedResourceObject; + var identifier = GetResourceIdentifierForResource(relatedResource); + identifiers.Add(identifier); + + IDictionary idDictionary; + if (!idDictionariesByType.TryGetValue(identifier.Type, out idDictionary)) + { + idDictionary = new Dictionary(); + idDictionariesByType[identifier.Type] = idDictionary; + } + + ResourceObject relatedResourceObject; + if (!idDictionary.TryGetValue(identifier.Id, out relatedResourceObject)) + { + relatedResourceObject = CreateResourceObject(relatedResource, idDictionariesByType, + childPath, includePathExpressions, linkBaseUrl); + idDictionary[identifier.Id] = relatedResourceObject; + } } + linkage = new ToManyResourceLinkage(identifiers.ToArray()); } - linkage = new ToManyResourceLinkage(identifiers.ToArray()); } else { From ba3388bd61ed035e37b2c071027848b9c42a1990 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 7 Jul 2015 17:52:28 -0400 Subject: [PATCH 114/186] remove extraneous select --- .../Http/EntityFrameworkDocumentMaterializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 15ea8beb..23aeeb8a 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -159,7 +159,7 @@ protected async Task GetRelatedToMany(str var primaryEntityQuery = FilterById(id, primaryEntityRegistration); // We have to see if the resource even exists, so we can throw a 404 if it doesn't - var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); + var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); if (relatedResource == null) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", primaryEntityRegistration.ResourceTypeName, id)); From d1256afc05d064f035e4688f93d7af0c9c579d74 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 8 Jul 2015 23:36:02 -0400 Subject: [PATCH 115/186] simplify async queryable enumeration transformer There was no need to be doing all that reflection. --- .../AsynchronousEnumerationTransformer.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs index 3a8c8588..ebb41f0f 100644 --- a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -1,10 +1,7 @@ -using System; -using System.Data.Entity; +using System.Data.Entity; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; -using JSONAPI.ActionFilters; using JSONAPI.QueryableTransformers; namespace JSONAPI.EntityFramework.ActionFilters @@ -14,18 +11,9 @@ namespace JSONAPI.EntityFramework.ActionFilters /// public class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer { - private readonly Lazy _toArrayAsyncMethod = new Lazy(() => - typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); - public async Task Enumerate(IQueryable query, CancellationToken cancellationToken) { - var queryableElementType = typeof (T); - var openToArrayAsyncMethod = _toArrayAsyncMethod.Value; - var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); - var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new object[] { query, cancellationToken }); - - var resultArray = await invocation; - return resultArray; + return await query.ToArrayAsync(cancellationToken); } } } From 4d0de76e65367d2e2590a9f4968a733df951e840 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 8 Jul 2015 23:39:47 -0400 Subject: [PATCH 116/186] add queryable enumeration transform method The queryable transformer is a ghetto version of IEnumerable. It only exists to let JSONAPI Core access an async version of methods like ToArray. FirstOrDefault is another method we need. --- .../ActionFilters/AsynchronousEnumerationTransformer.cs | 5 +++++ .../IQueryableEnumerationTransformer.cs | 9 +++++++++ .../SynchronousEnumerationTransformer.cs | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs index ebb41f0f..f6efe4c3 100644 --- a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -15,5 +15,10 @@ public async Task Enumerate(IQueryable query, CancellationToken cance { return await query.ToArrayAsync(cancellationToken); } + + public async Task FirstOrDefault(IQueryable query, CancellationToken cancellationToken) + { + return await query.FirstOrDefaultAsync(cancellationToken); + } } } diff --git a/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs b/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs index 0bcf07d3..c10eb983 100644 --- a/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryableEnumerationTransformer.cs @@ -18,5 +18,14 @@ public interface IQueryableEnumerationTransformer /// The queryable element type /// A task yielding the enumerated results of the query Task Enumerate(IQueryable query, CancellationToken cancellationToken); + + /// + /// Gets the first result for the specified query. + /// + /// The query to enumerate + /// The request's cancellation token. If this token is cancelled during enumeration, enumeration must halt. + /// The queryable element type + /// A task yielding the enumerated results of the query + Task FirstOrDefault(IQueryable query, CancellationToken cancellationToken); } } diff --git a/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs b/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs index 38053221..2c7b0b95 100644 --- a/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs +++ b/JSONAPI/QueryableTransformers/SynchronousEnumerationTransformer.cs @@ -13,5 +13,10 @@ public Task Enumerate(IQueryable query, CancellationToken cancellatio { return Task.FromResult(query.ToArray()); } + + public Task FirstOrDefault(IQueryable query, CancellationToken cancellationToken) + { + return Task.FromResult(query.FirstOrDefault()); + } } } From bf14fe04691ab779682b88631e21c6dcbe408714 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 9 Jul 2015 15:42:31 -0400 Subject: [PATCH 117/186] add materializer for mapping entities to DTOs --- .../EntityFrameworkDocumentMaterializer.cs | 6 + JSONAPI/Http/IDocumentMaterializer.cs | 9 +- JSONAPI/Http/MappedDocumentMaterializer.cs | 228 ++++++++++++++++++ JSONAPI/JSONAPI.csproj | 1 + 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 JSONAPI/Http/MappedDocumentMaterializer.cs diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 23aeeb8a..1be58067 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -63,6 +63,12 @@ public virtual Task GetRecords(HttpRequestMessage r return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); } + public Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) + { + var query = _dbContext.Set().AsQueryable().Where(filter); + return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); + } + public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); diff --git a/JSONAPI/Http/IDocumentMaterializer.cs b/JSONAPI/Http/IDocumentMaterializer.cs index b0fbd6be..e0cbf418 100644 --- a/JSONAPI/Http/IDocumentMaterializer.cs +++ b/JSONAPI/Http/IDocumentMaterializer.cs @@ -1,4 +1,6 @@ -using System.Net.Http; +using System; +using System.Linq.Expressions; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using JSONAPI.Documents; @@ -16,6 +18,11 @@ public interface IDocumentMaterializer where T : class /// Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken); + /// + /// Returns a document containing records matching the provided lambda expression. + /// + Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken); + /// /// Returns a document with the resource identified by the given ID. /// diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs new file mode 100644 index 00000000..9e6c9e7a --- /dev/null +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.Http +{ + /// + /// Document materializer for mapping from a database entity to a data transfer object. + /// + /// + /// + public abstract class MappedDocumentMaterializer : IDocumentMaterializer where TDto : class + { + /// + /// Materializes a document for the resources found on the other side of the to-many relationship belonging to the resource. + /// + protected delegate Task MaterializeDocumentForToManyRelationship( + TDto resource, HttpRequestMessage request, CancellationToken cancellationToken); + + /// + /// Materializes a document for the resources found on the other side of the to-one relationship belonging to the resource. + /// + protected delegate Task MaterializeDocumentForToOneRelationship( + TDto resource, HttpRequestMessage request, CancellationToken cancellationToken); + + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + private readonly IBaseUrlService _baseUrlService; + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; + private readonly IQueryableEnumerationTransformer _queryableEnumerationTransformer; + private readonly IResourceTypeRegistry _resourceTypeRegistry; + private readonly IDictionary _toManyRelatedResourceMaterializers; + private readonly IDictionary _toOneRelatedResourceMaterializers; + + /// + /// Gets a query returning all entities for this endpoint + /// + /// + protected abstract IQueryable GetQuery(); + protected abstract IQueryable GetByIdQuery(string id); + protected abstract IQueryable GetMappedQuery(IQueryable entityQuery, Expression>[] propertiesToInclude); + + protected MappedDocumentMaterializer( + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IBaseUrlService baseUrlService, + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + IQueryableEnumerationTransformer queryableEnumerationTransformer, + IResourceTypeRegistry resourceTypeRegistry) + { + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + _baseUrlService = baseUrlService; + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; + _queryableEnumerationTransformer = queryableEnumerationTransformer; + _resourceTypeRegistry = resourceTypeRegistry; + _toManyRelatedResourceMaterializers = + new ConcurrentDictionary(); + _toOneRelatedResourceMaterializers = + new ConcurrentDictionary(); + } + + public async Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) + { + return await GetRecordsMatchingExpression(m => true, request, cancellationToken); + } + + public async Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) + { + var entityQuery = GetQuery(); + var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; + var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); + var mappedQuery = GetMappedQuery(entityQuery, includePaths); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths); + } + + public async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) + { + var entityQuery = GetByIdQuery(id); + var includePaths = GetIncludePathsForSingleResource() ?? new Expression>[] { }; + var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); + var mappedQuery = GetMappedQuery(entityQuery, includePaths); + var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); + if (primaryResource == null) throw JsonApiException.CreateForNotFound(string.Format("No record exists with ID {0} for the requested type.", id)); + + var baseUrl = _baseUrlService.GetBaseUrl(request); + return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths); + } + + public async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken) + { + var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); + + var primaryEntityQuery = GetByIdQuery(id); + var mappedQuery = GetMappedQuery(primaryEntityQuery, null); + var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); + if (primaryResource == null) + { + var dtoRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); + throw JsonApiException.CreateForNotFound(string.Format( + "No resource of type `{0}` exists with id `{1}`.", + dtoRegistration.ResourceTypeName, id)); + } + + var relationship = (ResourceTypeRelationship)registration.GetFieldByName(relationshipKey); + if (relationship == null) + throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", + relationshipKey, registration.ResourceTypeName, id)); + + if (relationship.IsToMany) + { + MaterializeDocumentForToManyRelationship documentFactory; + if (!_toManyRelatedResourceMaterializers.TryGetValue(relationship, out documentFactory)) + { + documentFactory = GetMaterializerForToManyRelatedResource(relationship); + _toManyRelatedResourceMaterializers.Add(relationship, documentFactory); + } + return await documentFactory(primaryResource, request, cancellationToken); + } + else + { + MaterializeDocumentForToOneRelationship relatedResourceMaterializer; + if (!_toOneRelatedResourceMaterializers.TryGetValue(relationship, out relatedResourceMaterializer)) + { + relatedResourceMaterializer = GetMaterializerForToOneRelatedResource(relationship); + _toOneRelatedResourceMaterializers.Add(relationship, relatedResourceMaterializer); + } + return await relatedResourceMaterializer(primaryResource, request, cancellationToken); + } + } + + public Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DeleteRecord(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + /// Returns a list of property paths to be included when constructing a query for this resource type + /// + protected virtual Expression>[] GetIncludePathsForQuery() + { + return null; + } + + /// + /// Returns a list of property paths to be included when returning a single resource of this resource type + /// + protected virtual Expression>[] GetIncludePathsForSingleResource() + { + return null; + } + + /// + /// Returns a materialization delegate to handle related resource requests for a to-many relationship + /// + protected abstract MaterializeDocumentForToManyRelationship GetMaterializerForToManyRelatedResource(ResourceTypeRelationship relationship); + + /// + /// Returns a materialization delegate to handle related resource requests for a to-one relationship + /// + protected abstract MaterializeDocumentForToOneRelationship GetMaterializerForToOneRelatedResource(ResourceTypeRelationship relationship); + + private string ConvertToJsonKeyPath(Expression> expression) + { + var visitor = new PathVisitor(_resourceTypeRegistry); + visitor.Visit(expression); + return visitor.Path; + } + + private class PathVisitor : ExpressionVisitor + { + private readonly IResourceTypeRegistry _resourceTypeRegistry; + + public PathVisitor(IResourceTypeRegistry resourceTypeRegistry) + { + _resourceTypeRegistry = resourceTypeRegistry; + } + + private readonly Stack _segments = new Stack(); + public string Path { get { return string.Join(".", _segments.ToArray()); } } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "Select") + { + Visit(node.Arguments[1]); + Visit(node.Arguments[0]); + } + return node; + } + + protected override Expression VisitMember(MemberExpression node) + { + var property = node.Member as PropertyInfo; + if (property == null) return node; + + var registration = _resourceTypeRegistry.GetRegistrationForType(property.DeclaringType); + if (registration == null || registration.Relationships == null) return node; + + var relationship = registration.Relationships.FirstOrDefault(r => r.Property == property); + if (relationship == null) return node; + + _segments.Push(relationship.JsonKey); + + return base.VisitMember(node); + } + } + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 13eb023f..c655cb7f 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -66,6 +66,7 @@ + From 464d6486db113151fc5d1c5adc2e3b7d61e0b1dd Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 10 Jul 2015 19:24:14 -0400 Subject: [PATCH 118/186] allow serializing null related resources --- .../Controllers/BuildingsController.cs | 13 ++++++++++++ .../Controllers/CompaniesController.cs | 13 ++++++++++++ ...PI.EntityFramework.Tests.TestWebApp.csproj | 4 ++++ .../Models/Building.cs | 20 +++++++++++++++++++ .../Models/Company.cs | 12 +++++++++++ .../Models/TestDbContext.cs | 2 ++ .../Startup.cs | 2 ++ .../Acceptance/Data/Building.csv | 3 +++ .../Acceptance/Data/Company.csv | 2 ++ .../Acceptance/FetchingResourcesTests.cs | 15 ++++++++++++++ .../Get_related_to_one_where_it_is_null.json | 3 +++ .../JSONAPI.EntityFramework.Tests.csproj | 7 +++++++ .../EntityFrameworkDocumentMaterializer.cs | 5 +++-- ...rivenSingleResourceDocumentBuilderTests.cs | 15 ++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + ...lize_ResourceObject_for_null_resource.json | 1 + .../Json/ResourceObjectFormatterTests.cs | 7 +++++++ .../Builders/RegistryDrivenDocumentBuilder.cs | 2 ++ JSONAPI/Json/ResourceObjectFormatter.cs | 6 ++++++ 19 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs create mode 100644 JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv create mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_null_resource.json diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs new file mode 100644 index 00000000..63bd0756 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs @@ -0,0 +1,13 @@ +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class BuildingsController : JsonApiController + { + public BuildingsController(IDocumentMaterializer documentMaterializer) + : base(documentMaterializer) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs new file mode 100644 index 00000000..dde3dccc --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs @@ -0,0 +1,13 @@ +using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +{ + public class CompaniesController : JsonApiController + { + public CompaniesController(IDocumentMaterializer documentMaterializer) + : base(documentMaterializer) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj index 5a55ebf8..a1613ed0 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj @@ -115,7 +115,9 @@ + + @@ -127,7 +129,9 @@ + + diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs new file mode 100644 index 00000000..e7c720b9 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public class Building + { + [Key] + public string Id { get; set; } + + public string Address { get; set; } + + [JsonIgnore] + public string OwnerCompanyId { get; set; } + + [ForeignKey("OwnerCompanyId")] + public virtual Company Owner { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs new file mode 100644 index 00000000..05061cf7 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +{ + public class Company + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs index aefb33e6..b6580b76 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs @@ -35,6 +35,8 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .Map(c => c.MapLeftKey("PostId").MapRightKey("TagId").ToTable("PostTagLink")); } + public DbSet Buildings { get; set; } + public DbSet Companies { get; set; } public DbSet Comments { get; set; } public DbSet Languages { get; set; } public DbSet Posts { get; set; } diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index cf370542..8ccacfa5 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -50,8 +50,10 @@ public void Configuration(IAppBuilder app) var namingConventions = new DefaultNamingConventions(pluralizationService); var configuration = new JsonApiAutofacConfiguration(namingConventions); + configuration.RegisterResourceType(typeof(Building)); configuration.RegisterResourceType(typeof(City)); configuration.RegisterResourceType(typeof(Comment)); + configuration.RegisterResourceType(typeof(Company)); configuration.RegisterResourceType(typeof(Language)); configuration.RegisterResourceType(typeof(LanguageUserLink), sortByIdFactory: LanguageUserLinkSortByIdFactory, diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv b/JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv new file mode 100644 index 00000000..5faa1c59 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv @@ -0,0 +1,3 @@ +Id,Address,OwnerCompanyId +"1000","123 Sesame St.","1100" +"1001","1600 Pennsylvania Avenue", diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv b/JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv new file mode 100644 index 00000000..4dc5014b --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv @@ -0,0 +1,2 @@ +Id,Name +"1100","Big Bird and Friends" diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs index 33bc8d57..ab32b9e0 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs @@ -168,5 +168,20 @@ await AssertResponseContent(response, HttpStatusCode.NotFound, true); } } + + [TestMethod] + [DeploymentItem(@"Acceptance\Data\Building.csv", @"Acceptance\Data")] + [DeploymentItem(@"Acceptance\Data\Company.csv", @"Acceptance\Data")] + public async Task Get_related_to_one_where_it_is_null() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "buildings/1001/owner"); + + await AssertResponseContent(response, + @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_where_it_is_null.json", + HttpStatusCode.OK); + } + } } } diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json new file mode 100644 index 00000000..fd4493f0 --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json @@ -0,0 +1,3 @@ +{ + "data": null +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 8e53e12c..e5198d6a 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -189,6 +189,12 @@ + + Always + + + Always + Always @@ -207,6 +213,7 @@ + Designer diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 1be58067..b107e1a9 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -187,10 +187,11 @@ protected async Task GetRelatedToOne(string i var lambda = Expression.Lambda>(accessorExpr, param); var primaryEntityQuery = FilterById(id, primaryEntityRegistration); - var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); - if (relatedResource == null) + var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken); + if (!primaryEntityExists) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", primaryEntityRegistration.ResourceTypeName, id)); + var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null); } diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index 982917d3..084dbca5 100644 --- a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -244,6 +244,21 @@ public void Returns_correct_document_for_resource() continentCountriesRelationship.Value.Linkage.Should().BeNull(); } + [TestMethod] + public void Returns_correct_document_for_null_resource() + { + // Arrange + var mockRegistry = new Mock(MockBehavior.Strict); + var linkConventions = new DefaultLinkConventions(); + + // Act + var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); + var document = documentBuilder.BuildDocument(null, "http://www.example.com", null); + + // Assert + document.PrimaryData.Should().BeNull(); + } + private void AssertToOneRelationship(KeyValuePair relationshipPair, string keyName, string selfLink, string relatedResourceLink, string linkageType, string linkageId) { diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 097eb362..33863ee7 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -144,6 +144,7 @@ + diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_null_resource.json b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_null_resource.json new file mode 100644 index 00000000..c296c2ee --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_null_resource.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs b/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs index f11e32d5..945061c2 100644 --- a/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs +++ b/JSONAPI.Tests/Json/ResourceObjectFormatterTests.cs @@ -15,6 +15,13 @@ namespace JSONAPI.Tests.Json [TestClass] public class ResourceObjectFormatterTests : JsonApiFormatterTestsBase { + [TestMethod] + public async Task Serialize_ResourceObject_for_null_resource() + { + var formatter = new ResourceObjectFormatter(null, null, null); + await AssertSerializeOutput(formatter, (IResourceObject)null, "Json/Fixtures/ResourceObjectFormatter/Serialize_ResourceObject_for_null_resource.json"); + } + [TestMethod] public async Task Serialize_ResourceObject_for_resource_without_attributes() { diff --git a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs index 2995d18a..ddcfdda2 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs @@ -50,6 +50,8 @@ internal static bool PathExpressionMatchesCurrentPath(string currentPath, string protected ResourceObject CreateResourceObject(object modelObject, IDictionary> idDictionariesByType, string currentPath, string[] includePathExpressions, string linkBaseUrl) { + if (modelObject == null) return null; + var modelObjectRuntimeType = modelObject.GetType(); var resourceTypeRegistration = _resourceTypeRegistry.GetRegistrationForType(modelObjectRuntimeType); diff --git a/JSONAPI/Json/ResourceObjectFormatter.cs b/JSONAPI/Json/ResourceObjectFormatter.cs index 6ad50171..076f0e0f 100644 --- a/JSONAPI/Json/ResourceObjectFormatter.cs +++ b/JSONAPI/Json/ResourceObjectFormatter.cs @@ -38,6 +38,12 @@ public ResourceObjectFormatter(IRelationshipObjectFormatter relationshipObjectFo public Task Serialize(IResourceObject resourceObject, JsonWriter writer) { + if (resourceObject == null) + { + writer.WriteNull(); + return Task.FromResult(0); + } + writer.WriteStartObject(); writer.WritePropertyName(TypeKeyName); From 48e19a4c6e089e9d992353d8c770148027dcd18e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 10 Jul 2015 19:33:50 -0400 Subject: [PATCH 119/186] allow serializing metadata with ISingleResourceDocumentBuilder --- .../Http/EntityFrameworkDocumentMaterializer.cs | 8 ++++---- .../Builders/FallbackDocumentBuilderTests.cs | 2 +- ...egistryDrivenSingleResourceDocumentBuilderTests.cs | 11 +++++++++-- JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs | 2 +- .../Builders/ISingleResourceDocumentBuilder.cs | 3 ++- .../RegistryDrivenSingleResourceDocumentBuilder.cs | 4 ++-- JSONAPI/Http/MappedDocumentMaterializer.cs | 2 +- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index b107e1a9..e963129c 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -77,7 +77,7 @@ public virtual async Task GetRecordById(string id, Http if (singleResource == null) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", registration.ResourceTypeName, id)); - return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null); + return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null); } public virtual async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, @@ -109,7 +109,7 @@ public virtual async Task CreateRecord(ISingleResourceD var apiBaseUrl = GetBaseUrlFromRequest(request); var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null, null); return returnDocument; } @@ -119,7 +119,7 @@ public virtual async Task UpdateRecord(string id, ISing { var apiBaseUrl = GetBaseUrlFromRequest(request); var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null, null); await _dbContext.SaveChangesAsync(cancellationToken); return returnDocument; @@ -192,7 +192,7 @@ protected async Task GetRelatedToOne(string i throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", primaryEntityRegistration.ResourceTypeName, id)); var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); - return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null); + return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null, null); } private IQueryable Filter(Expression> predicate, diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs index 65735175..0ecd1365 100644 --- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -32,7 +32,7 @@ public async Task Creates_single_resource_document_for_registered_non_collection var mockDocument = new Mock(MockBehavior.Strict); var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); - singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null)).Returns(mockDocument.Object); + singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null, null)).Returns(mockDocument.Object); var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index 084dbca5..ebab7a73 100644 --- a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -4,6 +4,7 @@ using JSONAPI.Core; using JSONAPI.Documents; using JSONAPI.Documents.Builders; +using JSONAPI.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json.Linq; @@ -169,9 +170,13 @@ public void Returns_correct_document_for_resource() var linkConventions = new DefaultLinkConventions(); + var metadataObject = new JObject(); + metadataObject["baz"] = "qux"; + var metadata = new BasicMetadata(metadataObject); + // Act var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); - var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }); + var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }, metadata); // Assert document.PrimaryData.Id.Should().Be("4"); @@ -242,6 +247,8 @@ public void Returns_correct_document_for_resource() continentCountriesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/continents/1/relationships/countries"); continentCountriesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/continents/1/countries"); continentCountriesRelationship.Value.Linkage.Should().BeNull(); + + ((string) document.Metadata.MetaObject["baz"]).Should().Be("qux"); } [TestMethod] @@ -253,7 +260,7 @@ public void Returns_correct_document_for_null_resource() // Act var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); - var document = documentBuilder.BuildDocument(null, "http://www.example.com", null); + var document = documentBuilder.BuildDocument(null, "http://www.example.com", null, null); // Assert document.PrimaryData.Should().BeNull(); diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs index fd212875..b93beb9c 100644 --- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -84,7 +84,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage } // Single resource object - return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, null); + return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, null, null); } private static Type GetEnumerableElementType(Type collectionType) diff --git a/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs index 976fd730..cd35d6d7 100644 --- a/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs @@ -12,7 +12,8 @@ public interface ISingleResourceDocumentBuilder /// The string to prepend to link URLs. /// A list of dot-separated paths to include in the compound document. /// If this collection is null or empty, no linkage will be included. + /// Metadata to serialize at the top level of the document /// - ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions); + ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata); } } diff --git a/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs index 21e32a61..6930a5d3 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs @@ -19,13 +19,13 @@ public RegistryDrivenSingleResourceDocumentBuilder(IResourceTypeRegistry resourc { } - public ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions) + public ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata) { var idDictionariesByType = new Dictionary>(); var primaryDataResource = CreateResourceObject(primaryData, idDictionariesByType, null, includePathExpressions, linkBaseUrl); var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); - var document = new SingleResourceDocument(primaryDataResource, relatedData, null); + var document = new SingleResourceDocument(primaryDataResource, relatedData, topLevelMetadata); return document; } } diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 9e6c9e7a..28233ecc 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -91,7 +91,7 @@ public async Task GetRecordById(string id, HttpRequestM if (primaryResource == null) throw JsonApiException.CreateForNotFound(string.Format("No record exists with ID {0} for the requested type.", id)); var baseUrl = _baseUrlService.GetBaseUrl(request); - return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths); + return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); } public async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken) From 5dfaf4b8e02fc61c51ff62a13576c6f0cf6c5876 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 10 Jul 2015 19:37:33 -0400 Subject: [PATCH 120/186] add some documentation --- JSONAPI/Http/MappedDocumentMaterializer.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 28233ecc..b1b43a90 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -44,11 +44,22 @@ protected delegate Task MaterializeDocumentForToOneRela /// /// Gets a query returning all entities for this endpoint /// - /// protected abstract IQueryable GetQuery(); + + + /// + /// Gets a query for only the entity matching the given ID + /// protected abstract IQueryable GetByIdQuery(string id); + + /// + /// Gets a query for the DTOs based on the given entity query. + /// protected abstract IQueryable GetMappedQuery(IQueryable entityQuery, Expression>[] propertiesToInclude); + /// + /// Creates a new MappedDocumentMaterializer + /// protected MappedDocumentMaterializer( IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, IBaseUrlService baseUrlService, From 13b8c32241d63f446c16561dbf6d9a126b5d7e9a Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 10 Jul 2015 19:55:18 -0400 Subject: [PATCH 121/186] add single record filter to IDocumentMaterializer --- .../Http/EntityFrameworkDocumentMaterializer.cs | 8 ++++++++ JSONAPI/Http/IDocumentMaterializer.cs | 9 ++++++++- JSONAPI/Http/MappedDocumentMaterializer.cs | 14 +++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index e963129c..e37b4e69 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -69,6 +69,14 @@ public Task GetRecordsMatchingExpression(Expression return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); } + public async Task GetSingleRecordMatchingExpression(Expression> filter, HttpRequestMessage request, + CancellationToken cancellationToken) + { + var apiBaseUrl = GetBaseUrlFromRequest(request); + var singleResource = await Filter(filter).FirstOrDefaultAsync(cancellationToken); + return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null); + } + public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); diff --git a/JSONAPI/Http/IDocumentMaterializer.cs b/JSONAPI/Http/IDocumentMaterializer.cs index e0cbf418..7f211460 100644 --- a/JSONAPI/Http/IDocumentMaterializer.cs +++ b/JSONAPI/Http/IDocumentMaterializer.cs @@ -21,7 +21,14 @@ public interface IDocumentMaterializer where T : class /// /// Returns a document containing records matching the provided lambda expression. /// - Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken); + Task GetRecordsMatchingExpression(Expression> filter, + HttpRequestMessage request, CancellationToken cancellationToken); + + /// + /// Returns a document containing the first record matching the provided lambda expression. + /// + Task GetSingleRecordMatchingExpression(Expression> filter, + HttpRequestMessage request, CancellationToken cancellationToken); /// /// Returns a document with the resource identified by the given ID. diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index b1b43a90..4127957f 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -46,7 +46,6 @@ protected delegate Task MaterializeDocumentForToOneRela /// protected abstract IQueryable GetQuery(); - /// /// Gets a query for only the entity matching the given ID /// @@ -92,6 +91,19 @@ public async Task GetRecordsMatchingExpression(Expr return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths); } + public async Task GetSingleRecordMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) + { + var entityQuery = GetQuery(); + var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; + var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); + var mappedQuery = GetMappedQuery(entityQuery, includePaths); + var filteredQuery = mappedQuery.Where(filter); + var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(filteredQuery, cancellationToken); + + var baseUrl = _baseUrlService.GetBaseUrl(request); + return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); + } + public async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var entityQuery = GetByIdQuery(id); From 91ca6a36e468c85b29bec9e1a7a08d217b04c1f2 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 11 Jul 2015 17:58:22 -0400 Subject: [PATCH 122/186] rearrange acceptance test projects --- .../AcceptanceTestsBase.cs | 4 +- .../App.config | 37 +++ .../AttributeSerializationTests.cs | 5 +- .../ComputedIdTests.cs | 46 +++ .../CreatingResourcesTests.cs | 32 +- .../Data/Building.csv | 0 .../Data/Comment.csv | 0 .../Data/Company.csv | 0 .../Data/Language.csv | 0 .../Data/LanguageUserLink.csv | 0 .../Data/Post.csv | 0 .../Data/PostTagLink.csv | 0 .../Data/Tag.csv | 0 .../Data/User.csv | 0 .../Data/UserGroup.csv | 0 .../DeletingResourcesTests.cs | 12 +- .../ErrorsTests.cs | 6 +- .../FetchingResourcesTests.cs | 187 +++++++++++ ..._of_various_types_serialize_correctly.json | 0 ...of_resource_with_computed_id_Response.json | 0 ...ource_with_computed_id_by_id_Response.json | 0 .../Post_with_client_provided_id_Request.json | 0 .../Requests/Post_with_empty_id_Request.json | 0 ...Post_with_client_provided_id_Response.json | 0 .../Post_with_empty_id_Response.json | 0 .../Controller_action_throws_exception.json | 0 .../Errors/Controller_does_not_exist.json | 0 .../FetchingResources/GetAllResponse.json | 0 .../FetchingResources/GetByIdResponse.json | 0 .../GetWithFilterResponse.json | 0 .../Get_dasherized_resource.json | 0 ...ce_for_relationship_that_doesnt_exist.json | 0 ...o_many_for_resource_that_doesnt_exist.json | 0 .../Get_related_to_many_response.json | 0 ...to_one_for_resource_that_doesnt_exist.json | 0 .../Get_related_to_one_response.json | 0 .../Get_related_to_one_where_it_is_null.json | 0 .../Get_resource_by_id_that_doesnt_exist.json | 0 .../Responses/GetSearchResultsResponse.json | 0 .../Responses/GetSortedAscendingResponse.json | 0 .../GetSortedByMixedDirectionResponse.json | 0 .../GetSortedByMultipleAscendingResponse.json | 0 ...GetSortedByMultipleDescendingResponse.json | 0 .../GetSortedBySameColumnTwiceResponse.json | 0 .../GetSortedByUnknownColumnResponse.json | 0 .../GetSortedDescendingResponse.json | 0 .../PatchWithArrayForToOneLinkageRequest.json | 0 ...atchWithArrayRelationshipValueRequest.json | 0 .../PatchWithAttributeUpdateRequest.json | 0 .../PatchWithMissingToManyLinkageRequest.json | 0 .../PatchWithMissingToOneLinkageRequest.json | 0 .../PatchWithNullForToManyLinkageRequest.json | 0 .../PatchWithNullToOneUpdateRequest.json | 0 ...atchWithObjectForToManyLinkageRequest.json | 0 ...atchWithStringForToManyLinkageRequest.json | 0 ...PatchWithStringForToOneLinkageRequest.json | 0 ...tchWithStringRelationshipValueRequest.json | 0 ...chWithToManyEmptyLinkageUpdateRequest.json | 0 ...ithToManyHomogeneousDataUpdateRequest.json | 0 ...thToManyLinkageObjectMissingIdRequest.json | 0 ...ToManyLinkageObjectMissingTypeRequest.json | 0 .../PatchWithToManyUpdateRequest.json | 0 ...ithToOneLinkageObjectMissingIdRequest.json | 0 ...hToOneLinkageObjectMissingTypeRequest.json | 0 .../Requests/PatchWithToOneUpdateRequest.json | 0 .../Patch_with_unknown_attribute_Request.json | 0 ...tch_with_unknown_relationship_Request.json | 0 ...PatchWithArrayForToOneLinkageResponse.json | 0 ...tchWithArrayRelationshipValueResponse.json | 0 .../PatchWithAttributeUpdateResponse.json | 0 ...PatchWithMissingToManyLinkageResponse.json | 0 .../PatchWithMissingToOneLinkageResponse.json | 0 ...PatchWithNullForToManyLinkageResponse.json | 0 .../PatchWithNullToOneUpdateResponse.json | 0 ...tchWithObjectForToManyLinkageResponse.json | 0 ...tchWithStringForToManyLinkageResponse.json | 0 ...atchWithStringForToOneLinkageResponse.json | 0 ...chWithStringRelationshipValueResponse.json | 0 ...hWithToManyEmptyLinkageUpdateResponse.json | 0 ...thToManyHomogeneousDataUpdateResponse.json | 0 ...hToManyLinkageObjectMissingIdResponse.json | 0 ...oManyLinkageObjectMissingTypeResponse.json | 0 .../PatchWithToManyUpdateResponse.json | 0 ...thToOneLinkageObjectMissingIdResponse.json | 0 ...ToOneLinkageObjectMissingTypeResponse.json | 0 .../PatchWithToOneUpdateResponse.json | 0 ...Patch_with_unknown_attribute_Response.json | 0 ...ch_with_unknown_relationship_Response.json | 0 .../HeterogeneousTests.cs | 26 ++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 254 +++++++++++++++ .../Properties/AssemblyInfo.cs | 36 +++ .../SortingTests.cs | 122 ++++++++ .../TestHelpers.cs | 11 +- .../UpdatingResourcesTests.cs | 296 +++++++++--------- .../packages.config | 11 + .../Controllers/BuildingsController.cs | 0 .../Controllers/CitiesController.cs | 0 .../Controllers/CommentsController.cs | 0 .../Controllers/CompaniesController.cs | 0 .../LanguageUserLinksController.cs | 0 .../Controllers/PostsController.cs | 0 .../Controllers/PresidentsController.cs | 0 .../Controllers/SamplesController.cs | 0 .../Controllers/SearchController.cs | 0 .../Controllers/TagsController.cs | 0 .../Controllers/TreesController.cs | 0 .../Controllers/UserGroupsController.cs | 0 .../Controllers/UsersController.cs | 0 ...tityFrameworkResourceObjectMaterializer.cs | 0 ...anceTests.EntityFrameworkTestWebApp.csproj | 4 +- .../Models/Building.cs | 0 .../Models/City.cs | 0 .../Models/Comment.cs | 0 .../Models/Company.cs | 0 .../Models/Language.cs | 0 .../Models/LanguageUserLink.cs | 0 .../Models/Post.cs | 0 .../Models/Sample.cs | 0 .../Models/State.cs | 0 .../Models/Tag.cs | 0 .../Models/TestDbContext.cs | 0 .../Models/User.cs | 0 .../Models/UserGroup.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../Startup.cs | 0 .../Web.Debug.config | 0 .../Web.Release.config | 0 .../Web.config | 0 .../packages.config | 2 +- .../JSONAPI.Autofac.EntityFramework.csproj | 1 + JSONAPI.Autofac.EntityFramework/app.config | 15 + .../Acceptance/ComputedIdTests.cs | 46 --- .../Acceptance/DocumentTests.cs | 21 -- .../Acceptance/FetchingResourcesTests.cs | 187 ----------- ...t_returns_IResourceCollectionDocument.json | 12 - .../Acceptance/HeterogeneousTests.cs | 27 -- .../Acceptance/SortingTests.cs | 122 -------- JSONAPI.EntityFramework.Tests/App.Config | 7 +- .../JSONAPI.EntityFramework.Tests.csproj | 123 +------- JSONAPI.EntityFramework.Tests/packages.config | 2 +- JSONAPI.EntityFramework/App.config | 7 +- .../JSONAPI.EntityFramework.csproj | 4 +- JSONAPI.EntityFramework/packages.config | 2 +- .../JSONAPI.TodoMVC.API.csproj | 4 +- JSONAPI.TodoMVC.API/packages.config | 2 +- JSONAPI.sln | 8 +- JSONAPI/Properties/AssemblyInfo.cs | 2 +- 147 files changed, 953 insertions(+), 730 deletions(-) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/AcceptanceTestsBase.cs (97%) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/AttributeSerializationTests.cs (66%) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/CreatingResourcesTests.cs (60%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Building.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Comment.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Company.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Language.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/LanguageUserLink.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Post.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/PostTagLink.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/Tag.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/User.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Data/UserGroup.csv (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/DeletingResourcesTests.cs (72%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/ErrorsTests.cs (70%) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Errors/Controller_action_throws_exception.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Errors/Controller_does_not_exist.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/GetAllResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/GetByIdResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/GetWithFilterResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_dasherized_resource.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_to_many_response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_to_one_response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json (100%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json (100%) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs rename {JSONAPI.EntityFramework.Tests => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/TestHelpers.cs (61%) rename {JSONAPI.EntityFramework.Tests/Acceptance => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests}/UpdatingResourcesTests.cs (60%) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/BuildingsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/CitiesController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/CommentsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/CompaniesController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/LanguageUserLinksController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/PostsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/PresidentsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/SamplesController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/SearchController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/TagsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/TreesController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/UserGroupsController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Controllers/UsersController.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/CustomEntityFrameworkResourceObjectMaterializer.cs (100%) rename JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj (98%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Building.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/City.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Comment.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Company.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Language.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/LanguageUserLink.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Post.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Sample.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/State.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/Tag.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/TestDbContext.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/User.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Models/UserGroup.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Properties/AssemblyInfo.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Startup.cs (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Web.Debug.config (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Web.Release.config (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/Web.config (100%) rename {JSONAPI.EntityFramework.Tests.TestWebApp => JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp}/packages.config (93%) create mode 100644 JSONAPI.Autofac.EntityFramework/app.config delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/ComputedIdTests.cs delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs delete mode 100644 JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs similarity index 97% rename from JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs index 0488f29e..6e867609 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -12,7 +12,7 @@ using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] public abstract class AcceptanceTestsBase @@ -25,7 +25,7 @@ public abstract class AcceptanceTestsBase protected static DbConnection GetEffortConnection() { - return TestHelpers.GetEffortConnection(@"Acceptance\Data"); + return TestHelpers.GetEffortConnection(@"Data"); } protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config new file mode 100644 index 00000000..da9a789a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config @@ -0,0 +1,37 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs similarity index 66% rename from JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs index 698dee96..fd06bda9 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AttributeSerializationTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs @@ -1,9 +1,8 @@ using System.Net; using System.Threading.Tasks; -using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] public class AttributeSerializationTests : AcceptanceTestsBase @@ -15,7 +14,7 @@ public async Task Attributes_of_various_types_serialize_correctly() { var response = await SubmitGet(effortConnection, "samples"); - await AssertResponseContent(response, @"Acceptance\Fixtures\AttributeSerialization\Attributes_of_various_types_serialize_correctly.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\AttributeSerialization\Attributes_of_various_types_serialize_correctly.json", HttpStatusCode.OK); } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs new file mode 100644 index 00000000..64c09347 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs @@ -0,0 +1,46 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class ComputedIdTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Language.csv", @"Data")] + [DeploymentItem(@"Data\LanguageUserLink.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_all_of_resource_with_computed_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "language-user-links"); + + await AssertResponseContent(response, @"Fixtures\ComputedId\Responses\Get_all_of_resource_with_computed_id_Response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Language.csv", @"Data")] + [DeploymentItem(@"Data\LanguageUserLink.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_resource_with_computed_id_by_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "language-user-links/9001_402"); + + await AssertResponseContent(response, @"Fixtures\ComputedId\Responses\Get_resource_with_computed_id_by_id_Response.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs similarity index 60% rename from JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs index aa9911db..1fb92825 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/CreatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs @@ -6,24 +6,24 @@ using JSONAPI.EntityFramework.Tests.TestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] - public class PostsTests : AcceptanceTestsBase + public class CreatingResourcesTests : AcceptanceTestsBase { [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Post_with_client_provided_id() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\CreatingResources\Requests\Post_with_client_provided_id_Request.json"); + var response = await SubmitPost(effortConnection, "posts", @"Fixtures\CreatingResources\Requests\Post_with_client_provided_id_Request.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\CreatingResources\Responses\Post_with_client_provided_id_Response.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_client_provided_id_Response.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -40,18 +40,18 @@ public async Task Post_with_client_provided_id() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Post_with_empty_id() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPost(effortConnection, "posts", @"Acceptance\Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); + var response = await SubmitPost(effortConnection, "posts", @"Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\CreatingResources\Responses\Post_with_empty_id_Response.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_empty_id_Response.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Building.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Building.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Building.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Comment.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Comment.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Comment.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Comment.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Company.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Company.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Company.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Language.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Language.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Language.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Language.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/LanguageUserLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/LanguageUserLink.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/LanguageUserLink.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/LanguageUserLink.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Post.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Post.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Post.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Post.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/PostTagLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostTagLink.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/PostTagLink.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostTagLink.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Tag.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Tag.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Tag.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Tag.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/User.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/User.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/UserGroup.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/UserGroup.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/UserGroup.csv diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs similarity index 72% rename from JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs index 9d08272f..d85b3a11 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/DeletingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs @@ -5,17 +5,17 @@ using JSONAPI.EntityFramework.Tests.TestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] public class DeletingResourcesTests : AcceptanceTestsBase { [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\User.csv", @"Acceptance\Data")] public async Task Delete() { using (var effortConnection = GetEffortConnection()) diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs similarity index 70% rename from JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs index 164049b5..f1c663c6 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/ErrorsTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] public class ErrorsTests : AcceptanceTestsBase @@ -14,7 +14,7 @@ public async Task Controller_action_throws_exception() { var response = await SubmitGet(effortConnection, "trees"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Errors\Controller_action_throws_exception.json", HttpStatusCode.InternalServerError, true); + await AssertResponseContent(response, @"Fixtures\Errors\Controller_action_throws_exception.json", HttpStatusCode.InternalServerError, true); } } @@ -27,7 +27,7 @@ public async Task Controller_does_not_exist() { var response = await SubmitGet(effortConnection, "foo"); - await AssertResponseContent(response, @"Acceptance\Fixtures\Errors\Controller_does_not_exist.json", HttpStatusCode.NotFound, true); + await AssertResponseContent(response, @"Fixtures\Errors\Controller_does_not_exist.json", HttpStatusCode.NotFound, true); } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs new file mode 100644 index 00000000..5ec6cc98 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class FetchingResourcesTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetAll() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetAllResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetWithFilterResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetById() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/202"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetByIdResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_resource_by_id_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\UserGroup.csv", @"Data")] + public async Task Get_dasherized_resource() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "user-groups"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_dasherized_resource.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_many() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/comments"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_one_response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_one_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/author"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_one_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_many_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/tags"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_many_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_resource_for_relationship_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/bananas"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_resource_for_relationship_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Building.csv", @"Data")] + [DeploymentItem(@"Data\Company.csv", @"Data")] + public async Task Get_related_to_one_where_it_is_null() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "buildings/1001/owner"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_one_where_it_is_null.json", + HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_action_throws_exception.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_action_throws_exception.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_action_throws_exception.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_does_not_exist.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Errors/Controller_does_not_exist.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_does_not_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetAllResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetAllResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetAllResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetAllResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetByIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetByIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetByIdResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetByIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetWithFilterResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetWithFilterResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/GetWithFilterResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetWithFilterResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_dasherized_resource.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_dasherized_resource.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_dasherized_resource.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_dasherized_resource.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_many_response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs new file mode 100644 index 00000000..22c765ef --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class HeterogeneousTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Post.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Tag.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\User.csv", @"Acceptance\Data")] + public async Task Get() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "search?s=1"); + + await AssertResponseContent(response, @"Fixtures\Heterogeneous\Responses\GetSearchResultsResponse.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj new file mode 100644 index 00000000..f5a0554b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -0,0 +1,254 @@ + + + + Debug + AnyCPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB} + Library + Properties + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Effort.EF6.1.1.4\lib\net45\Effort.dll + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.dll + + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll + + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Hosting.3.0.0\lib\net45\Microsoft.Owin.Hosting.dll + + + ..\packages\Microsoft.Owin.Testing.3.0.0\lib\net45\Microsoft.Owin.Testing.dll + + + ..\packages\NMemory.1.0.1\lib\net45\NMemory.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {76dee472-723b-4be6-8b97-428ac326e30f} + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + + Always + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..af90b244 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3d646890-c7b9-4a90-9706-eb8378591814")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs new file mode 100644 index 00000000..7e11a58e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class SortingTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedAscending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedAscendingResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedDesending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedDescendingResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedByMultipleAscending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=last-name,first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedByMultipleDescending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=-last-name,-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedByMixedDirection() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=last-name,-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedByUnknownColumn() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=foobar"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetSortedBySameColumnTwice() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=first-name,first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest, true); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/TestHelpers.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs similarity index 61% rename from JSONAPI.EntityFramework.Tests/TestHelpers.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs index 3411ffdd..0bb78809 100644 --- a/JSONAPI.EntityFramework.Tests/TestHelpers.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs @@ -5,13 +5,20 @@ using Effort; using Effort.DataLoaders; -namespace JSONAPI.EntityFramework.Tests +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { internal static class TestHelpers { + // http://stackoverflow.com/questions/21175713/no-entity-framework-provider-found-for-the-ado-net-provider-with-invariant-name + private static volatile Type _dependency; + static TestHelpers() + { + _dependency = typeof(System.Data.Entity.SqlServer.SqlProviderServices); + } + public static string ReadEmbeddedFile(string path) { - var resourcePath = "JSONAPI.EntityFramework.Tests." + path.Replace("\\", ".").Replace("/", "."); + var resourcePath = "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests." + path.Replace("\\", ".").Replace("/", "."); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath)) { if (stream == null) throw new Exception("Could not find a file at the path: " + path); diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs similarity index 60% rename from JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs index c2141bb4..bd0d384b 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/UpdatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs @@ -7,24 +7,24 @@ using JSONAPI.EntityFramework.Tests.TestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace JSONAPI.EntityFramework.Tests.Acceptance +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { [TestClass] public class UpdatingResourcesTests : AcceptanceTestsBase { [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithAttributeUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -42,18 +42,18 @@ public async Task PatchWithAttributeUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Patch_with_unknown_attribute() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\Patch_with_unknown_attribute_Request.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\Patch_with_unknown_attribute_Request.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\Patch_with_unknown_attribute_Response.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\Patch_with_unknown_attribute_Response.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -71,18 +71,18 @@ public async Task Patch_with_unknown_attribute() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Patch_with_unknown_relationship() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\Patch_with_unknown_relationship_Request.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\Patch_with_unknown_relationship_Request.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\Patch_with_unknown_relationship_Response.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\Patch_with_unknown_relationship_Response.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -100,18 +100,18 @@ public async Task Patch_with_unknown_relationship() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToManyUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -129,18 +129,18 @@ public async Task PatchWithToManyUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToManyHomogeneousDataUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -158,18 +158,18 @@ public async Task PatchWithToManyHomogeneousDataUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToManyEmptyLinkageUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -187,18 +187,18 @@ public async Task PatchWithToManyEmptyLinkageUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -216,18 +216,18 @@ public async Task PatchWithToOneUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithNullToOneUpdate() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithNullToOneUpdateRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithNullToOneUpdateRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -245,18 +245,18 @@ public async Task PatchWithNullToOneUpdate() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithMissingToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithMissingToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithMissingToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -274,18 +274,18 @@ public async Task PatchWithMissingToOneLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToOneLinkageObjectMissingId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -303,18 +303,18 @@ public async Task PatchWithToOneLinkageObjectMissingId() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToOneLinkageObjectMissingType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -332,18 +332,18 @@ public async Task PatchWithToOneLinkageObjectMissingType() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithArrayForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithArrayForToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithArrayForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -361,18 +361,18 @@ public async Task PatchWithArrayForToOneLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithStringForToOneLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringForToOneLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringForToOneLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -390,18 +390,18 @@ public async Task PatchWithStringForToOneLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithMissingToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithMissingToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithMissingToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -419,18 +419,18 @@ public async Task PatchWithMissingToManyLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToManyLinkageObjectMissingId() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -448,18 +448,18 @@ public async Task PatchWithToManyLinkageObjectMissingId() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithToManyLinkageObjectMissingType() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -477,18 +477,18 @@ public async Task PatchWithToManyLinkageObjectMissingType() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithObjectForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithObjectForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithObjectForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -506,18 +506,18 @@ public async Task PatchWithObjectForToManyLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithStringForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) @@ -536,18 +536,18 @@ public async Task PatchWithStringForToManyLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithNullForToManyLinkage() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithNullForToManyLinkageRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithNullForToManyLinkageRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -565,18 +565,18 @@ public async Task PatchWithNullForToManyLinkage() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithArrayRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithArrayRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithArrayRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { @@ -594,18 +594,18 @@ public async Task PatchWithArrayRelationshipValue() } [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task PatchWithStringRelationshipValue() { using (var effortConnection = GetEffortConnection()) { - var response = await SubmitPatch(effortConnection, "posts/202", @"Acceptance\Fixtures\UpdatingResources\Requests\PatchWithStringRelationshipValueRequest.json"); + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringRelationshipValueRequest.json"); - await AssertResponseContent(response, @"Acceptance\Fixtures\UpdatingResources\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); using (var dbContext = new TestDbContext(effortConnection, false)) { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config new file mode 100644 index 00000000..f704445b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/BuildingsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CitiesController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CompaniesController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/LanguageUserLinksController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PresidentsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SamplesController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/SearchController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TreesController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UserGroupsController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj similarity index 98% rename from JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index a1613ed0..ff6011fa 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -54,11 +54,11 @@ False - ..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Building.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/City.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Comment.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Comment.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Company.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Language.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Language.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/LanguageUserLink.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/LanguageUserLink.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Sample.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/State.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Tag.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Tag.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/UserGroup.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Properties/AssemblyInfo.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.Debug.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Debug.config similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.Debug.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Debug.config diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.Release.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Release.config similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.Release.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Release.config diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config similarity index 93% rename from JSONAPI.EntityFramework.Tests.TestWebApp/packages.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config index 116d5bd9..2fb9670f 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/packages.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config @@ -4,7 +4,7 @@ - + diff --git a/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj index c80282f1..a4c1ab87 100644 --- a/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj +++ b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj @@ -49,6 +49,7 @@ + diff --git a/JSONAPI.Autofac.EntityFramework/app.config b/JSONAPI.Autofac.EntityFramework/app.config new file mode 100644 index 00000000..b678ca2c --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/ComputedIdTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/ComputedIdTests.cs deleted file mode 100644 index 73a5cf9c..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/ComputedIdTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class ComputedIdTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Language.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\LanguageUserLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_all_of_resource_with_computed_id() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "language-user-links"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\ComputedId\Responses\Get_all_of_resource_with_computed_id_Response.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Language.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\LanguageUserLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_resource_with_computed_id_by_id() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "language-user-links/9001_402"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\ComputedId\Responses\Get_resource_with_computed_id_by_id_Response.json", HttpStatusCode.OK); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs deleted file mode 100644 index 56e5372a..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/DocumentTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class DocumentTests : AcceptanceTestsBase - { - [TestMethod] - public async Task Get_returns_IResourceCollectionDocument() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "presidents"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Document\Responses\Get_returns_IResourceCollectionDocument.json", HttpStatusCode.OK); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs deleted file mode 100644 index ab32b9e0..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/FetchingResourcesTests.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class FetchingResourcesTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetAll() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetAllResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetWithFilter() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetWithFilterResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetById() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/202"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\GetByIdResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_resource_by_id_that_doesnt_exist() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/3000"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\UserGroup.csv", @"Acceptance\Data")] - public async Task Get_dasherized_resource() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "user-groups"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_dasherized_resource.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_many() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/201/comments"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_related_to_many_response.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_one() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/201/author"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_response.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_one_for_resource_that_doesnt_exist() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/3000/author"); - - await AssertResponseContent(response, - @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_for_resource_that_doesnt_exist.json", - HttpStatusCode.NotFound, true); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_to_many_for_resource_that_doesnt_exist() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/3000/tags"); - - await AssertResponseContent(response, - @"Acceptance\Fixtures\FetchingResources\Get_related_to_many_for_resource_that_doesnt_exist.json", - HttpStatusCode.NotFound, true); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get_related_resource_for_relationship_that_doesnt_exist() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "posts/201/bananas"); - - await AssertResponseContent(response, - @"Acceptance\Fixtures\FetchingResources\Get_related_resource_for_relationship_that_doesnt_exist.json", - HttpStatusCode.NotFound, true); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Building.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Company.csv", @"Acceptance\Data")] - public async Task Get_related_to_one_where_it_is_null() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "buildings/1001/owner"); - - await AssertResponseContent(response, - @"Acceptance\Fixtures\FetchingResources\Get_related_to_one_where_it_is_null.json", - HttpStatusCode.OK); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json deleted file mode 100644 index 23653849..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Document/Responses/Get_returns_IResourceCollectionDocument.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data": [ - { - "type": "users", - "id": "6500" - }, - { - "type": "users", - "id": "6501" - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs deleted file mode 100644 index 400bd900..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/HeterogeneousTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class HeterogeneousTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "search?s=1"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Heterogeneous\Responses\GetSearchResultsResponse.json", HttpStatusCode.OK); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs deleted file mode 100644 index 38364ae5..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/SortingTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class SortingTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedAscending() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedAscendingResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedDesending() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=-first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedDescendingResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedByMultipleAscending() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=last-name,first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedByMultipleDescending() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=-last-name,-first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedByMixedDirection() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=last-name,-first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.json", HttpStatusCode.OK); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedByUnknownColumn() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=foobar"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest, true); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetSortedBySameColumnTwice() - { - using (var effortConnection = GetEffortConnection()) - { - var response = await SubmitGet(effortConnection, "users?sort=first-name,first-name"); - - await AssertResponseContent(response, @"Acceptance\Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest, true); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/App.Config b/JSONAPI.EntityFramework.Tests/App.Config index 2a6fe587..75b64305 100644 --- a/JSONAPI.EntityFramework.Tests/App.Config +++ b/JSONAPI.EntityFramework.Tests/App.Config @@ -1,9 +1,9 @@  -
- + + @@ -49,5 +49,8 @@ + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index e5198d6a..2938cf78 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -44,11 +44,11 @@ False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False @@ -110,17 +110,6 @@ - - - - - - - - - - - @@ -132,115 +121,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - - - - - - Always - - - Always - - - Always - - - Always - - - - - - - - - - - - - - Designer - - - - - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - Always @@ -250,9 +135,9 @@ - + {76dee472-723b-4be6-8b97-428ac326e30f} - JSONAPI.EntityFramework.Tests.TestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp {E906356C-93F6-41F6-9A0D-73B8A99AA53C} diff --git a/JSONAPI.EntityFramework.Tests/packages.config b/JSONAPI.EntityFramework.Tests/packages.config index df24cfb7..a5ce68c2 100644 --- a/JSONAPI.EntityFramework.Tests/packages.config +++ b/JSONAPI.EntityFramework.Tests/packages.config @@ -1,7 +1,7 @@  - + diff --git a/JSONAPI.EntityFramework/App.config b/JSONAPI.EntityFramework/App.config index 22a39869..5dcb1e0c 100644 --- a/JSONAPI.EntityFramework/App.config +++ b/JSONAPI.EntityFramework/App.config @@ -1,15 +1,18 @@  -
- + + + + + diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index b8944aad..dcfd635e 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -40,11 +40,11 @@ False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False diff --git a/JSONAPI.EntityFramework/packages.config b/JSONAPI.EntityFramework/packages.config index eddb39d3..d19b6caa 100644 --- a/JSONAPI.EntityFramework/packages.config +++ b/JSONAPI.EntityFramework/packages.config @@ -1,6 +1,6 @@  - + diff --git a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj index 85ce0859..323ffe8d 100644 --- a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj +++ b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj @@ -49,11 +49,11 @@ False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll diff --git a/JSONAPI.TodoMVC.API/packages.config b/JSONAPI.TodoMVC.API/packages.config index 3ee10fcb..922aad91 100644 --- a/JSONAPI.TodoMVC.API/packages.config +++ b/JSONAPI.TodoMVC.API/packages.config @@ -2,7 +2,7 @@ - + diff --git a/JSONAPI.sln b/JSONAPI.sln index 4f7cf212..68419028 100644 --- a/JSONAPI.sln +++ b/JSONAPI.sln @@ -25,12 +25,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{021B87 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.TodoMVC.API", "JSONAPI.TodoMVC.API\JSONAPI.TodoMVC.API.csproj", "{ECFA3EC5-47B8-4060-925E-9205146D6562}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.EntityFramework.Tests.TestWebApp", "JSONAPI.EntityFramework.Tests.TestWebApp\JSONAPI.EntityFramework.Tests.TestWebApp.csproj", "{76DEE472-723B-4BE6-8B97-428AC326E30F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp", "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp\JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj", "{76DEE472-723B-4BE6-8B97-428AC326E30F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac", "JSONAPI.Autofac\JSONAPI.Autofac.csproj", "{AF7861F3-550B-4F70-A33E-1E5F48D39333}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac.EntityFramework", "JSONAPI.Autofac.EntityFramework\JSONAPI.Autofac.EntityFramework.csproj", "{64ABE648-EFCB-46EE-9E1A-E163F52BF372}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests", "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests\JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj", "{58AEF8B8-8D51-4175-AC96-BC622703E8BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Debug|Any CPU.Build.0 = Debug|Any CPU {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Release|Any CPU.ActiveCfg = Release|Any CPU {64ABE648-EFCB-46EE-9E1A-E163F52BF372}.Release|Any CPU.Build.0 = Release|Any CPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/JSONAPI/Properties/AssemblyInfo.cs b/JSONAPI/Properties/AssemblyInfo.cs index 492b29a6..6bef268b 100644 --- a/JSONAPI/Properties/AssemblyInfo.cs +++ b/JSONAPI/Properties/AssemblyInfo.cs @@ -24,7 +24,7 @@ [assembly: InternalsVisibleTo("JSONAPI.Tests")] [assembly: InternalsVisibleTo("JSONAPI.EntityFramework")] -[assembly: InternalsVisibleTo("JSONAPI.EntityFramework.Tests")] +[assembly: InternalsVisibleTo("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] // This assembly is the default dynamic assembly generated Castle DynamicProxy, // used by Moq. Paste in a single line. From e5f9eb22285c5004a0123e82d8e6029efd53ad5c Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 12 Jul 2015 18:05:03 -0400 Subject: [PATCH 123/186] Refactor configuration and document materialization systems --- .../AcceptanceTestsBase.cs | 11 +- .../CreatingResourcesTests.cs | 2 +- .../Data/Officer.csv | 6 + .../Data/Starship.csv | 4 + .../Data/StarshipClass.csv | 4 + .../Data/StarshipOfficerLink.csv | 6 + .../DeletingResourcesTests.cs | 2 +- ...ce_for_relationship_that_doesnt_exist.json | 2 +- .../Fixtures/Mapped/Responses/Get_all.json | 70 ++ .../Fixtures/Mapped/Responses/Get_by_id.json | 24 + .../Get_related_to_many_response.json | 72 ++ .../Get_related_to_one_response.json | 19 + .../Get_resource_by_id_that_doesnt_exist.json | 10 + .../HeterogeneousTests.cs | 10 +- ...sts.EntityFrameworkTestWebApp.Tests.csproj | 18 + .../MappedTests.cs | 79 ++ .../TestHelpers.cs | 1 + .../UpdatingResourcesTests.cs | 2 +- .../Controllers/BuildingsController.cs | 13 - .../Controllers/CitiesController.cs | 42 - .../Controllers/CommentsController.cs | 12 - .../Controllers/CompaniesController.cs | 13 - .../LanguageUserLinksController.cs | 13 - .../Controllers/MainController.cs | 12 + .../Controllers/PostsController.cs | 12 - .../Controllers/PresidentsController.cs | 36 - .../Controllers/SamplesController.cs | 4 +- .../Controllers/SearchController.cs | 4 +- .../Controllers/TagsController.cs | 12 - .../Controllers/TreesController.cs | 2 +- .../Controllers/UserGroupsController.cs | 12 - .../Controllers/UsersController.cs | 12 - ...tityFrameworkResourceObjectMaterializer.cs | 5 +- .../StarshipDocumentMaterializer.cs | 50 ++ ...icersRelatedResourceMaterializer - Copy.cs | 41 + ...shipOfficersRelatedResourceMaterializer.cs | 37 + ...anceTests.EntityFrameworkTestWebApp.csproj | 45 +- .../Models/Building.cs | 2 +- .../Models/City.cs | 2 +- .../Models/Comment.cs | 2 +- .../Models/Company.cs | 2 +- .../Models/Language.cs | 2 +- .../Models/LanguageUserLink.cs | 2 +- .../Models/Officer.cs | 14 + .../Models/Post.cs | 2 +- .../Models/Sample.cs | 2 +- .../Models/Starship.cs | 21 + .../Models/StarshipClass.cs | 12 + .../Models/StarshipDto.cs | 19 + .../Models/StarshipOfficerDto.cs | 18 + .../Models/StarshipOfficerLink.cs | 22 + .../Models/State.cs | 2 +- .../Models/Tag.cs | 2 +- .../Models/TestDbContext.cs | 6 +- .../Models/User.cs | 2 +- .../Models/UserGroup.cs | 2 +- .../Properties/AssemblyInfo.cs | 1 - .../Startup.cs | 107 ++- .../Web.config | 4 +- .../packages.config | 2 +- .../JSONAPI.Autofac.EntityFramework.csproj | 4 +- .../JsonApiAutofacConfigurationExtensions.cs | 12 - .../JsonApiAutofacEntityFrameworkModule.cs | 66 +- .../JSONAPI.Autofac.Tests.csproj | 89 ++ .../Properties/AssemblyInfo.cs | 36 + JSONAPI.Autofac.Tests/UnitTest1.cs | 14 + .../HttpConfigurationExtensions.cs | 17 - JSONAPI.Autofac/JSONAPI.Autofac.csproj | 15 +- .../JsonApiAutofacConfiguration.cs | 41 - JSONAPI.Autofac/JsonApiAutofacModule.cs | 109 ++- .../JsonApiConfigurationExtensions.cs | 17 + .../JsonApiHttpAutofacConfigurator.cs | 40 + JSONAPI.Autofac/packages.config | 2 +- .../JsonApiAutofacConfigurationExtensions.cs | 20 + .../EntityFrameworkDocumentMaterializer.cs | 73 +- ...ManyRelatedResourceDocumentMaterializer.cs | 81 ++ ...oOneRelatedResourceDocumentMaterializer.cs | 73 ++ .../JSONAPI.EntityFramework.csproj | 3 + .../DefaultFilteringTransformerTests.cs | 7 +- .../DefaultSortingTransformerTests.cs | 7 +- .../Core/ResourceTypeRegistrarTests.cs | 717 +++++++++++++++ .../Core/ResourceTypeRegistryTests.cs | 843 ++---------------- .../Http/PascalizedControllerSelectorTests.cs | 107 --- JSONAPI.Tests/JSONAPI.Tests.csproj | 6 +- JSONAPI.Tests/app.config | 8 + .../Controllers/MainController.cs | 12 + .../Controllers/TodosController.cs | 12 - .../JSONAPI.TodoMVC.API.csproj | 2 +- JSONAPI.TodoMVC.API/Startup.cs | 24 +- JSONAPI.sln | 6 + .../IResourceTypeConfiguration.cs | 50 ++ JSONAPI/Configuration/JsonApiConfiguration.cs | 108 +++ .../JsonApiHttpConfiguration.cs | 10 +- .../ResourceTypeConfiguration.cs | 92 ++ .../ResourceTypeRelationshipConfiguration.cs | 37 + JSONAPI/Core/DefaultNamingConventions.cs | 61 ++ JSONAPI/Core/INamingConventions.cs | 25 + JSONAPI/Core/IResourceTypeRegistrar.cs | 19 + JSONAPI/Core/IResourceTypeRegistration.cs | 5 + JSONAPI/Core/IResourceTypeRegistry.cs | 6 + JSONAPI/Core/JsonApiConfiguration.cs | 90 -- JSONAPI/Core/ResourceTypeAttribute.cs | 39 + JSONAPI/Core/ResourceTypeField.cs | 102 +-- JSONAPI/Core/ResourceTypeRegistrar.cs | 187 ++++ JSONAPI/Core/ResourceTypeRegistration.cs | 70 ++ JSONAPI/Core/ResourceTypeRegistry.cs | 361 +------- JSONAPI/Core/ResourceTypeRelationship.cs | 45 + .../Core/ToManyResourceTypeRelationship.cs | 17 + JSONAPI/Core/ToOneResourceTypeRelationship.cs | 17 + .../Core/TypeRegistrationNotFoundException.cs | 4 +- JSONAPI/Http/DocumentMaterializerLocator.cs | 41 + JSONAPI/Http/IDocumentMaterializer.cs | 24 +- JSONAPI/Http/IDocumentMaterializerLocator.cs | 25 + .../IRelatedResourceDocumentMaterializer.cs | 82 ++ JSONAPI/Http/JsonApiController.cs | 63 +- JSONAPI/Http/MappedDocumentMaterializer.cs | 104 +-- JSONAPI/Http/PascalizedControllerSelector.cs | 24 - JSONAPI/JSONAPI.csproj | 21 +- JSONAPI/Properties/AssemblyInfo.cs | 1 + 119 files changed, 3081 insertions(+), 2081 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs delete mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs delete mode 100644 JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs create mode 100644 JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj create mode 100644 JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs create mode 100644 JSONAPI.Autofac.Tests/UnitTest1.cs delete mode 100644 JSONAPI.Autofac/HttpConfigurationExtensions.cs delete mode 100644 JSONAPI.Autofac/JsonApiAutofacConfiguration.cs create mode 100644 JSONAPI.Autofac/JsonApiConfigurationExtensions.cs create mode 100644 JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs create mode 100644 JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs create mode 100644 JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs create mode 100644 JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs create mode 100644 JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs delete mode 100644 JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs create mode 100644 JSONAPI.TodoMVC.API/Controllers/MainController.cs delete mode 100644 JSONAPI.TodoMVC.API/Controllers/TodosController.cs create mode 100644 JSONAPI/Configuration/IResourceTypeConfiguration.cs create mode 100644 JSONAPI/Configuration/JsonApiConfiguration.cs rename JSONAPI/{Core => Configuration}/JsonApiHttpConfiguration.cs (82%) create mode 100644 JSONAPI/Configuration/ResourceTypeConfiguration.cs create mode 100644 JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs create mode 100644 JSONAPI/Core/DefaultNamingConventions.cs create mode 100644 JSONAPI/Core/INamingConventions.cs create mode 100644 JSONAPI/Core/IResourceTypeRegistrar.cs delete mode 100644 JSONAPI/Core/JsonApiConfiguration.cs create mode 100644 JSONAPI/Core/ResourceTypeAttribute.cs create mode 100644 JSONAPI/Core/ResourceTypeRegistrar.cs create mode 100644 JSONAPI/Core/ResourceTypeRegistration.cs create mode 100644 JSONAPI/Core/ResourceTypeRelationship.cs create mode 100644 JSONAPI/Core/ToManyResourceTypeRelationship.cs create mode 100644 JSONAPI/Core/ToOneResourceTypeRelationship.cs create mode 100644 JSONAPI/Http/DocumentMaterializerLocator.cs create mode 100644 JSONAPI/Http/IDocumentMaterializerLocator.cs create mode 100644 JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs delete mode 100644 JSONAPI/Http/PascalizedControllerSelector.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs index 6e867609..40aa3945 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -6,8 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using JSONAPI.Json; using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -58,7 +57,7 @@ protected async Task SubmitGet(DbConnection effortConnectio { using (var server = TestServer.Create(app => { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); + var startup = new Startup(() => new TestDbContext(effortConnection, false)); startup.Configuration(app); })) { @@ -75,7 +74,7 @@ protected async Task SubmitPost(DbConnection effortConnecti { using (var server = TestServer.Create(app => { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); + var startup = new Startup(() => new TestDbContext(effortConnection, false)); startup.Configuration(app); })) { @@ -100,7 +99,7 @@ protected async Task SubmitPatch(DbConnection effortConnect { using (var server = TestServer.Create(app => { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); + var startup = new Startup(() => new TestDbContext(effortConnection, false)); startup.Configuration(app); })) { @@ -124,7 +123,7 @@ protected async Task SubmitDelete(DbConnection effortConnec { using (var server = TestServer.Create(app => { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); + var startup = new Startup(() => new TestDbContext(effortConnection, false)); startup.Configuration(app); })) { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs index 1fb92825..40a973df 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv new file mode 100644 index 00000000..3d36dcbe --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv @@ -0,0 +1,6 @@ +OfficerId,Name,Rank +"12000","James T. Kirk","Captain" +"12010","Jean-Luc Picard","Captain" +"12011","William T. Riker","Commander" +"12012","Data","Lt. Commander" +"12013","Deanna Troi","Lt. Commander" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv new file mode 100644 index 00000000..7214a1df --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv @@ -0,0 +1,4 @@ +StarshipId,Name,StarshipClassId +"NCC-1701","USS Enterprise","80001" +"NCC-1701-D","USS Enterprise","80002" +"NCC-74656","USS Voyager","80003" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv new file mode 100644 index 00000000..4aeb6903 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv @@ -0,0 +1,4 @@ +StarshipClassId,Name +"80001","Constitution" +"80002","Galaxy" +"80003","Intrepid" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv new file mode 100644 index 00000000..be97ff49 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv @@ -0,0 +1,6 @@ +StarshipId,OfficerId,Position +"NCC-1701","12000","Commanding Officer" +"NCC-1701-D","12010","Commanding Officer" +"NCC-1701-D","12011","First Officer" +"NCC-1701-D","12012","Second Officer" +"NCC-1701-D","12013","Ship's Counselor" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs index d85b3a11..c5902256 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs @@ -2,7 +2,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json index ec74d25a..44e502d2 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "404", "title": "Resource not found", - "detail": "No relationship `bananas` exists for the resource with type `posts` and id `201`." + "detail": "No relationship `bananas` exists for the resource type `posts`." } ] } \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json new file mode 100644 index 00000000..07c6389e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json @@ -0,0 +1,70 @@ +{ + "data": [ + { + "type": "starships", + "id": "NCC-1701", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Constitution" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701/ship-counselor" + } + } + } + }, + { + "type": "starships", + "id": "NCC-1701-D", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Galaxy" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701-D/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701-D/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701-D/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701-D/ship-counselor" + } + } + } + }, + { + "type": "starships", + "id": "NCC-74656", + "attributes": { + "name": "USS Voyager", + "starship-class": "Intrepid" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-74656/relationships/officers", + "related": "https://www.example.com/starships/NCC-74656/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-74656/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-74656/ship-counselor" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json new file mode 100644 index 00000000..d891b913 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json @@ -0,0 +1,24 @@ +{ + "data": { + "type": "starships", + "id": "NCC-1701", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Constitution" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701/ship-counselor" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json new file mode 100644 index 00000000..c1221a27 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json @@ -0,0 +1,72 @@ +{ + "data": [ + { + "type": "starship-officers", + "id": "NCC-1701-D_12010", + "attributes": { + "name": "Jean-Luc Picard", + "position": "Commanding Officer", + "rank": "Captain" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12010/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12010/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12011", + "attributes": { + "name": "William T. Riker", + "position": "First Officer", + "rank": "Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12011/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12011/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12012", + "attributes": { + "name": "Data", + "position": "Second Officer", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12012/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12012/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12013", + "attributes": { + "name": "Deanna Troi", + "position": "Ship's Counselor", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12013/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12013/current-ship" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json new file mode 100644 index 00000000..f311d73c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "starship-officers", + "id": "NCC-1701-D_12013", + "attributes": { + "name": "Deanna Troi", + "position": "Ship's Counselor", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12013/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12013/current-ship" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json new file mode 100644 index 00000000..57931946 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No record exists with type `starships` and ID `NCC-asdf`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs index 22c765ef..013baa6b 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs @@ -8,11 +8,11 @@ namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests public class HeterogeneousTests : AcceptanceTestsBase { [TestMethod] - [DeploymentItem(@"Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Get() { using (var effortConnection = GetEffortConnection()) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index f5a0554b..7f2a1699 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -96,6 +96,7 @@ + @@ -122,6 +123,18 @@ + + Always + + + Always + + + Always + + + Always + Always @@ -215,6 +228,11 @@ + + + + + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs new file mode 100644 index 00000000..9ecf7f77 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class MappedTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_all() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_all.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_by_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_by_id.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_resource_by_id_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-asdf"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + [DeploymentItem(@"Data\Officer.csv", @"Data")] + [DeploymentItem(@"Data\StarshipOfficerLink.csv", @"Data")] + public async Task Get_related_to_many() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701-D/officers"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_related_to_many_response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + [DeploymentItem(@"Data\Officer.csv", @"Data")] + [DeploymentItem(@"Data\StarshipOfficerLink.csv", @"Data")] + public async Task Get_related_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701-D/ship-counselor"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_related_to_one_response.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs index 0bb78809..dbafdaee 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs @@ -10,6 +10,7 @@ namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests internal static class TestHelpers { // http://stackoverflow.com/questions/21175713/no-entity-framework-provider-found-for-the-ado-net-provider-with-invariant-name + // ReSharper disable once NotAccessedField.Local private static volatile Type _dependency; static TestHelpers() { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs index bd0d384b..9d898537 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs @@ -4,7 +4,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs deleted file mode 100644 index 63bd0756..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/BuildingsController.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class BuildingsController : JsonApiController - { - public BuildingsController(IDocumentMaterializer documentMaterializer) - : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs deleted file mode 100644 index d94a3232..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CitiesController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Web.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class CitiesController : ApiController - { - public IHttpActionResult Get(string id) - { - City city; - if (id == "9000") - { - city = - new City - { - Id = "9000", - Name = "Seattle", - State = new State - { - Id = "4000", - Name = "Washington" - } - }; - } - else if (id == "9001") - { - city = - new City - { - Id = "9001", - Name = "Tacoma" - }; - } - else - { - return NotFound(); - } - - return Ok(city); - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs deleted file mode 100644 index 52aa1b7c..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CommentsController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class CommentsController : JsonApiController - { - public CommentsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs deleted file mode 100644 index dde3dccc..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/CompaniesController.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class CompaniesController : JsonApiController - { - public CompaniesController(IDocumentMaterializer documentMaterializer) - : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs deleted file mode 100644 index 24e99835..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/LanguageUserLinksController.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class LanguageUserLinksController : JsonApiController - { - public LanguageUserLinksController(IDocumentMaterializer documentMaterializer) - : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs new file mode 100644 index 00000000..6c786a1c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs @@ -0,0 +1,12 @@ +using JSONAPI.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers +{ + public class MainController : JsonApiController + { + public MainController(IDocumentMaterializerLocator documentMaterializerLocator) + : base(documentMaterializerLocator) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs deleted file mode 100644 index f67dc20f..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PostsController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class PostsController : JsonApiController - { - public PostsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs deleted file mode 100644 index bf788105..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/PresidentsController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using System.Web.Http; -using JSONAPI.Documents; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class PresidentsController : ApiController - { - // This endpoint exists to demonstrate returning IResourceCollectionDocument - [Route("presidents")] - public IHttpActionResult GetPresidents() - { - var users = new[] - { - new User - { - Id = "6500", - FirstName = "George", - LastName = "Washington" - }, - new User - { - Id = "6501", - FirstName = "Abraham", - LastName = "Lincoln" - } - }; - - var userResources = users.Select(u => (IResourceObject)new ResourceObject("users", u.Id)).ToArray(); - - var document = new ResourceCollectionDocument(userResources, null, null); - return Ok(document); - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs index 5e2d8448..66588415 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs @@ -1,8 +1,8 @@ using System; using System.Web.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers { public class SamplesController : ApiController { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs index c58afd28..0d4c1667 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Threading.Tasks; using System.Web.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers { public class SearchController : ApiController { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs deleted file mode 100644 index 0901f3a8..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TagsController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class TagsController : JsonApiController - { - public TagsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs index 53e1d564..6a018db0 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs @@ -1,7 +1,7 @@ using System; using System.Web.Http; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers { public class TreesController : ApiController { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs deleted file mode 100644 index a4886e21..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UserGroupsController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class UserGroupsController : JsonApiController - { - public UserGroupsController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs deleted file mode 100644 index 462b7a04..00000000 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/UsersController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class UsersController : JsonApiController - { - public UsersController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs index a324c919..2a2c6190 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs @@ -1,11 +1,12 @@ using System; using System.Data.Entity; using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using JSONAPI.Core; using JSONAPI.Documents; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; +using JSONAPI.EntityFramework; -namespace JSONAPI.EntityFramework.Tests.TestWebApp +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp { public class CustomEntityFrameworkResourceObjectMaterializer : EntityFrameworkResourceObjectMaterializer { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs new file mode 100644 index 00000000..1b8d9e1b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipDocumentMaterializer : MappedDocumentMaterializer + { + private readonly TestDbContext _dbContext; + + public StarshipDocumentMaterializer( + TestDbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + IQueryableEnumerationTransformer queryableEnumerationTransformer, IResourceTypeRegistry resourceTypeRegistry) + : base( + queryableResourceCollectionDocumentBuilder, baseUrlService, singleResourceDocumentBuilder, + queryableEnumerationTransformer, resourceTypeRegistry) + { + _dbContext = dbContext; + } + + protected override IQueryable GetQuery() + { + return _dbContext.Starships; + } + + protected override IQueryable GetByIdQuery(string id) + { + return GetQuery().Where(s => s.StarshipId == id); + } + + protected override IQueryable GetMappedQuery(IQueryable entityQuery, Expression>[] propertiesToInclude) + { + return entityQuery.Select(s => new StarshipDto + { + Id = s.StarshipId, + Name = s.Name, + StarshipClass = s.StarshipClass.Name + }); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs new file mode 100644 index 00000000..5de51bd3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs @@ -0,0 +1,41 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipShipCounselorRelatedResourceMaterializer : EntityFrameworkToOneRelatedResourceDocumentMaterializer + { + private readonly DbContext _dbContext; + + public StarshipShipCounselorRelatedResourceMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService, + IResourceTypeRegistration primaryTypeRegistration, ResourceTypeRelationship relationship, + DbContext dbContext) + : base(singleResourceDocumentBuilder, baseUrlService, primaryTypeRegistration, relationship, dbContext) + { + _dbContext = dbContext; + } + + protected override async Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + { + var query = _dbContext.Set().Where(s => s.StarshipId == primaryResourceId) + .SelectMany(s => s.OfficerLinks) + .Where(l => l.Position == "Ship's Counselor") + .Select(l => new StarshipOfficerDto + { + Id = l.StarshipId + "_" + l.OfficerId, + Name = l.Officer.Name, + Rank = l.Officer.Rank, + Position = l.Position + }); + return await query.FirstOrDefaultAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs new file mode 100644 index 00000000..750a1faa --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs @@ -0,0 +1,37 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToManyRelatedResourceDocumentMaterializer + { + private readonly DbContext _dbContext; + + public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IResourceTypeRegistration primaryTypeRegistration) + : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, primaryTypeRegistration) + { + _dbContext = dbContext; + } + + protected override Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken) + { + var query = _dbContext.Set().Where(s => s.StarshipId == primaryResourceId).SelectMany(s => s.OfficerLinks) + .Select(l => new StarshipOfficerDto + { + Id = l.StarshipId + "_" + l.OfficerId, + Name = l.Officer.Name, + Rank = l.Officer.Rank, + Position = l.Position + }); + return Task.FromResult(query); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index ff6011fa..d8c3c30a 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -11,8 +11,8 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} Library Properties - JSONAPI.EntityFramework.Tests.TestWebApp - JSONAPI.EntityFramework.Tests.TestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp v4.5 true @@ -40,16 +40,20 @@ 4 - + + False ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - + + False ..\packages\Autofac.Owin.3.1.0\lib\net45\Autofac.Integration.Owin.dll - + + False ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll - + + False ..\packages\Autofac.WebApi2.Owin.3.2.0\lib\net45\Autofac.Integration.WebApi.Owin.dll @@ -67,8 +71,9 @@ ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll ..\packages\Owin.1.0\lib\net40\Owin.dll @@ -112,30 +117,32 @@ - + + Designer + - - - - + - - - - - - + + + + + + + + + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs index e7c720b9..3bfd590e 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Building { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs index e1ab9d36..43283f17 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using JSONAPI.Attributes; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class City { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs index 31d70a7f..f031485a 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Comment { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs index 05061cf7..55a4d6e7 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Company { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs index c0082cb1..4a8ec476 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Language { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs index d71e6091..8a895ea6 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class LanguageUserLink { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs new file mode 100644 index 00000000..e167d4be --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Officer + { + [Key] + public string OfficerId { get; set; } + + public string Name { get; set; } + + public string Rank { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs index 04ba1ad9..5c750537 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Post { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs index 33b91019..c56ea671 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs @@ -1,7 +1,7 @@ using System; using JSONAPI.Attributes; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public enum SampleEnum { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs new file mode 100644 index 00000000..96a55e60 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Starship + { + [Key] + public string StarshipId { get; set; } + + public string Name { get; set; } + + public string StarshipClassId { get; set; } + + [ForeignKey("StarshipClassId")] + public virtual StarshipClass StarshipClass { get; set; } + + public virtual ICollection OfficerLinks { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs new file mode 100644 index 00000000..028180c6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class StarshipClass + { + [Key] + public string StarshipClassId { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs new file mode 100644 index 00000000..e081fe44 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + [JsonObject(Title = "starship")] + public class StarshipDto + { + public string Id { get; set; } + + public string Name { get; set; } + + public string StarshipClass { get; set; } + + public virtual ICollection Officers { get; set; } + + public virtual StarshipOfficerDto ShipCounselor { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs new file mode 100644 index 00000000..ca18f6db --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + [JsonObject(Title = "starship-officer")] + public class StarshipOfficerDto + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Rank { get; set; } + + public string Position { get; set; } + + public virtual StarshipDto CurrentShip { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs new file mode 100644 index 00000000..26b5704b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class StarshipOfficerLink + { + [Key, Column(Order = 0)] + public string StarshipId { get; set; } + + [Key, Column(Order = 1)] + public string OfficerId { get; set; } + + [ForeignKey("StarshipId")] + public virtual Starship Starship { get; set; } + + [ForeignKey("OfficerId")] + public virtual Officer Officer { get; set; } + + public string Position { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs index e0203ca4..3b7ed57f 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class State { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs index b8529429..9d9f25b6 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Tag { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs index b6580b76..7efbb549 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs @@ -2,7 +2,7 @@ using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class TestDbContext : DbContext { @@ -43,5 +43,9 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) public DbSet Tags { get; set; } public DbSet Users { get; set; } public DbSet LanguageUserLinks { get; set; } + public DbSet Starships { get; set; } + public DbSet StarshipClasses { get; set; } + public DbSet Officers { get; set; } + public DbSet StarshipOfficerLinks { get; set; } } } \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs index eca0edda..54596d11 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class User { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs index 9d523f66..fd154f87 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class UserGroup { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs index ababced3..02097fc4 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index 8ccacfa5..6a2343cc 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -2,94 +2,91 @@ using System.Data.Entity; using System.Linq.Expressions; using System.Reflection; -using System.Web; using System.Web.Http; using Autofac; using Autofac.Integration.WebApi; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using JSONAPI.Autofac; using JSONAPI.Autofac.EntityFramework; -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using Microsoft.Owin; +using JSONAPI.Configuration; +using JSONAPI.EntityFramework; using Owin; -namespace JSONAPI.EntityFramework.Tests.TestWebApp +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp { public class Startup { - private const string DbContextKey = "TestWebApp.DbContext"; - - private readonly Func _dbContextFactory; + private readonly Func _dbContextFactory; public Startup() - : this(context => new TestDbContext()) + : this(() => new TestDbContext()) { } - public Startup(Func dbContextFactory) + public Startup(Func dbContextFactory) { _dbContextFactory = dbContextFactory; } public void Configuration(IAppBuilder app) { - // Setup db context for use in DI - app.Use(async (context, next) => - { - TestDbContext dbContext = _dbContextFactory(context); - context.Set(DbContextKey, dbContext); - - await next(); - - dbContext.Dispose(); - }); - - var pluralizationService = new EntityFrameworkPluralizationService(); - var namingConventions = new DefaultNamingConventions(pluralizationService); - - var configuration = new JsonApiAutofacConfiguration(namingConventions); - configuration.RegisterResourceType(typeof(Building)); - configuration.RegisterResourceType(typeof(City)); - configuration.RegisterResourceType(typeof(Comment)); - configuration.RegisterResourceType(typeof(Company)); - configuration.RegisterResourceType(typeof(Language)); - configuration.RegisterResourceType(typeof(LanguageUserLink), - sortByIdFactory: LanguageUserLinkSortByIdFactory, - filterByIdFactory: LanguageUserLinkFilterByIdFactory); - configuration.RegisterResourceType(typeof(Post)); - configuration.RegisterResourceType(typeof(Sample)); - configuration.RegisterResourceType(typeof(State)); - configuration.RegisterResourceType(typeof(Tag)); - configuration.RegisterResourceType(typeof(User)); - configuration.RegisterResourceType(typeof(UserGroup)); - var module = configuration.GetAutofacModule(); - var efModule = configuration.GetEntityFrameworkAutofacModule(); - var containerBuilder = new ContainerBuilder(); - containerBuilder.RegisterModule(module); - containerBuilder.RegisterModule(efModule); - containerBuilder.Register(c => HttpContext.Current.GetOwinContext()).As(); - containerBuilder.Register(c => c.Resolve().Get(DbContextKey)).AsSelf().As(); - containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + containerBuilder.Register(c => _dbContextFactory()) + .AsSelf() + .As() + .InstancePerRequest(); + containerBuilder.RegisterModule(); containerBuilder.RegisterType() .As(); + containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); var container = containerBuilder.Build(); + var configuration = new JsonApiConfiguration(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(c => + { + c.OverrideDefaultFilterById(LanguageUserLinkFilterByIdFactory); + c.OverrideDefaultSortById(LanguageUserLinkSortByIdFactory); + }); + configuration.RegisterResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterResourceType(); // Example of a resource type not controlled by EF + configuration.RegisterMappedType(c => + { + c.ConfigureRelationship(s => s.Officers, + rc => rc.UseMaterializer()); + c.ConfigureRelationship(s => s.ShipCounselor, + rc => rc.UseMaterializer()); + }); // Example of a resource that is mapped from a DB entity + configuration.RegisterResourceType(); + + var configurator = new JsonApiHttpAutofacConfigurator(container); + configurator.OnApplicationLifetimeScopeBegun(applicationLifetimeScope => + { + // TODO: is this a candidate for spinning into a JSONAPI.Autofac.WebApi.Owin package? Yuck + app.UseAutofacMiddleware(applicationLifetimeScope); + }); + var httpConfig = new HttpConfiguration { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always }; - httpConfig.UseJsonApiWithAutofac(container); - - // Web API routes - httpConfig.Routes.MapHttpRoute("ResourceCollection", "{controller}"); - httpConfig.Routes.MapHttpRoute("Resource", "{controller}/{id}"); - httpConfig.Routes.MapHttpRoute("RelatedResource", "{controller}/{id}/{relationshipName}"); - app.UseAutofacMiddleware(container); + // Additional Web API routes + httpConfig.Routes.MapHttpRoute("Samples", "samples", new { Controller = "Samples" }); + httpConfig.Routes.MapHttpRoute("Search", "search", new { Controller = "Search" }); + httpConfig.Routes.MapHttpRoute("Trees", "trees", new { Controller = "Trees" }); + configurator.Apply(httpConfig, configuration); app.UseWebApi(httpConfig); app.UseAutofacWebApi(httpConfig); } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config index 1bc02ea7..2d263f08 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config @@ -5,7 +5,7 @@ --> - +
@@ -22,7 +22,7 @@ - + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config index 2fb9670f..bba30ec4 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj index a4c1ab87..b7ee5706 100644 --- a/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj +++ b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj @@ -32,7 +32,8 @@ 4 - + + False ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll @@ -44,7 +45,6 @@ - diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs deleted file mode 100644 index 39803e65..00000000 --- a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacConfigurationExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Autofac.Core; - -namespace JSONAPI.Autofac.EntityFramework -{ - public static class JsonApiAutofacConfigurationExtensions - { - public static IModule GetEntityFrameworkAutofacModule(this JsonApiAutofacConfiguration jsonApiAutofacConfiguration) - { - return new JsonApiAutofacEntityFrameworkModule(); - } - } -} diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs index f0a940fa..481e823e 100644 --- a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs @@ -1,8 +1,11 @@ -using Autofac; -using JSONAPI.ActionFilters; +using System; +using System.Linq; +using Autofac; +using JSONAPI.Core; using JSONAPI.EntityFramework; using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; using JSONAPI.QueryableTransformers; namespace JSONAPI.Autofac.EntityFramework @@ -12,10 +15,65 @@ public class JsonApiAutofacEntityFrameworkModule : Module protected override void Load(ContainerBuilder builder) { builder.RegisterType().As(); - builder.RegisterGeneric(typeof(EntityFrameworkDocumentMaterializer<>)) - .AsImplementedInterfaces(); builder.RegisterType() .As(); + + builder.RegisterGeneric(typeof (EntityFrameworkDocumentMaterializer<>)); + builder.Register((ctx, parameters) => + { + var allParameters = parameters.ToArray(); + var typedParameters = allParameters.OfType().ToArray(); + var resourceTypeRegistrationParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(IResourceTypeRegistration)); + if (resourceTypeRegistrationParameter == null) + throw new Exception( + "An IResourceTypeRegistration parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRegistration = resourceTypeRegistrationParameter.Value as IResourceTypeRegistration; + if (resourceTypeRegistration == null) + throw new Exception( + "An IResourceTypeRegistration parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var openGenericType = typeof (EntityFrameworkDocumentMaterializer<>); + var materializerType = openGenericType.MakeGenericType(resourceTypeRegistration.Type); + return ctx.Resolve(materializerType, allParameters); + }).As(); + + builder.RegisterGeneric(typeof(EntityFrameworkToManyRelatedResourceDocumentMaterializer<,>)); + builder.RegisterGeneric(typeof(EntityFrameworkToOneRelatedResourceDocumentMaterializer<,>)); + builder.Register((ctx, parameters) => + { + var allParameters = parameters.ToArray(); + var typedParameters = allParameters.OfType().ToArray(); + var resourceTypeRegistrationParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(IResourceTypeRegistration)); + if (resourceTypeRegistrationParameter == null) + throw new Exception( + "An IResourceTypeRegistration parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRegistration = resourceTypeRegistrationParameter.Value as IResourceTypeRegistration; + if (resourceTypeRegistration == null) + throw new Exception( + "An IResourceTypeRegistration parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var resourceTypeRelationshipParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(ResourceTypeRelationship)); + if (resourceTypeRelationshipParameter == null) + throw new Exception( + "A ResourceTypeRelationship parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRelationship = resourceTypeRelationshipParameter.Value as ResourceTypeRelationship; + if (resourceTypeRelationship == null) + throw new Exception( + "A ResourceTypeRelationship parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var openGenericType = resourceTypeRelationship.IsToMany + ? typeof (EntityFrameworkToManyRelatedResourceDocumentMaterializer<,>) + : typeof (EntityFrameworkToOneRelatedResourceDocumentMaterializer<,>); + var materializerType = openGenericType.MakeGenericType(resourceTypeRegistration.Type, + resourceTypeRelationship.RelatedType); + return ctx.Resolve(materializerType, allParameters); + }).As(); } } } diff --git a/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj b/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj new file mode 100644 index 00000000..8d82db0c --- /dev/null +++ b/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj @@ -0,0 +1,89 @@ + + + + Debug + AnyCPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1} + Library + Properties + JSONAPI.Autofac.Tests + JSONAPI.Autofac.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs b/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e8bddb2b --- /dev/null +++ b/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5755d01a-25ed-4be5-9f24-c9a659699558")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac.Tests/UnitTest1.cs b/JSONAPI.Autofac.Tests/UnitTest1.cs new file mode 100644 index 00000000..d7da0c41 --- /dev/null +++ b/JSONAPI.Autofac.Tests/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Autofac.Tests +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/JSONAPI.Autofac/HttpConfigurationExtensions.cs b/JSONAPI.Autofac/HttpConfigurationExtensions.cs deleted file mode 100644 index 0a456b4e..00000000 --- a/JSONAPI.Autofac/HttpConfigurationExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Web.Http; -using Autofac; -using Autofac.Integration.WebApi; -using JSONAPI.Core; - -namespace JSONAPI.Autofac -{ - public static class HttpConfigurationExtensions - { - public static void UseJsonApiWithAutofac(this HttpConfiguration httpConfig, ILifetimeScope applicationLifetimeScope) - { - var jsonApiConfiguration = applicationLifetimeScope.Resolve(); - jsonApiConfiguration.Apply(httpConfig); - httpConfig.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); - } - } -} diff --git a/JSONAPI.Autofac/JSONAPI.Autofac.csproj b/JSONAPI.Autofac/JSONAPI.Autofac.csproj index e2fccfaa..d9dcafba 100644 --- a/JSONAPI.Autofac/JSONAPI.Autofac.csproj +++ b/JSONAPI.Autofac/JSONAPI.Autofac.csproj @@ -32,14 +32,17 @@ 4 - + + False ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - + + False ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll - - ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll @@ -59,8 +62,8 @@ - - + + diff --git a/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs b/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs deleted file mode 100644 index 03a6e942..00000000 --- a/JSONAPI.Autofac/JsonApiAutofacConfiguration.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using Autofac.Core; -using JSONAPI.Core; -using JSONAPI.Documents; - -namespace JSONAPI.Autofac -{ - public class JsonApiAutofacConfiguration - { - private readonly INamingConventions _namingConventions; - private readonly List> _registrationActions; - private ILinkConventions _linkConventions; - - public JsonApiAutofacConfiguration(INamingConventions namingConventions) - { - if (namingConventions == null) throw new ArgumentNullException("namingConventions"); - - _namingConventions = namingConventions; - _registrationActions = new List>(); - } - - public void RegisterResourceType(Type resourceType, string resourceTypeName = null, - Func filterByIdFactory = null, Func sortByIdFactory = null) - { - _registrationActions.Add( - registry => registry.RegisterResourceType(resourceType, resourceTypeName, filterByIdFactory, sortByIdFactory)); - } - - public void OverrideLinkConventions(ILinkConventions linkConventions) - { - _linkConventions = linkConventions; - } - - public IModule GetAutofacModule() - { - return new JsonApiAutofacModule(_namingConventions, _linkConventions, _registrationActions); - } - } -} diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 8db5a9b7..fa88f4e5 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -1,7 +1,8 @@ using System; -using System.Collections.Generic; using Autofac; +using Autofac.Core; using JSONAPI.ActionFilters; +using JSONAPI.Configuration; using JSONAPI.Core; using JSONAPI.Documents; using JSONAPI.Documents.Builders; @@ -13,33 +14,98 @@ namespace JSONAPI.Autofac { public class JsonApiAutofacModule : Module { - private readonly INamingConventions _namingConventions; - private readonly ILinkConventions _linkConventions; - private readonly IEnumerable> _registrationActions; + private readonly IJsonApiConfiguration _jsonApiConfiguration; - public JsonApiAutofacModule(INamingConventions namingConventions, ILinkConventions linkConventions, - IEnumerable> registrationActions) + internal JsonApiAutofacModule(IJsonApiConfiguration jsonApiConfiguration) { - _namingConventions = namingConventions; - _linkConventions = linkConventions; - _registrationActions = registrationActions; + _jsonApiConfiguration = jsonApiConfiguration; } protected override void Load(ContainerBuilder builder) { - // Registry - builder.Register(c => _namingConventions).As().SingleInstance(); - builder.RegisterType().AsSelf().SingleInstance(); + // Register resource types + var registry = new ResourceTypeRegistry(); + foreach (var resourceTypeConfiguration in _jsonApiConfiguration.ResourceTypeConfigurations) + { + var resourceTypeRegistration = resourceTypeConfiguration.BuildResourceTypeRegistration(); + registry.AddRegistration(resourceTypeRegistration); + + var configuration = resourceTypeConfiguration; + builder.Register(c => configuration) + .Keyed(resourceTypeRegistration.Type) + .Keyed(resourceTypeRegistration.ResourceTypeName) + .SingleInstance(); + + if (resourceTypeConfiguration.DocumentMaterializerType != null) + builder.RegisterType(resourceTypeConfiguration.DocumentMaterializerType); + + foreach (var relationshipConfiguration in resourceTypeConfiguration.RelationshipConfigurations) + { + var prop = relationshipConfiguration.Key; + var relationship = relationshipConfiguration.Value; + builder.RegisterType(relationship.MaterializerType); + } + } + + builder.Register(c => registry).As().SingleInstance(); builder.Register(c => { - var registry = c.Resolve(); - foreach (var registrationAction in _registrationActions) - registrationAction(registry); - return registry; - }).As().SingleInstance(); + var context = c.Resolve(); + Func factory = resourceTypeName => + { + var configuration = context.ResolveKeyed(resourceTypeName); + var registration = registry.GetRegistrationForResourceTypeName(resourceTypeName); + var parameters = new Parameter[] { new TypedParameter(typeof (IResourceTypeRegistration), registration) }; + if (configuration.DocumentMaterializerType != null) + return (IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters); + return context.Resolve(parameters); + }; + return factory; + }); + builder.Register(c => + { + var context = c.Resolve(); + Func factory = clrType => + { + var configuration = context.ResolveKeyed(clrType); + var registration = registry.GetRegistrationForType(clrType); + var parameters = new Parameter[] { new TypedParameter(typeof(IResourceTypeRegistration), registration) }; + if (configuration.DocumentMaterializerType != null) + return (IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters); + return context.Resolve(parameters); + }; + return factory; + }); + builder.Register(c => + { + var context = c.Resolve(); + Func factory = (resourceTypeName, relationshipName) => + { + var configuration = context.ResolveKeyed(resourceTypeName); + var registration = registry.GetRegistrationForResourceTypeName(resourceTypeName); + var relationship = registration.GetFieldByName(relationshipName) as ResourceTypeRelationship; + if (relationship == null) + throw JsonApiException.CreateForNotFound( + string.Format("No relationship `{0}` exists for the resource type `{1}`.", relationshipName, resourceTypeName)); + + var parameters = new Parameter[] + { + new TypedParameter(typeof(IResourceTypeRegistration), registration), + new TypedParameter(typeof(ResourceTypeRelationship), relationship) + }; + + IResourceTypeRelationshipConfiguration relationshipConfiguration; + if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property, + out relationshipConfiguration) && relationshipConfiguration.MaterializerType != null) + return (IRelatedResourceDocumentMaterializer)context.Resolve(relationshipConfiguration.MaterializerType, parameters); + return context.Resolve(parameters); + }; + return factory; + }); - builder.RegisterType(); - builder.RegisterType().As(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().InstancePerRequest(); // Serialization builder.RegisterType().As().SingleInstance(); @@ -58,9 +124,8 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - // document building - var linkConventions = _linkConventions ?? new DefaultLinkConventions(); - builder.Register(c => linkConventions).As().SingleInstance(); + // Document building + builder.Register(c => _jsonApiConfiguration.LinkConventions).As().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); diff --git a/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs b/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs new file mode 100644 index 00000000..56622b0c --- /dev/null +++ b/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs @@ -0,0 +1,17 @@ +using System.Web.Http; +using Autofac; +using Autofac.Integration.WebApi; +using JSONAPI.Configuration; + +namespace JSONAPI.Autofac +{ + public static class JsonApiConfigurationExtensions + { + public static void SetupHttpConfigurationUsingAutofac(this IJsonApiConfiguration configuration, + HttpConfiguration httpConfiguration, ILifetimeScope parentLifetimeScope) + { + var configurator = new JsonApiHttpAutofacConfigurator(parentLifetimeScope); + configurator.Apply(httpConfiguration, configuration); + } + } +} diff --git a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs new file mode 100644 index 00000000..7b9d48a9 --- /dev/null +++ b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs @@ -0,0 +1,40 @@ +using System; +using System.Web.Http; +using Autofac; +using Autofac.Integration.WebApi; +using JSONAPI.Configuration; + +namespace JSONAPI.Autofac +{ + public class JsonApiHttpAutofacConfigurator + { + private readonly ILifetimeScope _lifetimeScope; + private Action _appLifetimeScopeBegunAction; + + public JsonApiHttpAutofacConfigurator(ILifetimeScope lifetimeScope) + { + _lifetimeScope = lifetimeScope; + } + + public void OnApplicationLifetimeScopeBegun(Action appLifetimeScopeBegunAction) + { + _appLifetimeScopeBegunAction = appLifetimeScopeBegunAction; + } + + public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jsonApiConfiguration) + { + var applicationLifetimeScope = _lifetimeScope.BeginLifetimeScope(containerBuilder => + { + var module = new JsonApiAutofacModule(jsonApiConfiguration); + containerBuilder.RegisterModule(module); + }); + + if (_appLifetimeScopeBegunAction != null) + _appLifetimeScopeBegunAction(applicationLifetimeScope); + + var jsonApiHttpConfiguration = applicationLifetimeScope.Resolve(); + jsonApiHttpConfiguration.Apply(httpConfiguration); + httpConfiguration.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); + } + } +} diff --git a/JSONAPI.Autofac/packages.config b/JSONAPI.Autofac/packages.config index dc5afbb9..2ad7611f 100644 --- a/JSONAPI.Autofac/packages.config +++ b/JSONAPI.Autofac/packages.config @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs new file mode 100644 index 00000000..65be9bf8 --- /dev/null +++ b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using System; +using JSONAPI.Configuration; +using JSONAPI.EntityFramework.Http; + +namespace JSONAPI.Autofac.EntityFramework +{ + public static class JsonApiAutofacConfigurationExtensions + { + public static void RegisterEntityFrameworkResourceType(this JsonApiConfiguration jsonApiConfiguration, + Action> configurationAction = null) where TResourceType : class + { + jsonApiConfiguration.RegisterResourceType(c => + { + c.UseDocumentMaterializer>(); + if (configurationAction != null) + configurationAction(c); + }); + } + } +} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index e37b4e69..844ce89a 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Linq.Expressions; using System.Net.Http; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using JSONAPI.Core; @@ -17,44 +16,32 @@ namespace JSONAPI.EntityFramework.Http /// /// Implementation of IDocumentMaterializer for use with Entity Framework. /// - public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer where T : class + public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer where T : class { private readonly DbContext _dbContext; - private readonly IResourceTypeRegistry _resourceTypeRegistry; + private readonly IResourceTypeRegistration _resourceTypeRegistration; private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; private readonly IBaseUrlService _baseUrlService; - private readonly MethodInfo _getRelatedToManyMethod; - private readonly MethodInfo _getRelatedToOneMethod; /// /// Creates a new EntityFrameworkDocumentMaterializer /// - /// - /// - /// - /// - /// - /// public EntityFrameworkDocumentMaterializer( DbContext dbContext, - IResourceTypeRegistry resourceTypeRegistry, + IResourceTypeRegistration resourceTypeRegistration, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, IBaseUrlService baseUrlService) { _dbContext = dbContext; - _resourceTypeRegistry = resourceTypeRegistry; + _resourceTypeRegistration = resourceTypeRegistration; _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; _baseUrlService = baseUrlService; - _getRelatedToManyMethod = GetType() - .GetMethod("GetRelatedToMany", BindingFlags.NonPublic | BindingFlags.Instance); - _getRelatedToOneMethod = GetType() - .GetMethod("GetRelatedToOne", BindingFlags.NonPublic | BindingFlags.Instance); } public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) @@ -63,54 +50,16 @@ public virtual Task GetRecords(HttpRequestMessage r return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); } - public Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) - { - var query = _dbContext.Set().AsQueryable().Where(filter); - return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); - } - - public async Task GetSingleRecordMatchingExpression(Expression> filter, HttpRequestMessage request, - CancellationToken cancellationToken) - { - var apiBaseUrl = GetBaseUrlFromRequest(request); - var singleResource = await Filter(filter).FirstOrDefaultAsync(cancellationToken); - return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null); - } - public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); - var singleResource = await FilterById(id, registration).FirstOrDefaultAsync(cancellationToken); + var singleResource = await FilterById(id, _resourceTypeRegistration).FirstOrDefaultAsync(cancellationToken); if (singleResource == null) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", - registration.ResourceTypeName, id)); + _resourceTypeRegistration.ResourceTypeName, id)); return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null); } - public virtual async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, - CancellationToken cancellationToken) - { - var registration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); - var relationship = (ResourceTypeRelationship) registration.GetFieldByName(relationshipKey); - if (relationship == null) - throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", - relationshipKey, registration.ResourceTypeName, id)); - - if (relationship.IsToMany) - { - var method = _getRelatedToManyMethod.MakeGenericMethod(relationship.RelatedType); - var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); - return await result; - } - else - { - var method = _getRelatedToOneMethod.MakeGenericMethod(relationship.RelatedType); - var result = (Task)method.Invoke(this, new object[] { id, relationship, request, cancellationToken }); - return await result; - } - } - public virtual async Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken) { @@ -165,18 +114,17 @@ protected virtual async Task MaterializeAsync(IResourceObject resourceOb protected async Task GetRelatedToMany(string id, ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) { - var primaryEntityRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); var param = Expression.Parameter(typeof(T)); var accessorExpr = Expression.Property(param, relationship.Property); var lambda = Expression.Lambda>>(accessorExpr, param); - var primaryEntityQuery = FilterById(id, primaryEntityRegistration); + var primaryEntityQuery = FilterById(id, _resourceTypeRegistration); // We have to see if the resource even exists, so we can throw a 404 if it doesn't var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); if (relatedResource == null) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", - primaryEntityRegistration.ResourceTypeName, id)); + _resourceTypeRegistration.ResourceTypeName, id)); var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda); @@ -189,16 +137,15 @@ protected async Task GetRelatedToMany(str protected async Task GetRelatedToOne(string id, ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) { - var primaryEntityRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof(T)); var param = Expression.Parameter(typeof(T)); var accessorExpr = Expression.Property(param, relationship.Property); var lambda = Expression.Lambda>(accessorExpr, param); - var primaryEntityQuery = FilterById(id, primaryEntityRegistration); + var primaryEntityQuery = FilterById(id, _resourceTypeRegistration); var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken); if (!primaryEntityExists) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", - primaryEntityRegistration.ResourceTypeName, id)); + _resourceTypeRegistration.ResourceTypeName, id)); var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null, null); } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..e2078758 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of for use with Entity Framework + /// + public class EntityFrameworkToManyRelatedResourceDocumentMaterializer : + QueryableToManyRelatedResourceDocumentMaterializer where TPrimaryResource : class + { + private readonly IResourceTypeRegistration _primaryTypeRegistration; + private readonly ResourceTypeRelationship _relationship; + private readonly DbContext _dbContext; + + /// + /// Builds a new EntityFrameworkToManyRelatedResourceDocumentMaterializer. + /// + public EntityFrameworkToManyRelatedResourceDocumentMaterializer( + ResourceTypeRelationship relationship, + DbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IResourceTypeRegistration primaryTypeRegistration) + : base(queryableResourceCollectionDocumentBuilder) + { + _relationship = relationship; + _dbContext = dbContext; + _primaryTypeRegistration = primaryTypeRegistration; + } + + protected override async Task> GetRelatedQuery(string primaryResourceId, + CancellationToken cancellationToken) + { + var param = Expression.Parameter(typeof (TPrimaryResource)); + var accessorExpr = Expression.Property(param, _relationship.Property); + var lambda = Expression.Lambda>>(accessorExpr, param); + + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); + + // We have to see if the resource even exists, so we can throw a 404 if it doesn't + var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); + if (relatedResource == null) + throw JsonApiException.CreateForNotFound(string.Format( + "No resource of type `{0}` exists with id `{1}`.", + _primaryTypeRegistration.ResourceTypeName, primaryResourceId)); + + return primaryEntityQuery.SelectMany(lambda); + } + + private IQueryable FilterById(string id, + IResourceTypeRegistration resourceTypeRegistration, + params Expression>[] includes) where TResource : class + { + var param = Expression.Parameter(typeof (TResource)); + var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); + var predicate = Expression.Lambda>(filterByIdExpression, param); + return Filter(predicate, includes); + } + + private IQueryable Filter(Expression> predicate, + params Expression>[] includes) where TResource : class + { + IQueryable query = _dbContext.Set(); + if (includes != null && includes.Any()) + query = includes.Aggregate(query, (current, include) => current.Include(include)); + + if (predicate != null) + query = query.Where(predicate); + + return query.AsQueryable(); + } + } +} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..178ccd39 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,73 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of for use with Entity Framework + /// + public class EntityFrameworkToOneRelatedResourceDocumentMaterializer : + QueryableToOneRelatedResourceDocumentMaterializer where TPrimaryResource : class + { + private readonly IResourceTypeRegistration _primaryTypeRegistration; + private readonly ResourceTypeRelationship _relationship; + private readonly DbContext _dbContext; + + /// + /// Builds a new EntityFrameworkToOneRelatedResourceDocumentMaterializer + /// + public EntityFrameworkToOneRelatedResourceDocumentMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService, + IResourceTypeRegistration primaryTypeRegistration, ResourceTypeRelationship relationship, + DbContext dbContext) + : base(singleResourceDocumentBuilder, baseUrlService) + { + _primaryTypeRegistration = primaryTypeRegistration; + _relationship = relationship; + _dbContext = dbContext; + } + + protected override async Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + { + var param = Expression.Parameter(typeof(TPrimaryResource)); + var accessorExpr = Expression.Property(param, _relationship.Property); + var lambda = Expression.Lambda>(accessorExpr, param); + + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); + var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken); + if (!primaryEntityExists) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", + _primaryTypeRegistration.ResourceTypeName, primaryResourceId)); + return await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); + } + + private IQueryable Filter(Expression> predicate, + params Expression>[] includes) where TResource : class + { + IQueryable query = _dbContext.Set(); + if (includes != null && includes.Any()) + query = includes.Aggregate(query, (current, include) => current.Include(include)); + + if (predicate != null) + query = query.Where(predicate); + + return query.AsQueryable(); + } + + private IQueryable FilterById(string id, IResourceTypeRegistration resourceTypeRegistration, + params Expression>[] includes) where TResource : class + { + var param = Expression.Parameter(typeof(TResource)); + var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); + var predicate = Expression.Lambda>(filterByIdExpression, param); + return Filter(predicate, includes); + } + } +} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index dcfd635e..0f1dd5c8 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -74,6 +74,9 @@ + + + diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index e75850d5..e1677758 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -542,9 +542,10 @@ private DefaultFilteringTransformer GetTransformer() { {"Dummy", "Dummies"} }); - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(pluralizationService)); - registry.RegisterResourceType(typeof(Dummy)); - registry.RegisterResourceType(typeof(RelatedItemWithId)); + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService)); + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy))); + registry.AddRegistration(registrar.BuildRegistration(typeof(RelatedItemWithId))); return new DefaultFilteringTransformer(registry); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 197fc613..a2e7e4b3 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Net.Http; using FluentAssertions; -using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; using JSONAPI.QueryableTransformers; @@ -52,8 +51,10 @@ private DefaultSortingTransformer GetTransformer() { {"Dummy", "Dummies"} }); - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(pluralizationService)); - registry.RegisterResourceType(typeof(Dummy)); + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService)); + var registration = registrar.BuildRegistration(typeof(Dummy)); + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(registration); return new DefaultSortingTransformer(registry); } diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs new file mode 100644 index 00000000..c63c44d3 --- /dev/null +++ b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs @@ -0,0 +1,717 @@ +using System; +using JSONAPI.Attributes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using System.Reflection; +using System.Collections.Generic; +using System.Collections; +using System.Diagnostics; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class ResourceTypeRegistrarTests + { + private class InvalidModel // No Id discernable! + { + public string Data { get; set; } + } + + private class CustomIdModel + { + [UseAsId] + public Guid Uuid { get; set; } + + public string Data { get; set; } + } + + private class Salad + { + public string Id { get; set; } + + [JsonProperty("salad-type")] + public string TheSaladType { get; set; } + + [JsonProperty("salad-type")] + public string AnotherSaladType { get; set; } + } + + private class Continent + { + [UseAsId] + public string Name { get; set; } + + public string Id { get; set; } + } + + private class Boat + { + public string Id { get; set; } + + public string Type { get; set; } + } + + [TestMethod] + public void Cant_register_type_with_missing_id() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(InvalidModel)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Unable to determine Id property for type `InvalidModel`."); + } + + [TestMethod] + public void Cant_register_type_with_non_id_property_called_id() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(Continent)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Continent` because it contains a non-id property that would serialize as \"id\"."); + } + + [TestMethod] + public void Cant_register_type_with_property_called_type() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(Boat)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Boat` because it contains a property that would serialize as \"type\"."); + } + + [TestMethod] + public void Cant_register_type_with_two_properties_with_the_same_name() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + Type saladType = typeof(Salad); + + // Act + Action action = () => registrar.BuildRegistration(saladType); + + // Assert + action.ShouldThrow().Which.Message.Should() + .Be("Failed to register type `Salad` because contains multiple properties that would serialize as `salad-type`."); + } + + [TestMethod] + public void BuildRegistration_sets_up_registration_correctly() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var postReg = registrar.BuildRegistration(typeof(Post)); + + // Assert + postReg.IdProperty.Should().BeSameAs(typeof(Post).GetProperty("Id")); + postReg.ResourceTypeName.Should().Be("posts"); + postReg.Attributes.Length.Should().Be(1); + postReg.Attributes.First().Property.Should().BeSameAs(typeof(Post).GetProperty("Title")); + postReg.Relationships.Length.Should().Be(2); + postReg.Relationships[0].IsToMany.Should().BeFalse(); + postReg.Relationships[0].Property.Should().BeSameAs(typeof(Post).GetProperty("Author")); + postReg.Relationships[0].SelfLinkTemplate.Should().BeNull(); + postReg.Relationships[0].RelatedResourceLinkTemplate.Should().BeNull(); + postReg.Relationships[1].IsToMany.Should().BeTrue(); + postReg.Relationships[1].Property.Should().BeSameAs(typeof(Post).GetProperty("Comments")); + postReg.Relationships[1].SelfLinkTemplate.Should().Be("/posts/{1}/relationships/comments"); + postReg.Relationships[1].RelatedResourceLinkTemplate.Should().Be("/posts/{1}/comments"); + } + + private AttributeGrabBag InitializeGrabBag() + { + return new AttributeGrabBag() + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SbyteField = 123, + NullableSbyteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + Uint16Field = 64000, + NullableUint16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + Uint32Field = 3000000000, + NullableUint32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + Uint64Field = 9223372036854775808, + NullableUint64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" + }; + } + + private void AssertAttribute(IResourceTypeRegistration reg, string attributeName, + JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func getPropertyFunc) + { + var grabBag = InitializeGrabBag(); + + var field = reg.GetFieldByName(attributeName); + var attribute = (ResourceTypeAttribute) field; + attribute.JsonKey.Should().Be(attributeName); + + attribute.SetValue(grabBag, tokenToSet); + var propertyValueAfterSet = getPropertyFunc(grabBag); + propertyValueAfterSet.Should().Be(expectedPropertyValue); + + var convertedToken = attribute.GetValue(grabBag); + if (expectedTokenAfterSet == null) + convertedToken.Should().BeNull(); + else + { + var convertedTokenValue = convertedToken.Value(); + convertedTokenValue.Should().Be(expectedTokenAfterSet); + } + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_boolean_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "boolean-field", false, false, false, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", true, true, true, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", null, false, false, g => g.BooleanField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_boolean_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-boolean-field", false, false, false, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", true, true, true, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", null, null, (Boolean?) null, g => g.NullableBooleanField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_SByte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "sbyte-field", 0, 0, 0, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", 12, 12, 12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", -12, -12, -12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", null, 0, 0, g => g.SbyteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_SByte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-sbyte-field", 0, (SByte?)0, (SByte?)0, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", 12, (SByte?)12, (SByte?)12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", -12, (SByte?)-12, (SByte?)-12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", null, null, (SByte?)null, g => g.NullableSbyteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Byte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "byte-field", 0, 0, 0, g => g.ByteField); + AssertAttribute(reg, "byte-field", 12, 12, 12, g => g.ByteField); + AssertAttribute(reg, "byte-field", null, 0, 0, g => g.ByteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Byte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-byte-field", 0, (Byte?)0, (Byte?)0, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", 12, (Byte?)12, (Byte?)12, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", null, null, (Byte?)null, g => g.NullableByteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int16-field", 0, 0, 0, g => g.Int16Field); + AssertAttribute(reg, "int16-field", 4000, 4000, 4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", -4000, -4000, -4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", null, 0, 0, g => g.Int16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int16-field", 0, (Int16?)0, (Int16?)0, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", 4000, (Int16?)4000, (Int16?)4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", -4000, (Int16?)-4000, (Int16?)-4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", null, null, (Int16?)null, g => g.NullableInt16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint16-field", 0, 0, 0, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", 4000, 4000, 4000, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", null, 0, 0, g => g.Uint16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint16-field", 0, (UInt16?)0, (UInt16?)0, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", 4000, (UInt16?)4000, (UInt16?)4000, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", null, null, (UInt16?)null, g => g.NullableUint16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int32-field", 0, 0, 0, g => g.Int32Field); + AssertAttribute(reg, "int32-field", 2000000, 2000000, 2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", -2000000, -2000000, -2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", null, 0, 0, g => g.Int32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int32-field", 0, 0, (Int32?)0, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", 2000000, 2000000, (Int32?)2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", -2000000, -2000000, (Int32?)-2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", null, null, (Int32?)null, g => g.NullableInt32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint32-field", 0, (UInt32)0, (UInt32)0, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", 2000000, (UInt32)2000000, (UInt32)2000000, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", null, (UInt32)0, (UInt32)0, g => g.Uint32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint32-field", 0, (UInt32?)0, (UInt32?)0, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", 2000000, (UInt32?)2000000, (UInt32?)2000000, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", null, null, (UInt32?)null, g => g.NullableUint32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int64-field", 0, 0, 0, g => g.Int64Field); + AssertAttribute(reg, "int64-field", 20000000000, 20000000000, 20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", -20000000000, -20000000000, -20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", null, 0, 0, g => g.Int64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int64-field", 0, 0, (Int64?)0, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", 20000000000, 20000000000, (Int64?)20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", -20000000000, -20000000000, (Int64?)-20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", null, null, (Int64?)null, g => g.NullableInt64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint64-field", 0, (UInt64)0, (UInt64)0, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", 20000000000, (UInt64)20000000000, (UInt64)20000000000, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", null, (UInt64)0, (UInt64)0, g => g.Uint64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint64-field", 0, (UInt64?)0, (UInt64?)0, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", 20000000000, (UInt64?)20000000000, (UInt64?)20000000000, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", null, null, (UInt64?)null, g => g.NullableUint64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Single_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "single-field", 0f, 0f, 0f, g => g.SingleField); + AssertAttribute(reg, "single-field", 20000000000.1234f, 20000000000.1234f, 20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", null, 0, 0, g => g.SingleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Single_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-single-field", 0f, 0f, 0f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", 20000000000.1234f, 20000000000.1234f, (Int64?)20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", null, null, (Single?)null, g => g.NullableSingleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Double_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "double-field", 0d, 0d, 0d, g => g.DoubleField); + AssertAttribute(reg, "double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.DoubleField); + AssertAttribute(reg, "double-field", null, 0d, 0d, g => g.DoubleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Double_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-double-field", 0d, 0d, 0d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", null, null, (Double?)null, g => g.NullableDoubleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Decimal_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "decimal-field", "0", 0m, "0", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", null, 0m, "0", g => g.DecimalField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Decimal_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-decimal-field", "0", 0m, "0", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", null, null, (string)null, g => g.NullableDecimalField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_guid_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.GuidField); + AssertAttribute(reg, "guid-field", null, new Guid(), "00000000-0000-0000-0000-000000000000", g => g.GuidField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_guid_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "nullable-guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.NullableGuidField); + AssertAttribute(reg, "nullable-guid-field", null, null, (Guid?)null, g => g.NullableGuidField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_DateTime_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", null, new DateTime(), "0001-01-01T00:00:00", g => g.DateTimeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_DateTime_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", null, null, (DateTime?)null, g => g.NullableDateTimeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_DateTimeOffset_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset1 = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + var testDateTimeOffset2 = new DateTimeOffset(new DateTime(1776, 07, 04, 12, 30, 0), TimeSpan.FromHours(0)); + var testDateTimeOffset3 = new DateTimeOffset(new DateTime(2015, 03, 11, 04, 31, 0), TimeSpan.FromHours(0)); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset1, "1776-07-04T00:00:00.0000000-05:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T12:30:00+00:00", testDateTimeOffset2, "1776-07-04T12:30:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "2015-03-11T04:31:00.0000000+00:00", testDateTimeOffset3, "2015-03-11T04:31:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", null, new DateTimeOffset(), "0001-01-01T00:00:00.0000000+00:00", g => g.DateTimeOffsetField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_DateTimeOffset_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + AssertAttribute(reg, "nullable-date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset, "1776-07-04T00:00:00.0000000-05:00", + g => g.NullableDateTimeOffsetField); + AssertAttribute(reg, "nullable-date-time-offset-field", null, null, (DateTimeOffset?)null, + g => g.NullableDateTimeOffsetField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_string_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "string-field", "asdf", "asdf", "asdf", g => g.StringField); + AssertAttribute(reg, "string-field", null, null, (string)null, g => g.StringField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_enum_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.EnumField); + AssertAttribute(reg, "enum-field", null, (SampleEnum)0, 0, g => g.EnumField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_enum_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField); + AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField); + } + } +} diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs index 773def7d..7ffdb336 100644 --- a/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs +++ b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs @@ -1,787 +1,55 @@ using System; -using JSONAPI.Attributes; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using FluentAssertions; using JSONAPI.Core; using JSONAPI.Tests.Models; -using System.Reflection; -using System.Collections.Generic; -using System.Collections; -using System.Diagnostics; -using System.Linq; -using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; namespace JSONAPI.Tests.Core { [TestClass] public class ResourceTypeRegistryTests { - private class InvalidModel // No Id discernable! - { - public string Data { get; set; } - } - - private class CustomIdModel - { - [UseAsId] - public Guid Uuid { get; set; } - - public string Data { get; set; } - } - private class DerivedPost : Post { - - } - - private class Salad - { - public string Id { get; set; } - - [JsonProperty("salad-type")] - public string TheSaladType { get; set; } - - [JsonProperty("salad-type")] - public string AnotherSaladType { get; set; } - } - - private class Continent - { - [UseAsId] - public string Name { get; set; } - - public string Id { get; set; } - } - - private class Boat - { - public string Id { get; set; } - - public string Type { get; set; } - } - - [TestMethod] - public void Cant_register_type_with_missing_id() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - Action action = () => registry.RegisterResourceType(typeof(InvalidModel)); - - // Assert - action.ShouldThrow() - .Which.Message.Should() - .Be("Unable to determine Id property for type `InvalidModel`."); - } - - [TestMethod] - public void Cant_register_type_with_non_id_property_called_id() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - Action action = () => registry.RegisterResourceType(typeof(Continent)); - - // Assert - action.ShouldThrow() - .Which.Message.Should() - .Be("Failed to register type `Continent` because it contains a non-id property that would serialize as \"id\"."); - } - - [TestMethod] - public void Cant_register_type_with_property_called_type() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - Action action = () => registry.RegisterResourceType(typeof(Boat)); - - // Assert - action.ShouldThrow() - .Which.Message.Should() - .Be("Failed to register type `Boat` because it contains a property that would serialize as \"type\"."); - } - - [TestMethod] - public void Cant_register_type_with_two_properties_with_the_same_name() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - Type saladType = typeof(Salad); - - // Act - Action action = () => registry.RegisterResourceType(saladType); - - // Assert - action.ShouldThrow().Which.Message.Should() - .Be("Failed to register type `Salad` because contains multiple properties that would serialize as `salad-type`."); - } - - [TestMethod] - public void RegisterResourceType_sets_up_registration_correctly() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(Post)); - var postReg = registry.GetRegistrationForType(typeof(Post)); - - // Assert - postReg.IdProperty.Should().BeSameAs(typeof(Post).GetProperty("Id")); - postReg.ResourceTypeName.Should().Be("posts"); - postReg.Attributes.Length.Should().Be(1); - postReg.Attributes.First().Property.Should().BeSameAs(typeof(Post).GetProperty("Title")); - postReg.Relationships.Length.Should().Be(2); - postReg.Relationships[0].IsToMany.Should().BeFalse(); - postReg.Relationships[0].Property.Should().BeSameAs(typeof(Post).GetProperty("Author")); - postReg.Relationships[0].SelfLinkTemplate.Should().BeNull(); - postReg.Relationships[0].RelatedResourceLinkTemplate.Should().BeNull(); - postReg.Relationships[1].IsToMany.Should().BeTrue(); - postReg.Relationships[1].Property.Should().BeSameAs(typeof(Post).GetProperty("Comments")); - postReg.Relationships[1].SelfLinkTemplate.Should().Be("/posts/{1}/relationships/comments"); - postReg.Relationships[1].RelatedResourceLinkTemplate.Should().Be("/posts/{1}/comments"); - } - - private AttributeGrabBag InitializeGrabBag() - { - return new AttributeGrabBag() - { - Id = "2", - BooleanField = true, - NullableBooleanField = true, - SbyteField = 123, - NullableSbyteField = 123, - ByteField = 253, - NullableByteField = 253, - Int16Field = 32000, - NullableInt16Field = 32000, - Uint16Field = 64000, - NullableUint16Field = 64000, - Int32Field = 2000000000, - NullableInt32Field = 2000000000, - Uint32Field = 3000000000, - NullableUint32Field = 3000000000, - Int64Field = 9223372036854775807, - NullableInt64Field = 9223372036854775807, - Uint64Field = 9223372036854775808, - NullableUint64Field = 9223372036854775808, - DoubleField = 1056789.123, - NullableDoubleField = 1056789.123, - SingleField = 1056789.123f, - NullableSingleField = 1056789.123f, - DecimalField = 1056789.123m, - NullableDecimalField = 1056789.123m, - DateTimeField = new DateTime(1776, 07, 04), - NullableDateTimeField = new DateTime(1776, 07, 04), - DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), - NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), - GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), - NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), - StringField = "Some string 156", - EnumField = SampleEnum.Value1, - NullableEnumField = SampleEnum.Value2, - ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" - }; - } - - private void AssertAttribute(IResourceTypeRegistration reg, string attributeName, - JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func getPropertyFunc) - { - var grabBag = InitializeGrabBag(); - - var field = reg.GetFieldByName(attributeName); - var attribute = (ResourceTypeAttribute) field; - attribute.JsonKey.Should().Be(attributeName); - - attribute.SetValue(grabBag, tokenToSet); - var propertyValueAfterSet = getPropertyFunc(grabBag); - propertyValueAfterSet.Should().Be(expectedPropertyValue); - - var convertedToken = attribute.GetValue(grabBag); - if (expectedTokenAfterSet == null) - convertedToken.Should().BeNull(); - else - { - var convertedTokenValue = convertedToken.Value(); - convertedTokenValue.Should().Be(expectedTokenAfterSet); - } - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_boolean_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof (AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof (AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "boolean-field", false, false, false, g => g.BooleanField); - AssertAttribute(reg, "boolean-field", true, true, true, g => g.BooleanField); - AssertAttribute(reg, "boolean-field", null, false, false, g => g.BooleanField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_boolean_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof (AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof (AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-boolean-field", false, false, false, g => g.NullableBooleanField); - AssertAttribute(reg, "nullable-boolean-field", true, true, true, g => g.NullableBooleanField); - AssertAttribute(reg, "nullable-boolean-field", null, null, (Boolean?) null, g => g.NullableBooleanField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_SByte_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "sbyte-field", 0, 0, 0, g => g.SbyteField); - AssertAttribute(reg, "sbyte-field", 12, 12, 12, g => g.SbyteField); - AssertAttribute(reg, "sbyte-field", -12, -12, -12, g => g.SbyteField); - AssertAttribute(reg, "sbyte-field", null, 0, 0, g => g.SbyteField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_SByte_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-sbyte-field", 0, (SByte?)0, (SByte?)0, g => g.NullableSbyteField); - AssertAttribute(reg, "nullable-sbyte-field", 12, (SByte?)12, (SByte?)12, g => g.NullableSbyteField); - AssertAttribute(reg, "nullable-sbyte-field", -12, (SByte?)-12, (SByte?)-12, g => g.NullableSbyteField); - AssertAttribute(reg, "nullable-sbyte-field", null, null, (SByte?)null, g => g.NullableSbyteField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Byte_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "byte-field", 0, 0, 0, g => g.ByteField); - AssertAttribute(reg, "byte-field", 12, 12, 12, g => g.ByteField); - AssertAttribute(reg, "byte-field", null, 0, 0, g => g.ByteField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Byte_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-byte-field", 0, (Byte?)0, (Byte?)0, g => g.NullableByteField); - AssertAttribute(reg, "nullable-byte-field", 12, (Byte?)12, (Byte?)12, g => g.NullableByteField); - AssertAttribute(reg, "nullable-byte-field", null, null, (Byte?)null, g => g.NullableByteField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Int16_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "int16-field", 0, 0, 0, g => g.Int16Field); - AssertAttribute(reg, "int16-field", 4000, 4000, 4000, g => g.Int16Field); - AssertAttribute(reg, "int16-field", -4000, -4000, -4000, g => g.Int16Field); - AssertAttribute(reg, "int16-field", null, 0, 0, g => g.Int16Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int16_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-int16-field", 0, (Int16?)0, (Int16?)0, g => g.NullableInt16Field); - AssertAttribute(reg, "nullable-int16-field", 4000, (Int16?)4000, (Int16?)4000, g => g.NullableInt16Field); - AssertAttribute(reg, "nullable-int16-field", -4000, (Int16?)-4000, (Int16?)-4000, g => g.NullableInt16Field); - AssertAttribute(reg, "nullable-int16-field", null, null, (Int16?)null, g => g.NullableInt16Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_UInt16_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - // Assert - AssertAttribute(reg, "uint16-field", 0, 0, 0, g => g.Uint16Field); - AssertAttribute(reg, "uint16-field", 4000, 4000, 4000, g => g.Uint16Field); - AssertAttribute(reg, "uint16-field", null, 0, 0, g => g.Uint16Field); } [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt16_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-uint16-field", 0, (UInt16?)0, (UInt16?)0, g => g.NullableUint16Field); - AssertAttribute(reg, "nullable-uint16-field", 4000, (UInt16?)4000, (UInt16?)4000, g => g.NullableUint16Field); - AssertAttribute(reg, "nullable-uint16-field", null, null, (UInt16?)null, g => g.NullableUint16Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Int32_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "int32-field", 0, 0, 0, g => g.Int32Field); - AssertAttribute(reg, "int32-field", 2000000, 2000000, 2000000, g => g.Int32Field); - AssertAttribute(reg, "int32-field", -2000000, -2000000, -2000000, g => g.Int32Field); - AssertAttribute(reg, "int32-field", null, 0, 0, g => g.Int32Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int32_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-int32-field", 0, 0, (Int32?)0, g => g.NullableInt32Field); - AssertAttribute(reg, "nullable-int32-field", 2000000, 2000000, (Int32?)2000000, g => g.NullableInt32Field); - AssertAttribute(reg, "nullable-int32-field", -2000000, -2000000, (Int32?)-2000000, g => g.NullableInt32Field); - AssertAttribute(reg, "nullable-int32-field", null, null, (Int32?)null, g => g.NullableInt32Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_UInt32_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "uint32-field", 0, (UInt32)0, (UInt32)0, g => g.Uint32Field); - AssertAttribute(reg, "uint32-field", 2000000, (UInt32)2000000, (UInt32)2000000, g => g.Uint32Field); - AssertAttribute(reg, "uint32-field", null, (UInt32)0, (UInt32)0, g => g.Uint32Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt32_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-uint32-field", 0, (UInt32?)0, (UInt32?)0, g => g.NullableUint32Field); - AssertAttribute(reg, "nullable-uint32-field", 2000000, (UInt32?)2000000, (UInt32?)2000000, g => g.NullableUint32Field); - AssertAttribute(reg, "nullable-uint32-field", null, null, (UInt32?)null, g => g.NullableUint32Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Int64_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "int64-field", 0, 0, 0, g => g.Int64Field); - AssertAttribute(reg, "int64-field", 20000000000, 20000000000, 20000000000, g => g.Int64Field); - AssertAttribute(reg, "int64-field", -20000000000, -20000000000, -20000000000, g => g.Int64Field); - AssertAttribute(reg, "int64-field", null, 0, 0, g => g.Int64Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Int64_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-int64-field", 0, 0, (Int64?)0, g => g.NullableInt64Field); - AssertAttribute(reg, "nullable-int64-field", 20000000000, 20000000000, (Int64?)20000000000, g => g.NullableInt64Field); - AssertAttribute(reg, "nullable-int64-field", -20000000000, -20000000000, (Int64?)-20000000000, g => g.NullableInt64Field); - AssertAttribute(reg, "nullable-int64-field", null, null, (Int64?)null, g => g.NullableInt64Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_UInt64_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "uint64-field", 0, (UInt64)0, (UInt64)0, g => g.Uint64Field); - AssertAttribute(reg, "uint64-field", 20000000000, (UInt64)20000000000, (UInt64)20000000000, g => g.Uint64Field); - AssertAttribute(reg, "uint64-field", null, (UInt64)0, (UInt64)0, g => g.Uint64Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_UInt64_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-uint64-field", 0, (UInt64?)0, (UInt64?)0, g => g.NullableUint64Field); - AssertAttribute(reg, "nullable-uint64-field", 20000000000, (UInt64?)20000000000, (UInt64?)20000000000, g => g.NullableUint64Field); - AssertAttribute(reg, "nullable-uint64-field", null, null, (UInt64?)null, g => g.NullableUint64Field); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Single_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "single-field", 0f, 0f, 0f, g => g.SingleField); - AssertAttribute(reg, "single-field", 20000000000.1234f, 20000000000.1234f, 20000000000.1234f, g => g.SingleField); - AssertAttribute(reg, "single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.SingleField); - AssertAttribute(reg, "single-field", null, 0, 0, g => g.SingleField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Single_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-single-field", 0f, 0f, 0f, g => g.NullableSingleField); - AssertAttribute(reg, "nullable-single-field", 20000000000.1234f, 20000000000.1234f, (Int64?)20000000000.1234f, g => g.NullableSingleField); - AssertAttribute(reg, "nullable-single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.NullableSingleField); - AssertAttribute(reg, "nullable-single-field", null, null, (Single?)null, g => g.NullableSingleField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Double_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "double-field", 0d, 0d, 0d, g => g.DoubleField); - AssertAttribute(reg, "double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.DoubleField); - AssertAttribute(reg, "double-field", null, 0d, 0d, g => g.DoubleField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Double_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-double-field", 0d, 0d, 0d, g => g.NullableDoubleField); - AssertAttribute(reg, "nullable-double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.NullableDoubleField); - AssertAttribute(reg, "nullable-double-field", null, null, (Double?)null, g => g.NullableDoubleField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_Decimal_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "decimal-field", "0", 0m, "0", g => g.DecimalField); - AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); - AssertAttribute(reg, "decimal-field", null, 0m, "0", g => g.DecimalField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_Decimal_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-decimal-field", "0", 0m, "0", g => g.NullableDecimalField); - AssertAttribute(reg, "nullable-decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.NullableDecimalField); - AssertAttribute(reg, "nullable-decimal-field", null, null, (string)null, g => g.NullableDecimalField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_guid_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); - AssertAttribute(reg, "guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.GuidField); - AssertAttribute(reg, "guid-field", null, new Guid(), "00000000-0000-0000-0000-000000000000", g => g.GuidField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_guid_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); - AssertAttribute(reg, "nullable-guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.NullableGuidField); - AssertAttribute(reg, "nullable-guid-field", null, null, (Guid?)null, g => g.NullableGuidField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_DateTime_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); - AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); - AssertAttribute(reg, "date-time-field", null, new DateTime(), "0001-01-01T00:00:00", g => g.DateTimeField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_DateTime_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); - AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); - AssertAttribute(reg, "nullable-date-time-field", null, null, (DateTime?)null, g => g.NullableDateTimeField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_DateTimeOffset_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - var testDateTimeOffset1 = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); - var testDateTimeOffset2 = new DateTimeOffset(new DateTime(1776, 07, 04, 12, 30, 0), TimeSpan.FromHours(0)); - var testDateTimeOffset3 = new DateTimeOffset(new DateTime(2015, 03, 11, 04, 31, 0), TimeSpan.FromHours(0)); - AssertAttribute(reg, "date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset1, "1776-07-04T00:00:00.0000000-05:00", g => g.DateTimeOffsetField); - AssertAttribute(reg, "date-time-offset-field", "1776-07-04T12:30:00+00:00", testDateTimeOffset2, "1776-07-04T12:30:00.0000000+00:00", g => g.DateTimeOffsetField); - AssertAttribute(reg, "date-time-offset-field", "2015-03-11T04:31:00.0000000+00:00", testDateTimeOffset3, "2015-03-11T04:31:00.0000000+00:00", g => g.DateTimeOffsetField); - AssertAttribute(reg, "date-time-offset-field", null, new DateTimeOffset(), "0001-01-01T00:00:00.0000000+00:00", g => g.DateTimeOffsetField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_DateTimeOffset_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - var testDateTimeOffset = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); - AssertAttribute(reg, "nullable-date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset, "1776-07-04T00:00:00.0000000-05:00", - g => g.NullableDateTimeOffsetField); - AssertAttribute(reg, "nullable-date-time-offset-field", null, null, (DateTimeOffset?)null, - g => g.NullableDateTimeOffsetField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_string_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "string-field", "asdf", "asdf", "asdf", g => g.StringField); - AssertAttribute(reg, "string-field", null, null, (string)null, g => g.StringField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_enum_field() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); - - // Assert - AssertAttribute(reg, "enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.EnumField); - AssertAttribute(reg, "enum-field", null, (SampleEnum)0, 0, g => g.EnumField); - } - - [TestMethod] - public void RegisterResourceType_sets_up_correct_attribute_for_nullable_enum_field() + public void GetRegistrationForType_returns_correct_value_for_registered_types() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - registry.RegisterResourceType(typeof(AttributeGrabBag)); - var reg = registry.GetRegistrationForType(typeof(AttributeGrabBag)); + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); - // Assert - AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField); - AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField); - } + var mockAuthorRegistration = new Mock(MockBehavior.Strict); + mockAuthorRegistration.Setup(m => m.Type).Returns(typeof(Author)); + mockAuthorRegistration.Setup(m => m.ResourceTypeName).Returns("authors"); - [TestMethod] - public void GetRegistrationForType_returns_correct_value_for_registered_types() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - registry.RegisterResourceType(typeof(Post)); - registry.RegisterResourceType(typeof(Author)); - registry.RegisterResourceType(typeof(Comment)); - registry.RegisterResourceType(typeof(UserGroup)); + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + registry.AddRegistration(mockAuthorRegistration.Object); // Act - var postReg = registry.GetRegistrationForType(typeof(Post)); var authorReg = registry.GetRegistrationForType(typeof(Author)); - var commentReg = registry.GetRegistrationForType(typeof(Comment)); - var userGroupReg = registry.GetRegistrationForType(typeof(UserGroup)); + var postReg = registry.GetRegistrationForType(typeof(Post)); // Assert - postReg.ResourceTypeName.Should().Be("posts"); - authorReg.ResourceTypeName.Should().Be("authors"); - commentReg.ResourceTypeName.Should().Be("comments"); - userGroupReg.ResourceTypeName.Should().Be("user-groups"); + postReg.Should().BeSameAs(mockPostRegistration.Object); + authorReg.Should().BeSameAs(mockAuthorRegistration.Object); } [TestMethod] public void GetRegistrationForType_gets_registration_for_closest_registered_base_type_for_unregistered_type() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - registry.RegisterResourceType(typeof(Post)); + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); // Act var registration = registry.GetRegistrationForType(typeof(DerivedPost)); @@ -794,7 +62,7 @@ public void GetRegistrationForType_gets_registration_for_closest_registered_base public void GetRegistrationForType_fails_when_getting_unregistered_type() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + var registry = new ResourceTypeRegistry(); // Act Action action = () => @@ -803,14 +71,14 @@ public void GetRegistrationForType_fails_when_getting_unregistered_type() }; // Assert - action.ShouldThrow().WithMessage("No model registration was found for the type \"Post\"."); + action.ShouldThrow().WithMessage("No type registration was found for the type \"Post\"."); } [TestMethod] public void GetRegistrationForResourceTypeName_fails_when_getting_unregistered_type_name() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + var registry = new ResourceTypeRegistry(); // Act Action action = () => @@ -819,41 +87,47 @@ public void GetRegistrationForResourceTypeName_fails_when_getting_unregistered_t }; // Assert - action.ShouldThrow().WithMessage("No model registration was found for the type name \"posts\"."); + action.ShouldThrow().WithMessage("No type registration was found for the type name \"posts\"."); } [TestMethod] - public void GetModelRegistrationForResourceTypeName_returns_correct_value_for_registered_names() + public void GetRegistrationForResourceTypeName_returns_correct_value_for_registered_names() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - registry.RegisterResourceType(typeof(Post)); - registry.RegisterResourceType(typeof(Author)); - registry.RegisterResourceType(typeof(Comment)); - registry.RegisterResourceType(typeof(UserGroup)); + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var mockAuthorRegistration = new Mock(MockBehavior.Strict); + mockAuthorRegistration.Setup(m => m.Type).Returns(typeof(Author)); + mockAuthorRegistration.Setup(m => m.ResourceTypeName).Returns("authors"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + registry.AddRegistration(mockAuthorRegistration.Object); // Act var postReg = registry.GetRegistrationForResourceTypeName("posts"); var authorReg = registry.GetRegistrationForResourceTypeName("authors"); - var commentReg = registry.GetRegistrationForResourceTypeName("comments"); - var userGroupReg = registry.GetRegistrationForResourceTypeName("user-groups"); // Assert - postReg.Type.Should().Be(typeof (Post)); - authorReg.Type.Should().Be(typeof (Author)); - commentReg.Type.Should().Be(typeof (Comment)); - userGroupReg.Type.Should().Be(typeof (UserGroup)); + postReg.Should().BeSameAs(mockPostRegistration.Object); + authorReg.Should().BeSameAs(mockAuthorRegistration.Object); } [TestMethod] public void TypeIsRegistered_returns_true_if_type_is_registered() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - registry.RegisterResourceType(typeof (Post)); + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); // Act - var isRegistered = registry.TypeIsRegistered(typeof (Post)); + var isRegistered = registry.TypeIsRegistered(typeof(Post)); // Assert isRegistered.Should().BeTrue(); @@ -863,8 +137,12 @@ public void TypeIsRegistered_returns_true_if_type_is_registered() public void TypeIsRegistered_returns_true_if_parent_type_is_registered() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - registry.RegisterResourceType(typeof(Post)); + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); // Act var isRegistered = registry.TypeIsRegistered(typeof(DerivedPost)); @@ -877,7 +155,7 @@ public void TypeIsRegistered_returns_true_if_parent_type_is_registered() public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered() { // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); + var registry = new ResourceTypeRegistry(); // Act var isRegistered = registry.TypeIsRegistered(typeof(Comment)); @@ -885,18 +163,5 @@ public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered // Assert isRegistered.Should().BeFalse(); } - - [TestMethod] - public void TypeIsRegistered_returns_false_for_collection_of_unregistered_types() - { - // Arrange - var registry = new ResourceTypeRegistry(new DefaultNamingConventions(new PluralizationService())); - - // Act - var isRegistered = registry.TypeIsRegistered(typeof(ICollection)); - - // Assert - isRegistered.Should().BeFalse(); - } } } diff --git a/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs b/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs deleted file mode 100644 index 6fa032b5..00000000 --- a/JSONAPI.Tests/Http/PascalizedControllerSelectorTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Dispatcher; -using System.Web.Http.Routing; -using FluentAssertions; -using JSONAPI.Http; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; - -namespace JSONAPI.Tests.Http -{ - [TestClass] - public class PascalizedControllerSelectorTests - { - private class FooBarBazQuxController - { - - } - - private class TestHttpControllerTypeResolver : IHttpControllerTypeResolver - { - public ICollection GetControllerTypes(IAssembliesResolver assembliesResolver) - { - return new List - { - typeof (FooBarBazQuxController) - }; - } - } - - [TestMethod] - public void Selects_controller_for_all_lower_case_name() - { - TestForDefaultString("foobarbazqux"); - } - - [TestMethod] - public void Selects_controller_for_name_with_underscores() - { - TestForDefaultString("foo_bar_baz_qux"); - } - - [TestMethod] - public void Selects_controller_for_name_with_dots() - { - TestForDefaultString("foo.bar.baz.qux"); - } - - [TestMethod] - public void Selects_controller_for_name_with_dashes() - { - TestForDefaultString("foo-bar-baz-qux"); - } - - [TestMethod] - public void Selects_controller_for_name_with_all_three() - { - TestForDefaultString("foo.bar-baz_qux"); - } - - [TestMethod] - public void Selects_controller_for_pascalized_name() - { - TestForDefaultString("FooBarBazQux"); - } - - [TestMethod] - public void Selects_controller_for_all_caps_name() - { - TestForDefaultString("FOOBARBAZQUX"); - } - - private void TestForDefaultString(string defaultString) - { - // Arrange - var routeDataDict = new Dictionary - { - {"controller", defaultString} - }; - - var mockRouteData = new Mock(MockBehavior.Strict); - mockRouteData.Setup(m => m.Route).Returns((IHttpRoute)null); - mockRouteData.Setup(m => m.Values).Returns(routeDataDict); - - var httpConfig = new HttpConfiguration(); - httpConfig.Services.Replace(typeof(IHttpControllerTypeResolver), new TestHttpControllerTypeResolver()); - - var mockRequestContext = new Mock(MockBehavior.Strict); - mockRequestContext.Setup(m => m.Configuration).Returns(httpConfig); - mockRequestContext.Setup(m => m.RouteData).Returns(mockRouteData.Object); - - var request = new HttpRequestMessage(); - request.SetRequestContext(mockRequestContext.Object); - - var selector = new PascalizedControllerSelector(httpConfig); - - // Act - var actual = selector.SelectController(request); - - // Assert - actual.ControllerType.Should().Be(typeof (FooBarBazQuxController)); - } - } -} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 33863ee7..20e8ae3a 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -85,9 +85,9 @@ + - @@ -116,6 +116,10 @@ + + {0fe799ec-b6c5-499b-b56c-b97613342f6c} + JSONAPI.Tests.SingleControllerWebApp + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} JSONAPI diff --git a/JSONAPI.Tests/app.config b/JSONAPI.Tests/app.config index b7fb516c..cb613ec1 100644 --- a/JSONAPI.Tests/app.config +++ b/JSONAPI.Tests/app.config @@ -26,6 +26,14 @@ + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.TodoMVC.API/Controllers/MainController.cs b/JSONAPI.TodoMVC.API/Controllers/MainController.cs new file mode 100644 index 00000000..f1bc55de --- /dev/null +++ b/JSONAPI.TodoMVC.API/Controllers/MainController.cs @@ -0,0 +1,12 @@ +using JSONAPI.Http; + +namespace JSONAPI.TodoMVC.API.Controllers +{ + public class MainController : JsonApiController + { + public MainController(IDocumentMaterializerLocator documentMaterializerLocator) + : base(documentMaterializerLocator) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI.TodoMVC.API/Controllers/TodosController.cs b/JSONAPI.TodoMVC.API/Controllers/TodosController.cs deleted file mode 100644 index b347d51c..00000000 --- a/JSONAPI.TodoMVC.API/Controllers/TodosController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using JSONAPI.Http; -using JSONAPI.TodoMVC.API.Models; - -namespace JSONAPI.TodoMVC.API.Controllers -{ - public class TodosController : JsonApiController - { - public TodosController(IDocumentMaterializer documentMaterializer) : base(documentMaterializer) - { - } - } -} \ No newline at end of file diff --git a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj index 323ffe8d..13ac0840 100644 --- a/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj +++ b/JSONAPI.TodoMVC.API/JSONAPI.TodoMVC.API.csproj @@ -103,7 +103,7 @@ - + 201502061744049_Initial.cs diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index 91774036..82429343 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -5,8 +5,8 @@ using Autofac.Integration.WebApi; using JSONAPI.Autofac; using JSONAPI.Autofac.EntityFramework; +using JSONAPI.Configuration; using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; using JSONAPI.TodoMVC.API.Models; using Owin; @@ -24,28 +24,14 @@ private static HttpConfiguration GetWebApiConfiguration() { var httpConfig = new HttpConfiguration(); - var pluralizationService = new PluralizationService(); - pluralizationService.AddMapping("todo", "todos"); - var namingConventions = new DefaultNamingConventions(pluralizationService); - - var configuration = new JsonApiAutofacConfiguration(namingConventions); - configuration.RegisterResourceType(typeof(Todo)); - var module = configuration.GetAutofacModule(); - var efModule = configuration.GetEntityFrameworkAutofacModule(); - var containerBuilder = new ContainerBuilder(); - containerBuilder.RegisterModule(module); - containerBuilder.RegisterModule(efModule); - containerBuilder.RegisterGeneric(typeof(EntityFrameworkDocumentMaterializer<>)) - .AsImplementedInterfaces(); containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); - containerBuilder.RegisterType().As(); - + containerBuilder.RegisterType().As().InstancePerRequest(); var container = containerBuilder.Build(); - httpConfig.UseJsonApiWithAutofac(container); - // Web API routes - httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); + var configuration = new JsonApiConfiguration(); + configuration.RegisterEntityFrameworkResourceType(c => c.OverrideDefaultResourceTypeName("todos")); + configuration.SetupHttpConfigurationUsingAutofac(httpConfig, container); return httpConfig; } diff --git a/JSONAPI.sln b/JSONAPI.sln index 68419028..07c3e3b5 100644 --- a/JSONAPI.sln +++ b/JSONAPI.sln @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac.EntityFrame EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests", "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests\JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj", "{58AEF8B8-8D51-4175-AC96-BC622703E8BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSONAPI.Autofac.Tests", "JSONAPI.Autofac.Tests\JSONAPI.Autofac.Tests.csproj", "{AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +77,10 @@ Global {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {58AEF8B8-8D51-4175-AC96-BC622703E8BB}.Release|Any CPU.Build.0 = Release|Any CPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/JSONAPI/Configuration/IResourceTypeConfiguration.cs b/JSONAPI/Configuration/IResourceTypeConfiguration.cs new file mode 100644 index 00000000..46d1f72f --- /dev/null +++ b/JSONAPI/Configuration/IResourceTypeConfiguration.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using JSONAPI.Core; + +namespace JSONAPI.Configuration +{ + /// + /// Configuration mechanism for resource types + /// + public interface IResourceTypeConfiguration + { + /// + /// The JSON name for this resource type + /// + string ResourceTypeName { get; } + + /// + /// The CLR type corresponding to this resource type + /// + Type ClrType { get; } + + /// + /// The type of document materializer to use for resources of this type + /// + Type DocumentMaterializerType { get; } + + /// + /// Configurations for this type's resources + /// + IDictionary RelationshipConfigurations { get; } + + /// + /// A factory to use to build expressions to filter a collection of resources of this type by ID. + /// + Func FilterByIdExpressionFactory { get; } + + /// + /// A factory to use to build expressions to sort a collection of resources of this type by ID. + /// + Func SortByIdExpressionFactory { get; } + + /// + /// Builds a resource type registration corresponding to this type + /// + /// + IResourceTypeRegistration BuildResourceTypeRegistration(); + } +} \ No newline at end of file diff --git a/JSONAPI/Configuration/JsonApiConfiguration.cs b/JSONAPI/Configuration/JsonApiConfiguration.cs new file mode 100644 index 00000000..9a48b361 --- /dev/null +++ b/JSONAPI/Configuration/JsonApiConfiguration.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Base class for JSON API configuration services + /// + public class JsonApiConfiguration : IJsonApiConfiguration + { + private readonly IResourceTypeRegistrar _resourceTypeRegistrar; + public ILinkConventions LinkConventions { get; private set; } + public IEnumerable ResourceTypeConfigurations { get { return _resourceTypeConfigurations; } } + + private readonly IList _resourceTypeConfigurations; + + /// + /// Creates a new JsonApiConfiguration + /// + public JsonApiConfiguration() + : this(new PluralizationService()) + { + } + + /// + /// Creates a new JsonApiConfiguration + /// + public JsonApiConfiguration(IPluralizationService pluralizationService) + : this(new DefaultNamingConventions(pluralizationService)) + { + } + + /// + /// Creates a new JsonApiConfiguration + /// + public JsonApiConfiguration(INamingConventions namingConventions) + : this(new ResourceTypeRegistrar(namingConventions)) + { + } + + /// + /// Creates a new JsonApiConfiguration + /// + public JsonApiConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) + { + _resourceTypeRegistrar = resourceTypeRegistrar; + if (resourceTypeRegistrar == null) throw new ArgumentNullException("resourceTypeRegistrar"); + + _resourceTypeConfigurations = new List(); + LinkConventions = new DefaultLinkConventions(); + } + + /// + /// Registers a resource type with the configuration + /// + public void RegisterResourceType(Action> configurationAction = null) + { + var configuration = new ResourceTypeConfiguration(_resourceTypeRegistrar); + if (configurationAction != null) + configurationAction(configuration); + _resourceTypeConfigurations.Add(configuration); + } + + /// + /// Registers an entity type/resource type pair for use with MappedDocumentMaterializer /> + /// + public void RegisterMappedType(Action> configurationAction = null) + where TMaterializer : MappedDocumentMaterializer + where TResourceType : class + { + RegisterResourceType(c => + { + c.UseDocumentMaterializer(); + if (configurationAction != null) + configurationAction(c); + }); + } + + /// + /// Allows overriding how links will be formatted. + /// + /// + public void OverrideLinkConventions(ILinkConventions linkConventions) + { + LinkConventions = linkConventions; + } + } + + /// + /// Configuration interface for JSON API + /// + public interface IJsonApiConfiguration + { + /// + /// Conventions for serializing links with resource objects + /// + ILinkConventions LinkConventions { get; } + + /// + /// A set of resource type configurations. These configurations will be converted into IResourceTypeRegistrations + /// by the ResourceTypeRegistrar + /// + IEnumerable ResourceTypeConfigurations { get; } + } +} diff --git a/JSONAPI/Core/JsonApiHttpConfiguration.cs b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs similarity index 82% rename from JSONAPI/Core/JsonApiHttpConfiguration.cs rename to JSONAPI/Configuration/JsonApiHttpConfiguration.cs index 12e2344b..32ce73b1 100644 --- a/JSONAPI/Core/JsonApiHttpConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs @@ -1,11 +1,9 @@ using System; using System.Web.Http; -using System.Web.Http.Dispatcher; using JSONAPI.ActionFilters; -using JSONAPI.Http; using JSONAPI.Json; -namespace JSONAPI.Core +namespace JSONAPI.Configuration { /// /// Configures an HttpConfiguration object for use with JSONAPI.NET @@ -44,8 +42,10 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Filters.Add(_fallbackDocumentBuilderAttribute); httpConfig.Filters.Add(_jsonApiExceptionFilterAttribute); - httpConfig.Services.Replace(typeof(IHttpControllerSelector), - new PascalizedControllerSelector(httpConfig)); + // Web API routes + httpConfig.Routes.MapHttpRoute("ResourceCollection", "{resourceType}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("Resource", "{resourceType}/{id}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("RelatedResource", "{resourceType}/{id}/{relationshipName}", new { controller = "Main" }); } } } diff --git a/JSONAPI/Configuration/ResourceTypeConfiguration.cs b/JSONAPI/Configuration/ResourceTypeConfiguration.cs new file mode 100644 index 00000000..3c632680 --- /dev/null +++ b/JSONAPI/Configuration/ResourceTypeConfiguration.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using JSONAPI.Core; +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Configuration mechanism for resource types. + /// + /// + public sealed class ResourceTypeConfiguration : IResourceTypeConfiguration + { + private readonly IResourceTypeRegistrar _resourceTypeRegistrar; + + internal ResourceTypeConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) + { + _resourceTypeRegistrar = resourceTypeRegistrar; + RelationshipConfigurations = new ConcurrentDictionary(); + ClrType = typeof (TResourceType); + } + + public string ResourceTypeName { get; private set; } + public Type ClrType { get; private set; } + public Type DocumentMaterializerType { get; private set; } + public IDictionary RelationshipConfigurations { get; private set; } + public Func FilterByIdExpressionFactory { get; private set; } + public Func SortByIdExpressionFactory { get; private set; } + + /// + /// Configures the relationship corresponding to the specified property + /// + public void ConfigureRelationship(Expression> property, + Action relationshipConfiguration) + { + if (property == null) throw new ArgumentNullException("property"); + if (relationshipConfiguration == null) throw new ArgumentNullException("relationshipConfiguration"); + + var member = (MemberExpression) property.Body; + var propertyInfo = (PropertyInfo) member.Member; + + var config = new ResourceTypeRelationshipConfiguration(); + relationshipConfiguration(config); + + RelationshipConfigurations[propertyInfo] = config; + } + + /// + /// Specifies the materializer to use for this resource type + /// + /// + public void UseDocumentMaterializer() + where TMaterializer : IDocumentMaterializer + { + DocumentMaterializerType = typeof (TMaterializer); + } + + /// + /// Overrides the resource type name from naming conventions + /// + /// + public void OverrideDefaultResourceTypeName(string resourceTypeName) + { + ResourceTypeName = resourceTypeName; + } + + /// + /// Specifies a function to use build expressions that allow filtering resources of this type by ID + /// + public void OverrideDefaultFilterById(Func filterByIdExpressionFactory) + { + FilterByIdExpressionFactory = filterByIdExpressionFactory; + } + + /// + /// Specifies a function to use build expressions that allow sorting resources of this type by ID + /// + public void OverrideDefaultSortById(Func sortByIdExpressionFactory) + { + SortByIdExpressionFactory = sortByIdExpressionFactory; + } + + public IResourceTypeRegistration BuildResourceTypeRegistration() + { + return _resourceTypeRegistrar.BuildRegistration(ClrType, ResourceTypeName, FilterByIdExpressionFactory, + SortByIdExpressionFactory); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs b/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs new file mode 100644 index 00000000..e63be10f --- /dev/null +++ b/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs @@ -0,0 +1,37 @@ +using System; +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Default implementation of + /// + public sealed class ResourceTypeRelationshipConfiguration : IResourceTypeRelationshipConfiguration + { + internal ResourceTypeRelationshipConfiguration() + { + } + + public Type MaterializerType { get; private set; } + + /// + /// Specify the materializer type to use for this particular relationship + /// + public void UseMaterializer() + where TMaterializerType : IRelatedResourceDocumentMaterializer + { + MaterializerType = typeof (TMaterializerType); + } + } + + /// + /// Configuration mechanism for relationships + /// + public interface IResourceTypeRelationshipConfiguration + { + /// + /// The type to use for materializing this relationship + /// + Type MaterializerType { get; } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/DefaultNamingConventions.cs b/JSONAPI/Core/DefaultNamingConventions.cs new file mode 100644 index 00000000..a0c0dc2f --- /dev/null +++ b/JSONAPI/Core/DefaultNamingConventions.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Reflection; +using JSONAPI.Extensions; +using Newtonsoft.Json; + +namespace JSONAPI.Core +{ + /// + /// Default implementation of INamingConventions + /// + public class DefaultNamingConventions : INamingConventions + { + private readonly IPluralizationService _pluralizationService; + + /// + /// Creates a new DefaultNamingConventions + /// + /// + public DefaultNamingConventions(IPluralizationService pluralizationService) + { + _pluralizationService = pluralizationService; + } + + /// + /// This method first checks if the property has a [JsonProperty] attribute. If so, + /// it uses the attribute's PropertyName. Otherwise, it falls back to taking the + /// property's name, and dasherizing it. + /// + /// + /// + public string GetFieldNameForProperty(PropertyInfo property) + { + var jsonPropertyAttribute = (JsonPropertyAttribute)property.GetCustomAttributes(typeof(JsonPropertyAttribute)).FirstOrDefault(); + return jsonPropertyAttribute != null ? jsonPropertyAttribute.PropertyName : property.Name.Dasherize(); + } + + /// + /// This method first checks if the type has a [JsonObject] attribute. If so, + /// it uses the attribute's Title. Otherwise it falls back to pluralizing the + /// type's name using the given and then + /// dasherizing that value. + /// + /// + /// + public string GetResourceTypeNameForType(Type type) + { + var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(JsonObjectAttribute)).ToList(); + + string title = type.Name; + if (attrs.Any()) + { + var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") + .Select(arg => arg.TypedValue.Value.ToString()).ToList(); + if (titles.Any()) title = titles.First(); + } + + return _pluralizationService.Pluralize(title).Dasherize(); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/INamingConventions.cs b/JSONAPI/Core/INamingConventions.cs new file mode 100644 index 00000000..3866d9d3 --- /dev/null +++ b/JSONAPI/Core/INamingConventions.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// Allows configuring how to calculate JSON API keys based on CLR types and properties + /// + public interface INamingConventions + { + /// + /// Calculates the field name for a given property + /// + /// + /// + string GetFieldNameForProperty(PropertyInfo property); + + /// + /// Calculates the resource type name for a CLR type + /// + /// + /// + string GetResourceTypeNameForType(Type type); + } +} \ No newline at end of file diff --git a/JSONAPI/Core/IResourceTypeRegistrar.cs b/JSONAPI/Core/IResourceTypeRegistrar.cs new file mode 100644 index 00000000..c92d3d7c --- /dev/null +++ b/JSONAPI/Core/IResourceTypeRegistrar.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq.Expressions; + +namespace JSONAPI.Core +{ + /// + /// Creates resource type registrations based on CLR types + /// + public interface IResourceTypeRegistrar + { + /// + /// Creates a registration for the given CLR type + /// + IResourceTypeRegistration BuildRegistration(Type type, + string resourceTypeName = null, + Func filterByIdFactory = null, + Func sortByIdFactory = null); + } +} \ No newline at end of file diff --git a/JSONAPI/Core/IResourceTypeRegistration.cs b/JSONAPI/Core/IResourceTypeRegistration.cs index 06249d4b..1c59cbb5 100644 --- a/JSONAPI/Core/IResourceTypeRegistration.cs +++ b/JSONAPI/Core/IResourceTypeRegistration.cs @@ -1,6 +1,11 @@ using System; using System.Linq.Expressions; +using System.Net.Http; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Http; namespace JSONAPI.Core { diff --git a/JSONAPI/Core/IResourceTypeRegistry.cs b/JSONAPI/Core/IResourceTypeRegistry.cs index 51fa9b11..c55177cb 100644 --- a/JSONAPI/Core/IResourceTypeRegistry.cs +++ b/JSONAPI/Core/IResourceTypeRegistry.cs @@ -29,5 +29,11 @@ public interface IResourceTypeRegistry /// The registration for the given type name. /// Thrown when the type name was not registered IResourceTypeRegistration GetRegistrationForResourceTypeName(string resourceTypeName); + + /// + /// Adds a registration to the registry. + /// + /// The registration to add + void AddRegistration(IResourceTypeRegistration registration); } } diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs deleted file mode 100644 index 71c63604..00000000 --- a/JSONAPI/Core/JsonApiConfiguration.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Web.Http; -using JSONAPI.ActionFilters; -using JSONAPI.Documents; -using JSONAPI.Documents.Builders; -using JSONAPI.Http; -using JSONAPI.Json; -using JSONAPI.QueryableTransformers; - -namespace JSONAPI.Core -{ - /// - /// This is a convenience class for configuring JSONAPI.NET in the simplest way possible. - /// - public class JsonApiConfiguration - { - private readonly IResourceTypeRegistry _resourceTypeRegistry; - private readonly ILinkConventions _linkConventions; - private IQueryableEnumerationTransformer _queryableEnumerationTransformer; - - /// - /// Creates a new configuration - /// - public JsonApiConfiguration(IResourceTypeRegistry resourceTypeRegistry) - { - _resourceTypeRegistry = resourceTypeRegistry; - } - - /// - /// Creates a new configuration - /// - public JsonApiConfiguration(IResourceTypeRegistry resourceTypeRegistry, ILinkConventions linkConventions) - { - _resourceTypeRegistry = resourceTypeRegistry; - _linkConventions = linkConventions; - } - - /// - /// Allows overriding the queryable document builder to use. This is useful for - /// - /// - public void UseQueryableEnumeration(IQueryableEnumerationTransformer queryableEnumerationTransformer) - { - _queryableEnumerationTransformer = queryableEnumerationTransformer; - } - - /// - /// Applies the running configuration to an HttpConfiguration instance - /// - /// The HttpConfiguration to apply this JsonApiConfiguration to - public void Apply(HttpConfiguration httpConfig) - { - var linkConventions = _linkConventions ?? new DefaultLinkConventions(); - - // Serialization - var metadataFormatter = new MetadataFormatter(); - var linkFormatter = new LinkFormatter(metadataFormatter); - var resourceLinkageFormatter = new ResourceLinkageFormatter(); - var relationshipObjectFormatter = new RelationshipObjectFormatter(linkFormatter, resourceLinkageFormatter, metadataFormatter); - var resourceObjectFormatter = new ResourceObjectFormatter(relationshipObjectFormatter, linkFormatter, metadataFormatter); - var errorFormatter = new ErrorFormatter(linkFormatter, metadataFormatter); - var singleResourceDocumentFormatter = new SingleResourceDocumentFormatter(resourceObjectFormatter, metadataFormatter); - var resourceCollectionDocumentFormatter = new ResourceCollectionDocumentFormatter(resourceObjectFormatter, metadataFormatter); - var errorDocumentFormatter = new ErrorDocumentFormatter(errorFormatter, metadataFormatter); - - // Queryable transforms - var queryableEnumerationTransformer = _queryableEnumerationTransformer ?? new SynchronousEnumerationTransformer(); - var filteringTransformer = new DefaultFilteringTransformer(_resourceTypeRegistry); - var sortingTransformer = new DefaultSortingTransformer(_resourceTypeRegistry); - var paginationTransformer = new DefaultPaginationTransformer(); - - // Builders - var baseUrlService = new BaseUrlService(); - var singleResourceDocumentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(_resourceTypeRegistry, linkConventions); - var resourceCollectionDocumentBuilder = new RegistryDrivenResourceCollectionDocumentBuilder(_resourceTypeRegistry, linkConventions); - var queryableResourceCollectionDocumentBuilder = new DefaultQueryableResourceCollectionDocumentBuilder(resourceCollectionDocumentBuilder, - queryableEnumerationTransformer, filteringTransformer, sortingTransformer, paginationTransformer, baseUrlService); - var errorDocumentBuilder = new ErrorDocumentBuilder(); - var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder, - queryableResourceCollectionDocumentBuilder, resourceCollectionDocumentBuilder, baseUrlService); - - // Dependencies for JsonApiHttpConfiguration - var formatter = new JsonApiFormatter(singleResourceDocumentFormatter, resourceCollectionDocumentFormatter, errorDocumentFormatter, errorDocumentBuilder); - var fallbackDocumentBuilderAttribute = new FallbackDocumentBuilderAttribute(fallbackDocumentBuilder, errorDocumentBuilder); - var exceptionFilterAttribute = new JsonApiExceptionFilterAttribute(errorDocumentBuilder, formatter); - - var jsonApiHttpConfiguration = new JsonApiHttpConfiguration(formatter, fallbackDocumentBuilderAttribute, exceptionFilterAttribute); - jsonApiHttpConfiguration.Apply(httpConfig); - } - } -} diff --git a/JSONAPI/Core/ResourceTypeAttribute.cs b/JSONAPI/Core/ResourceTypeAttribute.cs new file mode 100644 index 00000000..01865b35 --- /dev/null +++ b/JSONAPI/Core/ResourceTypeAttribute.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// A ResourceTypeField representing an attribute on a resource object + /// + public class ResourceTypeAttribute : ResourceTypeField + { + private readonly IAttributeValueConverter _attributeValueConverter; + + internal ResourceTypeAttribute(IAttributeValueConverter attributeValueConverter, PropertyInfo property, string jsonKey) + : base(property, jsonKey) + { + _attributeValueConverter = attributeValueConverter; + } + + /// + /// Gets the json-formatted value of this attribute for the given resource + /// + /// + /// + public JToken GetValue(object resource) + { + return _attributeValueConverter.GetValue(resource); + } + + /// + /// Sets the value of this attribute for the resource + /// + /// + /// + public void SetValue(object resource, JToken value) + { + _attributeValueConverter.SetValue(resource, value); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ResourceTypeField.cs b/JSONAPI/Core/ResourceTypeField.cs index e455bc69..b54797ec 100644 --- a/JSONAPI/Core/ResourceTypeField.cs +++ b/JSONAPI/Core/ResourceTypeField.cs @@ -1,6 +1,4 @@ -using System; -using System.Reflection; -using Newtonsoft.Json.Linq; +using System.Reflection; namespace JSONAPI.Core { @@ -25,102 +23,4 @@ internal ResourceTypeField(PropertyInfo property, string jsonKey) /// public string JsonKey { get; private set; } } - - /// - /// A ResourceTypeField representing an attribute on a resource object - /// - public class ResourceTypeAttribute : ResourceTypeField - { - private readonly IAttributeValueConverter _attributeValueConverter; - - internal ResourceTypeAttribute(IAttributeValueConverter attributeValueConverter, PropertyInfo property, string jsonKey) - : base(property, jsonKey) - { - _attributeValueConverter = attributeValueConverter; - } - - /// - /// Gets the json-formatted value of this attribute for the given resource - /// - /// - /// - public JToken GetValue(object resource) - { - return _attributeValueConverter.GetValue(resource); - } - - /// - /// Sets the value of this attribute for the resource - /// - /// - /// - public void SetValue(object resource, JToken value) - { - _attributeValueConverter.SetValue(resource, value); - } - } - - /// - /// A ResourceTypeField representing a relationship to another resource type - /// - public abstract class ResourceTypeRelationship : ResourceTypeField - { - internal ResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate, bool isToMany) - : base(property, jsonKey) - { - RelatedType = relatedType; - SelfLinkTemplate = selfLinkTemplate; - RelatedResourceLinkTemplate = relatedResourceLinkTemplate; - IsToMany = isToMany; - } - - /// - /// Whether this relationship represents a link to a collection of resources or a single one. - /// - public bool IsToMany { get; private set; } - - /// - /// The type of resource found on the other side of this relationship - /// - public Type RelatedType { get; private set; } - - /// - /// The template for building URLs to access the relationship itself. - /// If the string {1} appears in the template, it will be replaced by the ID of resource this - /// relationship belongs to. - /// - public string SelfLinkTemplate { get; private set; } - - /// - /// The template for building URLs to access the data making up the other side of this relationship. - /// If the string {1} appears in the template, it will be replaced by the ID of resource this - /// relationship belongs to. - /// - public string RelatedResourceLinkTemplate { get; private set; } - } - - /// - /// A ModelProperty representing a relationship to a collection of resources - /// - public sealed class ToManyResourceTypeRelationship : ResourceTypeRelationship - { - internal ToManyResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate) - : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, true) - { - } - } - - /// - /// A ModelProperty representing a relationship to a single resource - /// - public sealed class ToOneResourceTypeRelationship : ResourceTypeRelationship - { - internal ToOneResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate) - : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, false) - { - } - } } diff --git a/JSONAPI/Core/ResourceTypeRegistrar.cs b/JSONAPI/Core/ResourceTypeRegistrar.cs new file mode 100644 index 00000000..6ae7b057 --- /dev/null +++ b/JSONAPI/Core/ResourceTypeRegistrar.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JSONAPI.Attributes; +using JSONAPI.Configuration; +using JSONAPI.Extensions; +using Newtonsoft.Json; + +namespace JSONAPI.Core +{ + /// + /// Default implementation of + /// + public class ResourceTypeRegistrar : IResourceTypeRegistrar + { + private readonly INamingConventions _namingConventions; + + /// + /// Creates a new + /// + /// Conventions for naming types and fields + public ResourceTypeRegistrar(INamingConventions namingConventions) + { + if (namingConventions == null) throw new ArgumentNullException("namingConventions"); + _namingConventions = namingConventions; + } + + public IResourceTypeRegistration BuildRegistration(Type type, string resourceTypeName = null, + Func filterByIdFactory = null, + Func sortByIdFactory = null) + { + if (resourceTypeName == null) + resourceTypeName = _namingConventions.GetResourceTypeNameForType(type); + + var fieldMap = new Dictionary(); + + var idProperty = CalculateIdProperty(type); + if (idProperty == null) + throw new InvalidOperationException(String.Format( + "Unable to determine Id property for type `{0}`.", type.Name)); + + var props = type.GetProperties().OrderBy(p => p.Name); + foreach (var prop in props) + { + if (prop == idProperty) continue; + + var ignore = prop.CustomAttributes.Any(c => c.AttributeType == typeof (JsonIgnoreAttribute)); + if (ignore) continue; + + var property = CreateResourceTypeField(prop); + var jsonKey = property.JsonKey; + + if (jsonKey == "id") + throw new InvalidOperationException( + String.Format( + "Failed to register type `{0}` because it contains a non-id property that would serialize as \"id\".", + type.Name)); + + if (jsonKey == "type") + throw new InvalidOperationException( + String.Format( + "Failed to register type `{0}` because it contains a property that would serialize as \"type\".", + type.Name)); + + if (fieldMap.ContainsKey(jsonKey)) + throw new InvalidOperationException( + String.Format( + "Failed to register type `{0}` because contains multiple properties that would serialize as `{1}`.", + type.Name, jsonKey)); + + fieldMap[jsonKey] = property; + } + + if (filterByIdFactory == null) + { + filterByIdFactory = (param, id) => + { + var propertyExpr = Expression.Property(param, idProperty); + var idExpr = Expression.Constant(id); + return Expression.Equal(propertyExpr, idExpr); + }; + } + + if (sortByIdFactory == null) + { + sortByIdFactory = param => Expression.Property(param, idProperty); + } + + return new ResourceTypeRegistration(type, idProperty, resourceTypeName, fieldMap, filterByIdFactory, + sortByIdFactory); + } + + /// + /// Gets a value converter for the given property + /// + /// + /// + protected virtual IAttributeValueConverter GetValueConverterForProperty(PropertyInfo prop) + { + var serializeAsComplexAttribute = prop.GetCustomAttribute(); + if (serializeAsComplexAttribute != null) + return new ComplexAttributeValueConverter(prop); + + if (prop.PropertyType == typeof(DateTime)) + return new DateTimeAttributeValueConverter(prop, false); + + if (prop.PropertyType == typeof(DateTime?)) + return new DateTimeAttributeValueConverter(prop, true); + + if (prop.PropertyType == typeof(DateTimeOffset)) + return new DateTimeOffsetAttributeValueConverter(prop, false); + + if (prop.PropertyType == typeof(DateTimeOffset?)) + return new DateTimeOffsetAttributeValueConverter(prop, true); + + if (prop.PropertyType == typeof(Decimal) || prop.PropertyType == typeof(Decimal?)) + return new DecimalAttributeValueConverter(prop); + + if (prop.PropertyType == typeof(Guid)) + return new GuidAttributeValueConverter(prop, false); + + if (prop.PropertyType == typeof(Guid?)) + return new GuidAttributeValueConverter(prop, true); + + if (prop.PropertyType.IsEnum) + return new EnumAttributeValueConverter(prop, prop.PropertyType, true); + + Type enumType; + if (prop.PropertyType.IsGenericType && + prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && + (enumType = prop.PropertyType.GetGenericArguments()[0]).IsEnum) + { + return new EnumAttributeValueConverter(prop, enumType, true); + } + + var closedType = typeof(PrimitiveTypeAttributeValueConverter<>).MakeGenericType(prop.PropertyType); + return (IAttributeValueConverter)Activator.CreateInstance(closedType, prop); + } + + /// + /// Creates a cacheable model field representation from a PropertyInfo + /// + /// The property + /// A model field represenation + protected virtual ResourceTypeField CreateResourceTypeField(PropertyInfo prop) + { + var jsonKey = _namingConventions.GetFieldNameForProperty(prop); + + var type = prop.PropertyType; + + if (prop.PropertyType.CanWriteAsJsonApiAttribute()) + { + var converter = GetValueConverterForProperty(prop); + return new ResourceTypeAttribute(converter, prop, jsonKey); + } + + var selfLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); + var selfLinkTemplate = selfLinkTemplateAttribute == null ? null : selfLinkTemplateAttribute.TemplateString; + var relatedResourceLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); + var relatedResourceLinkTemplate = relatedResourceLinkTemplateAttribute == null ? null : relatedResourceLinkTemplateAttribute.TemplateString; + + var isToMany = + type.IsArray || + (type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType); + + if (!isToMany) return new ToOneResourceTypeRelationship(prop, jsonKey, type, selfLinkTemplate, relatedResourceLinkTemplate); + var relatedType = type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType(); + return new ToManyResourceTypeRelationship(prop, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate); + } + + /// + /// Calculates the ID property for a given resource type. + /// + /// The type to use to calculate the ID for + /// The ID property to use for this type + protected virtual PropertyInfo CalculateIdProperty(Type type) + { + return + type + .GetProperties() + .FirstOrDefault(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute))) + ?? type.GetProperty("Id"); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ResourceTypeRegistration.cs b/JSONAPI/Core/ResourceTypeRegistration.cs new file mode 100644 index 00000000..5eef1b6c --- /dev/null +++ b/JSONAPI/Core/ResourceTypeRegistration.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JSONAPI.Http; + +namespace JSONAPI.Core +{ + /// + /// Represents a type's registration with a registry + /// + public class ResourceTypeRegistration : IResourceTypeRegistration + { + private readonly IReadOnlyDictionary _fields; + private readonly Func _filterByIdExpressionFactory; + private readonly Func _sortByIdExpressionFactory; + + internal ResourceTypeRegistration(Type type, PropertyInfo idProperty, string resourceTypeName, + IDictionary fields, + Func filterByIdExpressionFactory, + Func sortByIdExpressionFactory) + { + IdProperty = idProperty; + Type = type; + ResourceTypeName = resourceTypeName; + _filterByIdExpressionFactory = filterByIdExpressionFactory; + _sortByIdExpressionFactory = sortByIdExpressionFactory; + Attributes = fields.Values.OfType().ToArray(); + Relationships = fields.Values.OfType().ToArray(); + _fields = new ReadOnlyDictionary(fields); + } + + public Type Type { get; private set; } + + public PropertyInfo IdProperty { get; private set; } + + public string ResourceTypeName { get; private set; } + + public ResourceTypeAttribute[] Attributes { get; private set; } + + public ResourceTypeRelationship[] Relationships { get; private set; } + + public string GetIdForResource(object resource) + { + return IdProperty.GetValue(resource).ToString(); + } + + public void SetIdForResource(object resource, string id) + { + IdProperty.SetValue(resource, id); // TODO: handle classes with non-string ID types + } + + public BinaryExpression GetFilterByIdExpression(ParameterExpression parameter, string id) + { + return _filterByIdExpressionFactory(parameter, id); + } + + public Expression GetSortByIdExpression(ParameterExpression parameter) + { + return _sortByIdExpressionFactory(parameter); + } + + public ResourceTypeField GetFieldByName(string name) + { + return _fields.ContainsKey(name) ? _fields[name] : null; + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ResourceTypeRegistry.cs b/JSONAPI/Core/ResourceTypeRegistry.cs index 154b601c..7f790e98 100644 --- a/JSONAPI/Core/ResourceTypeRegistry.cs +++ b/JSONAPI/Core/ResourceTypeRegistry.cs @@ -1,169 +1,25 @@ -using System.Collections.ObjectModel; -using JSONAPI.Attributes; -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using JSONAPI.Extensions; -using Newtonsoft.Json; namespace JSONAPI.Core { - /// - /// Allows configuring how to calculate JSON API keys based on CLR types and properties - /// - public interface INamingConventions - { - /// - /// Calculates the field name for a given property - /// - /// - /// - string GetFieldNameForProperty(PropertyInfo property); - - /// - /// Calculates the resource type name for a CLR type - /// - /// - /// - string GetResourceTypeNameForType(Type type); - } - - /// - /// Default implementation of INamingConventions - /// - public class DefaultNamingConventions : INamingConventions - { - private readonly IPluralizationService _pluralizationService; - - /// - /// Creates a new DefaultNamingConventions - /// - /// - public DefaultNamingConventions(IPluralizationService pluralizationService) - { - _pluralizationService = pluralizationService; - } - - /// - /// This method first checks if the property has a [JsonProperty] attribute. If so, - /// it uses the attribute's PropertyName. Otherwise, it falls back to taking the - /// property's name, and dasherizing it. - /// - /// - /// - public string GetFieldNameForProperty(PropertyInfo property) - { - var jsonPropertyAttribute = (JsonPropertyAttribute)property.GetCustomAttributes(typeof(JsonPropertyAttribute)).FirstOrDefault(); - return jsonPropertyAttribute != null ? jsonPropertyAttribute.PropertyName : property.Name.Dasherize(); - } - - /// - /// This method first checks if the type has a [JsonObject] attribute. If so, - /// it uses the attribute's Title. Otherwise it falls back to pluralizing the - /// type's name using the given and then - /// dasherizing that value. - /// - /// - /// - public string GetResourceTypeNameForType(Type type) - { - var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(JsonObjectAttribute)).ToList(); - - string title = type.Name; - if (attrs.Any()) - { - var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") - .Select(arg => arg.TypedValue.Value.ToString()).ToList(); - if (titles.Any()) title = titles.First(); - } - - return _pluralizationService.Pluralize(title).Dasherize(); - } - } - /// /// Default implementation of IModelRegistry /// public class ResourceTypeRegistry : IResourceTypeRegistry { - private readonly INamingConventions _namingConventions; + private readonly IDictionary _registrationsByName; + private readonly IDictionary _registrationsByType; /// /// Creates a new ResourceTypeRegistry /// - /// - public ResourceTypeRegistry(INamingConventions namingConventions) - { - _namingConventions = namingConventions; - RegistrationsByName = new Dictionary(); - RegistrationsByType = new Dictionary(); - } - - /// - /// Represents a type's registration with a registry - /// - protected sealed class ResourceTypeRegistration : IResourceTypeRegistration + public ResourceTypeRegistry() { - private readonly IReadOnlyDictionary _fields; - private readonly Func _filterByIdExpressionFactory; - private readonly Func _sortByIdExpressionFactory; - - internal ResourceTypeRegistration(Type type, PropertyInfo idProperty, string resourceTypeName, - IDictionary fields, - Func filterByIdExpressionFactory, - Func sortByIdExpressionFactory) - { - IdProperty = idProperty; - Type = type; - ResourceTypeName = resourceTypeName; - _filterByIdExpressionFactory = filterByIdExpressionFactory; - _sortByIdExpressionFactory = sortByIdExpressionFactory; - Attributes = fields.Values.OfType().ToArray(); - Relationships = fields.Values.OfType().ToArray(); - _fields = new ReadOnlyDictionary(fields); - } - - public Type Type { get; private set; } - - public PropertyInfo IdProperty { get; private set; } - - public string ResourceTypeName { get; private set; } - - public ResourceTypeAttribute[] Attributes { get; private set; } - - public ResourceTypeRelationship[] Relationships { get; private set; } - - public string GetIdForResource(object resource) - { - return IdProperty.GetValue(resource).ToString(); - } - - public void SetIdForResource(object resource, string id) - { - IdProperty.SetValue(resource, id); // TODO: handle classes with non-string ID types - } - - public BinaryExpression GetFilterByIdExpression(ParameterExpression parameter, string id) - { - return _filterByIdExpressionFactory(parameter, id); - } - - public Expression GetSortByIdExpression(ParameterExpression parameter) - { - return _sortByIdExpressionFactory(parameter); - } - - public ResourceTypeField GetFieldByName(string name) - { - return _fields.ContainsKey(name) ? _fields[name] : null; - } + _registrationsByName = new Dictionary(); + _registrationsByType = new Dictionary(); } - protected readonly IDictionary RegistrationsByName; - protected readonly IDictionary RegistrationsByType; - public bool TypeIsRegistered(Type type) { var registration = FindRegistrationForType(type); @@ -181,209 +37,54 @@ public IResourceTypeRegistration GetRegistrationForType(Type type) public IResourceTypeRegistration GetRegistrationForResourceTypeName(string resourceTypeName) { - lock (RegistrationsByName) + lock (_registrationsByName) { - ResourceTypeRegistration registration; - if (!RegistrationsByName.TryGetValue(resourceTypeName, out registration)) + IResourceTypeRegistration registration; + if (!_registrationsByName.TryGetValue(resourceTypeName, out registration)) throw new TypeRegistrationNotFoundException(resourceTypeName); return registration; } } - private ResourceTypeRegistration FindRegistrationForType(Type type) + public void AddRegistration(IResourceTypeRegistration registration) { - lock (RegistrationsByType) + lock (_registrationsByType) { - var currentType = type; - while (currentType != null && currentType != typeof(Object)) + lock (_registrationsByName) { - ResourceTypeRegistration registration; - if (RegistrationsByType.TryGetValue(currentType, out registration)) - return registration; - - // This particular type wasn't registered, but maybe the base type was. - currentType = currentType.BaseType; - } - } - - return null; - } - - /// - /// Registeres a type with this ResourceTypeRegistry, using a default resource type name. - /// - /// The type to register. - /// The resource type name to use - /// The factory to use to build an expression that - /// - public ResourceTypeRegistry RegisterResourceType(Type type, string resourceTypeName = null, - Func filterByIdFactory = null, Func sortByIdFactory = null) - { - lock (RegistrationsByType) - { - lock (RegistrationsByName) - { - if (resourceTypeName == null) - resourceTypeName = _namingConventions.GetResourceTypeNameForType(type); - - if (RegistrationsByType.ContainsKey(type)) + if (_registrationsByType.ContainsKey(registration.Type)) throw new InvalidOperationException(String.Format("The type `{0}` has already been registered.", - type.FullName)); + registration.Type.FullName)); - if (RegistrationsByName.ContainsKey(resourceTypeName)) + if (_registrationsByName.ContainsKey(registration.ResourceTypeName)) throw new InvalidOperationException( - String.Format("The resource type name `{0}` has already been registered.", resourceTypeName)); + String.Format("The resource type name `{0}` has already been registered.", + registration.ResourceTypeName)); - var fieldMap = new Dictionary(); - - var idProperty = CalculateIdProperty(type); - if (idProperty == null) - throw new InvalidOperationException(String.Format( - "Unable to determine Id property for type `{0}`.", type.Name)); - - var props = type.GetProperties().OrderBy(p => p.Name); - foreach (var prop in props) - { - if (prop == idProperty) continue; - - var ignore = prop.CustomAttributes.Any(c => c.AttributeType == typeof(JsonIgnoreAttribute)); - if (ignore) continue; - - var property = CreateResourceTypeField(prop); - var jsonKey = property.JsonKey; - - if (jsonKey == "id") - throw new InvalidOperationException( - String.Format("Failed to register type `{0}` because it contains a non-id property that would serialize as \"id\".", type.Name)); - - if (jsonKey == "type") - throw new InvalidOperationException( - String.Format("Failed to register type `{0}` because it contains a property that would serialize as \"type\".", type.Name)); - - if (fieldMap.ContainsKey(jsonKey)) - throw new InvalidOperationException( - String.Format("Failed to register type `{0}` because contains multiple properties that would serialize as `{1}`.", - type.Name, jsonKey)); - - fieldMap[jsonKey] = property; - } - - if (filterByIdFactory == null) - { - filterByIdFactory = (param, id) => - { - var propertyExpr = Expression.Property(param, idProperty); - var idExpr = Expression.Constant(id); - return Expression.Equal(propertyExpr, idExpr); - }; - } - - if (sortByIdFactory == null) - { - sortByIdFactory = param => Expression.Property(param, idProperty); - } - - var registration = new ResourceTypeRegistration(type, idProperty, resourceTypeName, fieldMap, filterByIdFactory, sortByIdFactory); - - RegistrationsByType.Add(type, registration); - RegistrationsByName.Add(resourceTypeName, registration); + _registrationsByType.Add(registration.Type, registration); + _registrationsByName.Add(registration.ResourceTypeName, registration); } } - - return this; } - /// - /// Gets a value converter for the given property - /// - /// - /// - protected virtual IAttributeValueConverter GetValueConverterForProperty(PropertyInfo prop) + private IResourceTypeRegistration FindRegistrationForType(Type type) { - var serializeAsComplexAttribute = prop.GetCustomAttribute(); - if (serializeAsComplexAttribute != null) - return new ComplexAttributeValueConverter(prop); - - if (prop.PropertyType == typeof(DateTime)) - return new DateTimeAttributeValueConverter(prop, false); - - if (prop.PropertyType == typeof(DateTime?)) - return new DateTimeAttributeValueConverter(prop, true); - - if (prop.PropertyType == typeof(DateTimeOffset)) - return new DateTimeOffsetAttributeValueConverter(prop, false); - - if (prop.PropertyType == typeof(DateTimeOffset?)) - return new DateTimeOffsetAttributeValueConverter(prop, true); - - if (prop.PropertyType == typeof (Decimal) || prop.PropertyType == typeof (Decimal?)) - return new DecimalAttributeValueConverter(prop); - - if (prop.PropertyType == typeof (Guid)) - return new GuidAttributeValueConverter(prop, false); - - if (prop.PropertyType == typeof(Guid?)) - return new GuidAttributeValueConverter(prop, true); - - if (prop.PropertyType.IsEnum) - return new EnumAttributeValueConverter(prop, prop.PropertyType, true); - - Type enumType; - if (prop.PropertyType.IsGenericType && - prop.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>) && - (enumType = prop.PropertyType.GetGenericArguments()[0]).IsEnum) + lock (_registrationsByType) { - return new EnumAttributeValueConverter(prop, enumType, true); - } - - var closedType = typeof(PrimitiveTypeAttributeValueConverter<>).MakeGenericType(prop.PropertyType); - return (IAttributeValueConverter)Activator.CreateInstance(closedType, prop); - } - - /// - /// Creates a cacheable model field representation from a PropertyInfo - /// - /// The property - /// A model field represenation - protected virtual ResourceTypeField CreateResourceTypeField(PropertyInfo prop) - { - var jsonKey = _namingConventions.GetFieldNameForProperty(prop); - - var type = prop.PropertyType; + var currentType = type; + while (currentType != null && currentType != typeof(Object)) + { + IResourceTypeRegistration registration; + if (_registrationsByType.TryGetValue(currentType, out registration)) + return registration; - if (prop.PropertyType.CanWriteAsJsonApiAttribute()) - { - var converter = GetValueConverterForProperty(prop); - return new ResourceTypeAttribute(converter, prop, jsonKey); + // This particular type wasn't registered, but maybe the base type was. + currentType = currentType.BaseType; + } } - var selfLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); - var selfLinkTemplate = selfLinkTemplateAttribute == null ? null : selfLinkTemplateAttribute.TemplateString; - var relatedResourceLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); - var relatedResourceLinkTemplate = relatedResourceLinkTemplateAttribute == null ? null : relatedResourceLinkTemplateAttribute.TemplateString; - - var isToMany = - type.IsArray || - (type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType); - - if (!isToMany) return new ToOneResourceTypeRelationship(prop, jsonKey, type, selfLinkTemplate, relatedResourceLinkTemplate); - var relatedType = type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType(); - return new ToManyResourceTypeRelationship(prop, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate); - } - - /// - /// Calculates the ID property for a given resource type. - /// - /// The type to use to calculate the ID for - /// The ID property to use for this type - protected virtual PropertyInfo CalculateIdProperty(Type type) - { - return - type - .GetProperties() - .FirstOrDefault(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute))) - ?? type.GetProperty("Id"); + return null; } } } diff --git a/JSONAPI/Core/ResourceTypeRelationship.cs b/JSONAPI/Core/ResourceTypeRelationship.cs new file mode 100644 index 00000000..d5ba5576 --- /dev/null +++ b/JSONAPI/Core/ResourceTypeRelationship.cs @@ -0,0 +1,45 @@ +using System; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// A ResourceTypeField representing a relationship to another resource type + /// + public abstract class ResourceTypeRelationship : ResourceTypeField + { + internal ResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, + string selfLinkTemplate, string relatedResourceLinkTemplate, bool isToMany) + : base(property, jsonKey) + { + RelatedType = relatedType; + SelfLinkTemplate = selfLinkTemplate; + RelatedResourceLinkTemplate = relatedResourceLinkTemplate; + IsToMany = isToMany; + } + + /// + /// Whether this relationship represents a link to a collection of resources or a single one. + /// + public bool IsToMany { get; private set; } + + /// + /// The type of resource found on the other side of this relationship + /// + public Type RelatedType { get; private set; } + + /// + /// The template for building URLs to access the relationship itself. + /// If the string {1} appears in the template, it will be replaced by the ID of resource this + /// relationship belongs to. + /// + public string SelfLinkTemplate { get; private set; } + + /// + /// The template for building URLs to access the data making up the other side of this relationship. + /// If the string {1} appears in the template, it will be replaced by the ID of resource this + /// relationship belongs to. + /// + public string RelatedResourceLinkTemplate { get; private set; } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ToManyResourceTypeRelationship.cs b/JSONAPI/Core/ToManyResourceTypeRelationship.cs new file mode 100644 index 00000000..c61b9e5b --- /dev/null +++ b/JSONAPI/Core/ToManyResourceTypeRelationship.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// A ModelProperty representing a relationship to a collection of resources + /// + public sealed class ToManyResourceTypeRelationship : ResourceTypeRelationship + { + internal ToManyResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, + string selfLinkTemplate, string relatedResourceLinkTemplate) + : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, true) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ToOneResourceTypeRelationship.cs b/JSONAPI/Core/ToOneResourceTypeRelationship.cs new file mode 100644 index 00000000..cb9a57c4 --- /dev/null +++ b/JSONAPI/Core/ToOneResourceTypeRelationship.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// A ModelProperty representing a relationship to a single resource + /// + public sealed class ToOneResourceTypeRelationship : ResourceTypeRelationship + { + internal ToOneResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, + string selfLinkTemplate, string relatedResourceLinkTemplate) + : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, false) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/TypeRegistrationNotFoundException.cs b/JSONAPI/Core/TypeRegistrationNotFoundException.cs index cac1d334..56c37c01 100644 --- a/JSONAPI/Core/TypeRegistrationNotFoundException.cs +++ b/JSONAPI/Core/TypeRegistrationNotFoundException.cs @@ -12,7 +12,7 @@ public class TypeRegistrationNotFoundException : Exception /// /// public TypeRegistrationNotFoundException(Type type) - : base("No model registration was found for the type \"" + type.Name + "\".") + : base("No type registration was found for the type \"" + type.Name + "\".") { } @@ -21,7 +21,7 @@ public TypeRegistrationNotFoundException(Type type) /// /// public TypeRegistrationNotFoundException(string resourceTypeName) - : base("No model registration was found for the type name \"" + resourceTypeName + "\".") + : base("No type registration was found for the type name \"" + resourceTypeName + "\".") { } } diff --git a/JSONAPI/Http/DocumentMaterializerLocator.cs b/JSONAPI/Http/DocumentMaterializerLocator.cs new file mode 100644 index 00000000..b5e3d9d0 --- /dev/null +++ b/JSONAPI/Http/DocumentMaterializerLocator.cs @@ -0,0 +1,41 @@ +using System; + +namespace JSONAPI.Http +{ + /// + /// Default implementation of + /// + public class DocumentMaterializerLocator : IDocumentMaterializerLocator + { + private readonly Func _nameResolver; + private readonly Func _typeResolver; + private readonly Func _relatedResourceMaterializerResolver; + + /// + /// Creates a new + /// + public DocumentMaterializerLocator(Func nameResolver, + Func typeResolver, + Func relatedResourceMaterializerResolver) + { + _nameResolver = nameResolver; + _typeResolver = typeResolver; + _relatedResourceMaterializerResolver = relatedResourceMaterializerResolver; + } + + public IDocumentMaterializer GetMaterializerByResourceTypeName(string resourceTypeName) + { + return _nameResolver(resourceTypeName); + } + + public IDocumentMaterializer GetMaterializerByType(Type type) + { + return _typeResolver(type); + } + + public IRelatedResourceDocumentMaterializer GetRelatedResourceMaterializer(string resourceTypeName, string relationshipName) + { + return _relatedResourceMaterializerResolver(resourceTypeName, relationshipName); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Http/IDocumentMaterializer.cs b/JSONAPI/Http/IDocumentMaterializer.cs index 7f211460..efe4d9bb 100644 --- a/JSONAPI/Http/IDocumentMaterializer.cs +++ b/JSONAPI/Http/IDocumentMaterializer.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq.Expressions; -using System.Net.Http; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using JSONAPI.Documents; @@ -10,7 +8,7 @@ namespace JSONAPI.Http /// /// This service provides the glue between JSONAPI.NET and your persistence layer. /// - public interface IDocumentMaterializer where T : class + public interface IDocumentMaterializer { /// /// Returns a document containing records that are filtered, sorted, @@ -18,30 +16,12 @@ public interface IDocumentMaterializer where T : class /// Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken); - /// - /// Returns a document containing records matching the provided lambda expression. - /// - Task GetRecordsMatchingExpression(Expression> filter, - HttpRequestMessage request, CancellationToken cancellationToken); - - /// - /// Returns a document containing the first record matching the provided lambda expression. - /// - Task GetSingleRecordMatchingExpression(Expression> filter, - HttpRequestMessage request, CancellationToken cancellationToken); - /// /// Returns a document with the resource identified by the given ID. /// Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken); - /// - /// Gets the resource(s) related to the resource identified by the given ID - /// - Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, - CancellationToken cancellationToken); - /// /// Creates a record corresponding to the data in the request document, and returns a document /// corresponding to the created record. diff --git a/JSONAPI/Http/IDocumentMaterializerLocator.cs b/JSONAPI/Http/IDocumentMaterializerLocator.cs new file mode 100644 index 00000000..46057845 --- /dev/null +++ b/JSONAPI/Http/IDocumentMaterializerLocator.cs @@ -0,0 +1,25 @@ +using System; + +namespace JSONAPI.Http +{ + /// + /// Service to lookup document materializers + /// + public interface IDocumentMaterializerLocator + { + /// + /// Resolves a for the given resource type name. + /// + IDocumentMaterializer GetMaterializerByResourceTypeName(string resourceTypeName); + + /// + /// Resolves a for the given type. + /// + IDocumentMaterializer GetMaterializerByType(Type type); + + /// + /// Resolves a for the given resource type and relationship. + /// + IRelatedResourceDocumentMaterializer GetRelatedResourceMaterializer(string resourceTypeName, string relationshipName); + } +} diff --git a/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..2c50f20f --- /dev/null +++ b/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,82 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; + +namespace JSONAPI.Http +{ + /// + /// Crafts a document corresponding to a related resource URL + /// + public interface IRelatedResourceDocumentMaterializer + { + /// + /// Builds a document containing the results of the relationship. + /// + Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, CancellationToken cancellationToken); + } + + /// + /// Base class for implementations of that use IQueryable to get related resources + /// for a to-many relationship. + /// + public abstract class QueryableToManyRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer + { + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + + /// + /// Creates a new QueryableRelatedResourceDocumentMaterializer + /// + protected QueryableToManyRelatedResourceDocumentMaterializer(IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder) + { + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + } + + public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, + CancellationToken cancellationToken) + { + var query = await GetRelatedQuery(primaryResourceId, cancellationToken); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); // TODO: allow implementors to specify includes and metadata + } + + /// + /// Gets the query for the related resources + /// + protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken); + } + + /// + /// Base class for implementations of that use IQueryable to get related resources + /// for a to-many relationship. + /// + public abstract class QueryableToOneRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer + { + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; + private readonly IBaseUrlService _baseUrlService; + + /// + /// Creates a new QueryableRelatedResourceDocumentMaterializer + /// + protected QueryableToOneRelatedResourceDocumentMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService) + { + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; + _baseUrlService = baseUrlService; + } + + public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, + CancellationToken cancellationToken) + { + var record = await GeRelatedRecord(primaryResourceId, cancellationToken); + var baseUrl = _baseUrlService.GetBaseUrl(request); + return _singleResourceDocumentBuilder.BuildDocument(record, baseUrl, null, null); // TODO: allow implementors to specify includes and metadata + } + + /// + /// Gets the query for the related resources + /// + protected abstract Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/JSONAPI/Http/JsonApiController.cs b/JSONAPI/Http/JsonApiController.cs index a69db870..78f5b66a 100644 --- a/JSONAPI/Http/JsonApiController.cs +++ b/JSONAPI/Http/JsonApiController.cs @@ -6,41 +6,38 @@ namespace JSONAPI.Http { /// - /// This generic ApiController provides JSON API-compatible endpoints corresponding to a - /// registered type. + /// This ApiController is capable of serving all requests using JSON API. /// - public class JsonApiController : ApiController where T : class + public class JsonApiController : ApiController { - private readonly IDocumentMaterializer _documentMaterializer; + private readonly IDocumentMaterializerLocator _documentMaterializerLocator; /// - /// Creates a new ApiController + /// Creates a new JsonApiController /// - /// - public JsonApiController(IDocumentMaterializer documentMaterializer) + /// The service locator to get document materializers for a given resource type. + public JsonApiController(IDocumentMaterializerLocator documentMaterializerLocator) { - _documentMaterializer = documentMaterializer; + _documentMaterializerLocator = documentMaterializerLocator; } /// /// Returns a document corresponding to a set of records of this type. /// - /// - public virtual async Task Get(CancellationToken cancellationToken) + public virtual async Task GetResourceCollection(string resourceType, CancellationToken cancellationToken) { - var document = await _documentMaterializer.GetRecords(Request, cancellationToken); + var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); + var document = await materializer.GetRecords(Request, cancellationToken); return Ok(document); } /// /// Returns a document corresponding to the single record matching the ID. /// - /// - /// - /// - public virtual async Task Get(string id, CancellationToken cancellationToken) + public virtual async Task Get(string resourceType, string id, CancellationToken cancellationToken) { - var document = await _documentMaterializer.GetRecordById(id, Request, cancellationToken); + var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); + var document = await materializer.GetRecordById(id, Request, cancellationToken); return Ok(document); } @@ -48,50 +45,40 @@ public virtual async Task Get(string id, CancellationToken ca /// Returns a document corresponding to the resource(s) related to the resource identified by the ID, /// and the relationship name. /// - /// - /// - /// - /// - public virtual async Task GetRelatedResource(string id, string relationshipName, CancellationToken cancellationToken) + public virtual async Task GetRelatedResource(string resourceType, string id, string relationshipName, CancellationToken cancellationToken) { - var document = await _documentMaterializer.GetRelated(id, relationshipName, Request, cancellationToken); + var materializer = _documentMaterializerLocator.GetRelatedResourceMaterializer(resourceType, relationshipName); + var document = await materializer.GetRelatedResourceDocument(id, Request, cancellationToken); return Ok(document); } - /// /// Creates a new record corresponding to the data in the request document. /// - /// - /// - /// - public virtual async Task Post([FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) + public virtual async Task Post(string resourceType, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) { - var document = await _documentMaterializer.CreateRecord(requestDocument, Request, cancellationToken); + var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); + var document = await materializer.CreateRecord(requestDocument, Request, cancellationToken); return Ok(document); } /// /// Updates the record with the given ID with data from the request payloaad. /// - /// - /// - /// - /// - public virtual async Task Patch(string id, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) + public virtual async Task Patch(string resourceType, string id, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) { - var document = await _documentMaterializer.UpdateRecord(id, requestDocument, Request, cancellationToken); + var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); + var document = await materializer.UpdateRecord(id, requestDocument, Request, cancellationToken); return Ok(document); } /// /// Deletes the record corresponding to the ID. /// - /// - /// - public virtual async Task Delete(string id, CancellationToken cancellationToken) + public virtual async Task Delete(string resourceType, string id, CancellationToken cancellationToken) { - var document = await _documentMaterializer.DeleteRecord(id, cancellationToken); + var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); + var document = await materializer.DeleteRecord(id, cancellationToken); return Ok(document); } } diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 4127957f..ce3f6a95 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -19,7 +18,7 @@ namespace JSONAPI.Http /// /// /// - public abstract class MappedDocumentMaterializer : IDocumentMaterializer where TDto : class + public abstract class MappedDocumentMaterializer : IDocumentMaterializer where TDto : class { /// /// Materializes a document for the resources found on the other side of the to-many relationship belonging to the resource. @@ -38,8 +37,6 @@ protected delegate Task MaterializeDocumentForToOneRela private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IQueryableEnumerationTransformer _queryableEnumerationTransformer; private readonly IResourceTypeRegistry _resourceTypeRegistry; - private readonly IDictionary _toManyRelatedResourceMaterializers; - private readonly IDictionary _toOneRelatedResourceMaterializers; /// /// Gets a query returning all entities for this endpoint @@ -71,18 +68,14 @@ protected MappedDocumentMaterializer( _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _queryableEnumerationTransformer = queryableEnumerationTransformer; _resourceTypeRegistry = resourceTypeRegistry; - _toManyRelatedResourceMaterializers = - new ConcurrentDictionary(); - _toOneRelatedResourceMaterializers = - new ConcurrentDictionary(); } - public async Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) + private string ResourceTypeName { - return await GetRecordsMatchingExpression(m => true, request, cancellationToken); + get { return _resourceTypeRegistry.GetRegistrationForType(typeof (TDto)).ResourceTypeName; } } - public async Task GetRecordsMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) + public async Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { var entityQuery = GetQuery(); var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; @@ -91,19 +84,6 @@ public async Task GetRecordsMatchingExpression(Expr return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths); } - public async Task GetSingleRecordMatchingExpression(Expression> filter, HttpRequestMessage request, CancellationToken cancellationToken) - { - var entityQuery = GetQuery(); - var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; - var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); - var mappedQuery = GetMappedQuery(entityQuery, includePaths); - var filteredQuery = mappedQuery.Where(filter); - var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(filteredQuery, cancellationToken); - - var baseUrl = _baseUrlService.GetBaseUrl(request); - return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); - } - public async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var entityQuery = GetByIdQuery(id); @@ -111,7 +91,9 @@ public async Task GetRecordById(string id, HttpRequestM var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); var mappedQuery = GetMappedQuery(entityQuery, includePaths); var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); - if (primaryResource == null) throw JsonApiException.CreateForNotFound(string.Format("No record exists with ID {0} for the requested type.", id)); + if (primaryResource == null) + throw JsonApiException.CreateForNotFound( + string.Format("No record exists with type `{0}` and ID `{1}`.", ResourceTypeName, id)); var baseUrl = _baseUrlService.GetBaseUrl(request); return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); @@ -119,44 +101,30 @@ public async Task GetRecordById(string id, HttpRequestM public async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken) { - var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); - - var primaryEntityQuery = GetByIdQuery(id); - var mappedQuery = GetMappedQuery(primaryEntityQuery, null); - var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); - if (primaryResource == null) - { - var dtoRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); - throw JsonApiException.CreateForNotFound(string.Format( - "No resource of type `{0}` exists with id `{1}`.", - dtoRegistration.ResourceTypeName, id)); - } - - var relationship = (ResourceTypeRelationship)registration.GetFieldByName(relationshipKey); - if (relationship == null) - throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", - relationshipKey, registration.ResourceTypeName, id)); - - if (relationship.IsToMany) - { - MaterializeDocumentForToManyRelationship documentFactory; - if (!_toManyRelatedResourceMaterializers.TryGetValue(relationship, out documentFactory)) - { - documentFactory = GetMaterializerForToManyRelatedResource(relationship); - _toManyRelatedResourceMaterializers.Add(relationship, documentFactory); - } - return await documentFactory(primaryResource, request, cancellationToken); - } - else - { - MaterializeDocumentForToOneRelationship relatedResourceMaterializer; - if (!_toOneRelatedResourceMaterializers.TryGetValue(relationship, out relatedResourceMaterializer)) - { - relatedResourceMaterializer = GetMaterializerForToOneRelatedResource(relationship); - _toOneRelatedResourceMaterializers.Add(relationship, relatedResourceMaterializer); - } - return await relatedResourceMaterializer(primaryResource, request, cancellationToken); - } + //var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); + + //var entityQuery = GetByIdQuery(id); + //var includePaths = GetIncludePathsForSingleResource() ?? new Expression>[] { }; + //var mappedQuery = GetMappedQuery(entityQuery, includePaths); + //var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); + //if (primaryResource == null) + // throw JsonApiException.CreateForNotFound( + // string.Format("No record exists with type `{0}` and ID `{1}`.", ResourceTypeName, id)); + + //var relationship = (ResourceTypeRelationship)registration.GetFieldByName(relationshipKey); + //if (relationship == null) + // throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", + // relationshipKey, registration.ResourceTypeName, id)); + + //var toManyRelationship = relationship as ToManyResourceTypeRelationship; + //if (toManyRelationship != null) + // return await toManyRelationship.GetRelatedResourceCollection(id, request, cancellationToken); + + //var toOneRelationship = relationship as ToOneResourceTypeRelationship; + //if (toOneRelationship != null) + // return await toOneRelationship.GetRelatedResource(id, request, cancellationToken); + + throw new NotSupportedException(); } public Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, @@ -192,16 +160,6 @@ protected virtual Expression>[] GetIncludePathsForSingleResou return null; } - /// - /// Returns a materialization delegate to handle related resource requests for a to-many relationship - /// - protected abstract MaterializeDocumentForToManyRelationship GetMaterializerForToManyRelatedResource(ResourceTypeRelationship relationship); - - /// - /// Returns a materialization delegate to handle related resource requests for a to-one relationship - /// - protected abstract MaterializeDocumentForToOneRelationship GetMaterializerForToOneRelatedResource(ResourceTypeRelationship relationship); - private string ConvertToJsonKeyPath(Expression> expression) { var visitor = new PathVisitor(_resourceTypeRegistry); diff --git a/JSONAPI/Http/PascalizedControllerSelector.cs b/JSONAPI/Http/PascalizedControllerSelector.cs deleted file mode 100644 index 3c4f3c34..00000000 --- a/JSONAPI/Http/PascalizedControllerSelector.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Dispatcher; -using JSONAPI.Extensions; - -namespace JSONAPI.Http -{ - /// - /// Chooses a controller based on the pascal-case version of the default controller name - /// - public class PascalizedControllerSelector : DefaultHttpControllerSelector - { - /// The configuration to use - public PascalizedControllerSelector(HttpConfiguration configuration) : base(configuration) - { - } - - public override string GetControllerName(HttpRequestMessage request) - { - var baseControllerName = base.GetControllerName(request); - return baseControllerName.Pascalize(); - } - } -} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index c655cb7f..24d00452 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -51,6 +51,7 @@ ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + False ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll @@ -66,6 +67,22 @@ + + + + + + + + + + + + + + + + @@ -92,8 +109,7 @@ - - + @@ -106,7 +122,6 @@ - diff --git a/JSONAPI/Properties/AssemblyInfo.cs b/JSONAPI/Properties/AssemblyInfo.cs index 6bef268b..53bb7fbb 100644 --- a/JSONAPI/Properties/AssemblyInfo.cs +++ b/JSONAPI/Properties/AssemblyInfo.cs @@ -25,6 +25,7 @@ [assembly: InternalsVisibleTo("JSONAPI.Tests")] [assembly: InternalsVisibleTo("JSONAPI.EntityFramework")] [assembly: InternalsVisibleTo("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] +[assembly: InternalsVisibleTo("JSONAPI.Autofac")] // This assembly is the default dynamic assembly generated Castle DynamicProxy, // used by Moq. Paste in a single line. From 0d2091b0f402b5727333da1fb5ee13fe7677aa15 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 12 Jul 2015 19:48:12 -0400 Subject: [PATCH 124/186] split into new files --- .../IRelatedResourceDocumentMaterializer.cs | 64 ------------------- ...ManyRelatedResourceDocumentMaterializer.cs | 38 +++++++++++ ...oOneRelatedResourceDocumentMaterializer.cs | 41 ++++++++++++ JSONAPI/JSONAPI.csproj | 2 + 4 files changed, 81 insertions(+), 64 deletions(-) create mode 100644 JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs create mode 100644 JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs diff --git a/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs index 2c50f20f..893c9069 100644 --- a/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/IRelatedResourceDocumentMaterializer.cs @@ -1,9 +1,7 @@ -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using JSONAPI.Documents; -using JSONAPI.Documents.Builders; namespace JSONAPI.Http { @@ -17,66 +15,4 @@ public interface IRelatedResourceDocumentMaterializer /// Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, CancellationToken cancellationToken); } - - /// - /// Base class for implementations of that use IQueryable to get related resources - /// for a to-many relationship. - /// - public abstract class QueryableToManyRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer - { - private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; - - /// - /// Creates a new QueryableRelatedResourceDocumentMaterializer - /// - protected QueryableToManyRelatedResourceDocumentMaterializer(IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder) - { - _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; - } - - public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, - CancellationToken cancellationToken) - { - var query = await GetRelatedQuery(primaryResourceId, cancellationToken); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); // TODO: allow implementors to specify includes and metadata - } - - /// - /// Gets the query for the related resources - /// - protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken); - } - - /// - /// Base class for implementations of that use IQueryable to get related resources - /// for a to-many relationship. - /// - public abstract class QueryableToOneRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer - { - private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; - private readonly IBaseUrlService _baseUrlService; - - /// - /// Creates a new QueryableRelatedResourceDocumentMaterializer - /// - protected QueryableToOneRelatedResourceDocumentMaterializer( - ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService) - { - _singleResourceDocumentBuilder = singleResourceDocumentBuilder; - _baseUrlService = baseUrlService; - } - - public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, - CancellationToken cancellationToken) - { - var record = await GeRelatedRecord(primaryResourceId, cancellationToken); - var baseUrl = _baseUrlService.GetBaseUrl(request); - return _singleResourceDocumentBuilder.BuildDocument(record, baseUrl, null, null); // TODO: allow implementors to specify includes and metadata - } - - /// - /// Gets the query for the related resources - /// - protected abstract Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken); - } } \ No newline at end of file diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..08fd04ed --- /dev/null +++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; + +namespace JSONAPI.Http +{ + /// + /// Base class for implementations of that use IQueryable to get related resources + /// for a to-many relationship. + /// + public abstract class QueryableToManyRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer + { + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + + /// + /// Creates a new QueryableRelatedResourceDocumentMaterializer + /// + protected QueryableToManyRelatedResourceDocumentMaterializer(IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder) + { + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + } + + public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, + CancellationToken cancellationToken) + { + var query = await GetRelatedQuery(primaryResourceId, cancellationToken); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); // TODO: allow implementors to specify includes and metadata + } + + /// + /// Gets the query for the related resources + /// + protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..e9390bb0 --- /dev/null +++ b/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,41 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; + +namespace JSONAPI.Http +{ + /// + /// Base class for implementations of that use IQueryable to get related resources + /// for a to-many relationship. + /// + public abstract class QueryableToOneRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer + { + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; + private readonly IBaseUrlService _baseUrlService; + + /// + /// Creates a new QueryableRelatedResourceDocumentMaterializer + /// + protected QueryableToOneRelatedResourceDocumentMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService) + { + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; + _baseUrlService = baseUrlService; + } + + public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, + CancellationToken cancellationToken) + { + var record = await GeRelatedRecord(primaryResourceId, cancellationToken); + var baseUrl = _baseUrlService.GetBaseUrl(request); + return _singleResourceDocumentBuilder.BuildDocument(record, baseUrl, null, null); // TODO: allow implementors to specify includes and metadata + } + + /// + /// Gets the query for the related resources + /// + protected abstract Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 24d00452..abfa5190 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -84,6 +84,8 @@ + + From 68dfca7f2557ec66dc50883d6d21f6e49ef8b3a6 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 10:09:41 -0400 Subject: [PATCH 125/186] remove unused delegates --- JSONAPI/Http/MappedDocumentMaterializer.cs | 40 ---------------------- 1 file changed, 40 deletions(-) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index ce3f6a95..7fa104a4 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -20,18 +20,6 @@ namespace JSONAPI.Http /// public abstract class MappedDocumentMaterializer : IDocumentMaterializer where TDto : class { - /// - /// Materializes a document for the resources found on the other side of the to-many relationship belonging to the resource. - /// - protected delegate Task MaterializeDocumentForToManyRelationship( - TDto resource, HttpRequestMessage request, CancellationToken cancellationToken); - - /// - /// Materializes a document for the resources found on the other side of the to-one relationship belonging to the resource. - /// - protected delegate Task MaterializeDocumentForToOneRelationship( - TDto resource, HttpRequestMessage request, CancellationToken cancellationToken); - private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly IBaseUrlService _baseUrlService; private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; @@ -99,34 +87,6 @@ public async Task GetRecordById(string id, HttpRequestM return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); } - public async Task GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken) - { - //var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto)); - - //var entityQuery = GetByIdQuery(id); - //var includePaths = GetIncludePathsForSingleResource() ?? new Expression>[] { }; - //var mappedQuery = GetMappedQuery(entityQuery, includePaths); - //var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken); - //if (primaryResource == null) - // throw JsonApiException.CreateForNotFound( - // string.Format("No record exists with type `{0}` and ID `{1}`.", ResourceTypeName, id)); - - //var relationship = (ResourceTypeRelationship)registration.GetFieldByName(relationshipKey); - //if (relationship == null) - // throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.", - // relationshipKey, registration.ResourceTypeName, id)); - - //var toManyRelationship = relationship as ToManyResourceTypeRelationship; - //if (toManyRelationship != null) - // return await toManyRelationship.GetRelatedResourceCollection(id, request, cancellationToken); - - //var toOneRelationship = relationship as ToOneResourceTypeRelationship; - //if (toOneRelationship != null) - // return await toOneRelationship.GetRelatedResource(id, request, cancellationToken); - - throw new NotSupportedException(); - } - public Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken) { From c2eaa23bd145ed746742f9c682ae5aebf5cac211 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 11:38:49 -0400 Subject: [PATCH 126/186] sanitize configuration interfaces slightly --- JSONAPI.Autofac/JsonApiAutofacModule.cs | 1 - .../IResourceTypeConfiguration.cs | 2 +- .../IResourceTypeConfigurator.cs | 24 +++++++++++++++++++ .../IResourceTypeRelationshipConfiguration.cs | 16 +++++++++++++ .../IResourceTypeRelationshipConfigurator.cs | 15 ++++++++++++ JSONAPI/Configuration/JsonApiConfiguration.cs | 2 +- .../ResourceTypeConfiguration.cs | 15 ++++-------- .../ResourceTypeRelationshipConfiguration.cs | 18 ++------------ JSONAPI/JSONAPI.csproj | 3 +++ 9 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 JSONAPI/Configuration/IResourceTypeConfigurator.cs create mode 100644 JSONAPI/Configuration/IResourceTypeRelationshipConfiguration.cs create mode 100644 JSONAPI/Configuration/IResourceTypeRelationshipConfigurator.cs diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index fa88f4e5..6c2949aa 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -41,7 +41,6 @@ protected override void Load(ContainerBuilder builder) foreach (var relationshipConfiguration in resourceTypeConfiguration.RelationshipConfigurations) { - var prop = relationshipConfiguration.Key; var relationship = relationshipConfiguration.Value; builder.RegisterType(relationship.MaterializerType); } diff --git a/JSONAPI/Configuration/IResourceTypeConfiguration.cs b/JSONAPI/Configuration/IResourceTypeConfiguration.cs index 46d1f72f..2379b303 100644 --- a/JSONAPI/Configuration/IResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/IResourceTypeConfiguration.cs @@ -7,7 +7,7 @@ namespace JSONAPI.Configuration { /// - /// Configuration mechanism for resource types + /// Results of configuring a resource type /// public interface IResourceTypeConfiguration { diff --git a/JSONAPI/Configuration/IResourceTypeConfigurator.cs b/JSONAPI/Configuration/IResourceTypeConfigurator.cs new file mode 100644 index 00000000..9758ad33 --- /dev/null +++ b/JSONAPI/Configuration/IResourceTypeConfigurator.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq.Expressions; +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Configuration mechanism for resource types + /// + public interface IResourceTypeConfigurator + { + /// + /// Configures the relationship corresponding to the specified property + /// + void ConfigureRelationship(Expression> property, + Action configurationAction); + + /// + /// Specifies the materializer to use for this resource type + /// + /// + void UseDocumentMaterializer() where TMaterializer : IDocumentMaterializer; + } +} \ No newline at end of file diff --git a/JSONAPI/Configuration/IResourceTypeRelationshipConfiguration.cs b/JSONAPI/Configuration/IResourceTypeRelationshipConfiguration.cs new file mode 100644 index 00000000..ea420fba --- /dev/null +++ b/JSONAPI/Configuration/IResourceTypeRelationshipConfiguration.cs @@ -0,0 +1,16 @@ +using System; +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Results of configuring a relationship + /// + public interface IResourceTypeRelationshipConfiguration + { + /// + /// The type to use for materializing this relationship + /// + Type MaterializerType { get; } + } +} \ No newline at end of file diff --git a/JSONAPI/Configuration/IResourceTypeRelationshipConfigurator.cs b/JSONAPI/Configuration/IResourceTypeRelationshipConfigurator.cs new file mode 100644 index 00000000..1d072bfe --- /dev/null +++ b/JSONAPI/Configuration/IResourceTypeRelationshipConfigurator.cs @@ -0,0 +1,15 @@ +using JSONAPI.Http; + +namespace JSONAPI.Configuration +{ + /// + /// Configuration mechanism for relationships + /// + public interface IResourceTypeRelationshipConfigurator + { + /// + /// Specify the materializer type to use for this particular relationship + /// + void UseMaterializer() where TMaterializerType : IRelatedResourceDocumentMaterializer; + } +} \ No newline at end of file diff --git a/JSONAPI/Configuration/JsonApiConfiguration.cs b/JSONAPI/Configuration/JsonApiConfiguration.cs index 9a48b361..4f7c229a 100644 --- a/JSONAPI/Configuration/JsonApiConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiConfiguration.cs @@ -67,7 +67,7 @@ public void RegisterResourceType(Action /// Registers an entity type/resource type pair for use with MappedDocumentMaterializer /> /// - public void RegisterMappedType(Action> configurationAction = null) + public void RegisterMappedType(Action> configurationAction = null) where TMaterializer : MappedDocumentMaterializer where TResourceType : class { diff --git a/JSONAPI/Configuration/ResourceTypeConfiguration.cs b/JSONAPI/Configuration/ResourceTypeConfiguration.cs index 3c632680..3797670c 100644 --- a/JSONAPI/Configuration/ResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/ResourceTypeConfiguration.cs @@ -12,7 +12,7 @@ namespace JSONAPI.Configuration /// Configuration mechanism for resource types. /// /// - public sealed class ResourceTypeConfiguration : IResourceTypeConfiguration + public sealed class ResourceTypeConfiguration : IResourceTypeConfiguration, IResourceTypeConfigurator { private readonly IResourceTypeRegistrar _resourceTypeRegistrar; @@ -30,28 +30,21 @@ internal ResourceTypeConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) public Func FilterByIdExpressionFactory { get; private set; } public Func SortByIdExpressionFactory { get; private set; } - /// - /// Configures the relationship corresponding to the specified property - /// public void ConfigureRelationship(Expression> property, - Action relationshipConfiguration) + Action configurationAction) { if (property == null) throw new ArgumentNullException("property"); - if (relationshipConfiguration == null) throw new ArgumentNullException("relationshipConfiguration"); + if (configurationAction == null) throw new ArgumentNullException("configurationAction"); var member = (MemberExpression) property.Body; var propertyInfo = (PropertyInfo) member.Member; var config = new ResourceTypeRelationshipConfiguration(); - relationshipConfiguration(config); + configurationAction(config); RelationshipConfigurations[propertyInfo] = config; } - /// - /// Specifies the materializer to use for this resource type - /// - /// public void UseDocumentMaterializer() where TMaterializer : IDocumentMaterializer { diff --git a/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs b/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs index e63be10f..3998a6a2 100644 --- a/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs +++ b/JSONAPI/Configuration/ResourceTypeRelationshipConfiguration.cs @@ -4,9 +4,9 @@ namespace JSONAPI.Configuration { /// - /// Default implementation of + /// Allows configuring a relationship /// - public sealed class ResourceTypeRelationshipConfiguration : IResourceTypeRelationshipConfiguration + public sealed class ResourceTypeRelationshipConfiguration : IResourceTypeRelationshipConfiguration, IResourceTypeRelationshipConfigurator { internal ResourceTypeRelationshipConfiguration() { @@ -14,24 +14,10 @@ internal ResourceTypeRelationshipConfiguration() public Type MaterializerType { get; private set; } - /// - /// Specify the materializer type to use for this particular relationship - /// public void UseMaterializer() where TMaterializerType : IRelatedResourceDocumentMaterializer { MaterializerType = typeof (TMaterializerType); } } - - /// - /// Configuration mechanism for relationships - /// - public interface IResourceTypeRelationshipConfiguration - { - /// - /// The type to use for materializing this relationship - /// - Type MaterializerType { get; } - } } \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index abfa5190..6e3552f3 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -68,6 +68,9 @@ + + + From 6ca601000c1fc6bab0f90e910569e7d0906c4e86 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 12:50:23 -0400 Subject: [PATCH 127/186] add way to configure a default related resource materializer per resource type --- JSONAPI.Autofac/JsonApiAutofacModule.cs | 9 +++++++++ JSONAPI/Configuration/IResourceTypeConfiguration.cs | 5 +++++ JSONAPI/Configuration/IResourceTypeConfigurator.cs | 7 +++++++ JSONAPI/Configuration/ResourceTypeConfiguration.cs | 8 +++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 6c2949aa..fd81bd64 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -93,10 +93,19 @@ protected override void Load(ContainerBuilder builder) new TypedParameter(typeof(ResourceTypeRelationship), relationship) }; + // First, see if they have set an explicit materializer for this relationship IResourceTypeRelationshipConfiguration relationshipConfiguration; if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property, out relationshipConfiguration) && relationshipConfiguration.MaterializerType != null) return (IRelatedResourceDocumentMaterializer)context.Resolve(relationshipConfiguration.MaterializerType, parameters); + + // They didn't set an explicit materializer. See if they specified a factory for this resource type. + if (configuration.RelatedResourceMaterializerTypeFactory != null) + { + var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship); + return (IRelatedResourceDocumentMaterializer)context.Resolve(materializerType, parameters); + } + return context.Resolve(parameters); }; return factory; diff --git a/JSONAPI/Configuration/IResourceTypeConfiguration.cs b/JSONAPI/Configuration/IResourceTypeConfiguration.cs index 2379b303..98fb7ef8 100644 --- a/JSONAPI/Configuration/IResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/IResourceTypeConfiguration.cs @@ -26,6 +26,11 @@ public interface IResourceTypeConfiguration /// Type DocumentMaterializerType { get; } + /// + /// A factory to determine the related resource materializer for a given relationship + /// + Func RelatedResourceMaterializerTypeFactory { get; } + /// /// Configurations for this type's resources /// diff --git a/JSONAPI/Configuration/IResourceTypeConfigurator.cs b/JSONAPI/Configuration/IResourceTypeConfigurator.cs index 9758ad33..6b6fb351 100644 --- a/JSONAPI/Configuration/IResourceTypeConfigurator.cs +++ b/JSONAPI/Configuration/IResourceTypeConfigurator.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using JSONAPI.Core; using JSONAPI.Http; namespace JSONAPI.Configuration @@ -20,5 +21,11 @@ void ConfigureRelationship(Expression> property, /// /// void UseDocumentMaterializer() where TMaterializer : IDocumentMaterializer; + + /// + /// Allows specifying a default materializer for related resources. + /// + /// + void UseDefaultRelatedResourceMaterializer(Func materializerTypeFactory); } } \ No newline at end of file diff --git a/JSONAPI/Configuration/ResourceTypeConfiguration.cs b/JSONAPI/Configuration/ResourceTypeConfiguration.cs index 3797670c..c0aa5185 100644 --- a/JSONAPI/Configuration/ResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/ResourceTypeConfiguration.cs @@ -12,7 +12,7 @@ namespace JSONAPI.Configuration /// Configuration mechanism for resource types. /// /// - public sealed class ResourceTypeConfiguration : IResourceTypeConfiguration, IResourceTypeConfigurator + public sealed class ResourceTypeConfiguration : IResourceTypeConfigurator, IResourceTypeConfiguration { private readonly IResourceTypeRegistrar _resourceTypeRegistrar; @@ -26,6 +26,7 @@ internal ResourceTypeConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) public string ResourceTypeName { get; private set; } public Type ClrType { get; private set; } public Type DocumentMaterializerType { get; private set; } + public Func RelatedResourceMaterializerTypeFactory { get; private set; } public IDictionary RelationshipConfigurations { get; private set; } public Func FilterByIdExpressionFactory { get; private set; } public Func SortByIdExpressionFactory { get; private set; } @@ -51,6 +52,11 @@ public void UseDocumentMaterializer() DocumentMaterializerType = typeof (TMaterializer); } + public void UseDefaultRelatedResourceMaterializer(Func materializerTypeFactory) + { + RelatedResourceMaterializerTypeFactory = materializerTypeFactory; + } + /// /// Overrides the resource type name from naming conventions /// From b963cd6e0fa79039485793fd2b7609493e608d50 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 12:53:44 -0400 Subject: [PATCH 128/186] more configuration interface cleanup --- .../Startup.cs | 1 + .../JsonApiAutofacConfigurationExtensions.cs | 6 +++--- JSONAPI.TodoMVC.API/Startup.cs | 1 + .../Configuration/IResourceTypeConfigurator.cs | 17 +++++++++++++++++ JSONAPI/Configuration/JsonApiConfiguration.cs | 2 +- .../Configuration/ResourceTypeConfiguration.cs | 10 ---------- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index 6a2343cc..14d5794d 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -11,6 +11,7 @@ using JSONAPI.Autofac.EntityFramework; using JSONAPI.Configuration; using JSONAPI.EntityFramework; +using JSONAPI.EntityFramework.Configuration; using Owin; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp diff --git a/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs index 65be9bf8..eca72b87 100644 --- a/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs +++ b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs @@ -2,12 +2,12 @@ using JSONAPI.Configuration; using JSONAPI.EntityFramework.Http; -namespace JSONAPI.Autofac.EntityFramework +namespace JSONAPI.EntityFramework.Configuration { - public static class JsonApiAutofacConfigurationExtensions + public static class JsonApiConfigurationExtensions { public static void RegisterEntityFrameworkResourceType(this JsonApiConfiguration jsonApiConfiguration, - Action> configurationAction = null) where TResourceType : class + Action> configurationAction = null) where TResourceType : class { jsonApiConfiguration.RegisterResourceType(c => { diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index 82429343..b5b6f0b0 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -7,6 +7,7 @@ using JSONAPI.Autofac.EntityFramework; using JSONAPI.Configuration; using JSONAPI.Core; +using JSONAPI.EntityFramework.Configuration; using JSONAPI.TodoMVC.API.Models; using Owin; diff --git a/JSONAPI/Configuration/IResourceTypeConfigurator.cs b/JSONAPI/Configuration/IResourceTypeConfigurator.cs index 6b6fb351..a861bbfe 100644 --- a/JSONAPI/Configuration/IResourceTypeConfigurator.cs +++ b/JSONAPI/Configuration/IResourceTypeConfigurator.cs @@ -27,5 +27,22 @@ void ConfigureRelationship(Expression> property, /// /// void UseDefaultRelatedResourceMaterializer(Func materializerTypeFactory); + + /// + /// Overrides the resource type name from naming conventions + /// + /// + void OverrideDefaultResourceTypeName(string resourceTypeName); + + /// + /// Specifies a function to use build expressions that allow filtering resources of this type by ID + /// + void OverrideDefaultFilterById(Func filterByIdExpressionFactory); + + /// + /// Specifies a function to use build expressions that allow sorting resources of this type by ID + /// + void OverrideDefaultSortById(Func sortByIdExpressionFactory); + } } \ No newline at end of file diff --git a/JSONAPI/Configuration/JsonApiConfiguration.cs b/JSONAPI/Configuration/JsonApiConfiguration.cs index 4f7c229a..22722ba4 100644 --- a/JSONAPI/Configuration/JsonApiConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiConfiguration.cs @@ -56,7 +56,7 @@ public JsonApiConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) /// /// Registers a resource type with the configuration /// - public void RegisterResourceType(Action> configurationAction = null) + public void RegisterResourceType(Action> configurationAction = null) { var configuration = new ResourceTypeConfiguration(_resourceTypeRegistrar); if (configurationAction != null) diff --git a/JSONAPI/Configuration/ResourceTypeConfiguration.cs b/JSONAPI/Configuration/ResourceTypeConfiguration.cs index c0aa5185..ad08449e 100644 --- a/JSONAPI/Configuration/ResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/ResourceTypeConfiguration.cs @@ -57,26 +57,16 @@ public void UseDefaultRelatedResourceMaterializer(Func - /// Overrides the resource type name from naming conventions - /// - /// public void OverrideDefaultResourceTypeName(string resourceTypeName) { ResourceTypeName = resourceTypeName; } - /// - /// Specifies a function to use build expressions that allow filtering resources of this type by ID - /// public void OverrideDefaultFilterById(Func filterByIdExpressionFactory) { FilterByIdExpressionFactory = filterByIdExpressionFactory; } - /// - /// Specifies a function to use build expressions that allow sorting resources of this type by ID - /// public void OverrideDefaultSortById(Func sortByIdExpressionFactory) { SortByIdExpressionFactory = sortByIdExpressionFactory; From d10b334238b45cc638bfc72f54d99461ca5244c4 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 15:15:50 -0400 Subject: [PATCH 129/186] fix a couple mis-named things --- ...ipCounselorRelatedResourceMaterializer.cs} | 2 +- ...anceTests.EntityFrameworkTestWebApp.csproj | 2 +- JSONAPI.Autofac/JsonApiAutofacModule.cs | 20 ++++++++++++++++--- ...oOneRelatedResourceDocumentMaterializer.cs | 2 +- ...oOneRelatedResourceDocumentMaterializer.cs | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) rename JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/{StarshipOfficersRelatedResourceMaterializer - Copy.cs => StarshipShipCounselorRelatedResourceMaterializer.cs} (92%) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs similarity index 92% rename from JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs index 5de51bd3..035c4837 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer - Copy.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs @@ -23,7 +23,7 @@ public StarshipShipCounselorRelatedResourceMaterializer( _dbContext = dbContext; } - protected override async Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + protected override async Task GetRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) { var query = _dbContext.Set().Where(s => s.StarshipId == primaryResourceId) .SelectMany(s => s.OfficerLinks) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index d8c3c30a..26e9df66 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -128,7 +128,7 @@ - + diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index fd81bd64..2a5dba8d 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -39,10 +39,24 @@ protected override void Load(ContainerBuilder builder) if (resourceTypeConfiguration.DocumentMaterializerType != null) builder.RegisterType(resourceTypeConfiguration.DocumentMaterializerType); - foreach (var relationshipConfiguration in resourceTypeConfiguration.RelationshipConfigurations) + foreach (var relationship in resourceTypeRegistration.Relationships) { - var relationship = relationshipConfiguration.Value; - builder.RegisterType(relationship.MaterializerType); + IResourceTypeRelationshipConfiguration relationshipConfiguration; + if (resourceTypeConfiguration.RelationshipConfigurations + .TryGetValue(relationship.Property, out relationshipConfiguration)) + { + if (relationshipConfiguration.MaterializerType != null) + { + builder.RegisterType(relationshipConfiguration.MaterializerType); + continue; + } + } + + // They didn't set an explicit materializer. See if they specified a factory for this resource type. + if (configuration.RelatedResourceMaterializerTypeFactory == null) continue; + + var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship); + builder.RegisterType(materializerType); } } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs index 178ccd39..7fcb3814 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs @@ -34,7 +34,7 @@ public EntityFrameworkToOneRelatedResourceDocumentMaterializer( _dbContext = dbContext; } - protected override async Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + protected override async Task GetRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) { var param = Expression.Parameter(typeof(TPrimaryResource)); var accessorExpr = Expression.Property(param, _relationship.Property); diff --git a/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs index e9390bb0..da9b23f4 100644 --- a/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/QueryableToOneRelatedResourceDocumentMaterializer.cs @@ -28,7 +28,7 @@ protected QueryableToOneRelatedResourceDocumentMaterializer( public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, CancellationToken cancellationToken) { - var record = await GeRelatedRecord(primaryResourceId, cancellationToken); + var record = await GetRelatedRecord(primaryResourceId, cancellationToken); var baseUrl = _baseUrlService.GetBaseUrl(request); return _singleResourceDocumentBuilder.BuildDocument(record, baseUrl, null, null); // TODO: allow implementors to specify includes and metadata } @@ -36,6 +36,6 @@ public async Task GetRelatedResourceDocument(string primaryRes /// /// Gets the query for the related resources /// - protected abstract Task GeRelatedRecord(string primaryResourceId, CancellationToken cancellationToken); + protected abstract Task GetRelatedRecord(string primaryResourceId, CancellationToken cancellationToken); } } \ No newline at end of file From d973bb1087d9dec02eb4e9f6627548447549278d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 16:48:07 -0400 Subject: [PATCH 130/186] provide hook for configuring lifetime scope before it is built --- .../Startup.cs | 24 ++++++------- .../JsonApiHttpAutofacConfigurator.cs | 36 ++++++++++++++++--- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index 14d5794d..ae14df36 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -33,17 +33,6 @@ public Startup(Func dbContextFactory) public void Configuration(IAppBuilder app) { - var containerBuilder = new ContainerBuilder(); - containerBuilder.Register(c => _dbContextFactory()) - .AsSelf() - .As() - .InstancePerRequest(); - containerBuilder.RegisterModule(); - containerBuilder.RegisterType() - .As(); - containerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); - var container = containerBuilder.Build(); - var configuration = new JsonApiConfiguration(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); @@ -70,7 +59,18 @@ public void Configuration(IAppBuilder app) }); // Example of a resource that is mapped from a DB entity configuration.RegisterResourceType(); - var configurator = new JsonApiHttpAutofacConfigurator(container); + var configurator = new JsonApiHttpAutofacConfigurator(); + configurator.OnApplicationLifetimeScopeCreating(builder => + { + builder.Register(c => _dbContextFactory()) + .AsSelf() + .As() + .InstancePerRequest(); + builder.RegisterModule(); + builder.RegisterType() + .As(); + builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + }); configurator.OnApplicationLifetimeScopeBegun(applicationLifetimeScope => { // TODO: is this a candidate for spinning into a JSONAPI.Autofac.WebApi.Owin package? Yuck diff --git a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs index 7b9d48a9..2fa79374 100644 --- a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs +++ b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs @@ -9,13 +9,23 @@ namespace JSONAPI.Autofac public class JsonApiHttpAutofacConfigurator { private readonly ILifetimeScope _lifetimeScope; + private Action _appLifetimeScopeCreating; private Action _appLifetimeScopeBegunAction; + public JsonApiHttpAutofacConfigurator() + { + } + public JsonApiHttpAutofacConfigurator(ILifetimeScope lifetimeScope) { _lifetimeScope = lifetimeScope; } + public void OnApplicationLifetimeScopeCreating(Action appLifetimeScopeCreating) + { + _appLifetimeScopeCreating = appLifetimeScopeCreating; + } + public void OnApplicationLifetimeScopeBegun(Action appLifetimeScopeBegunAction) { _appLifetimeScopeBegunAction = appLifetimeScopeBegunAction; @@ -23,11 +33,20 @@ public void OnApplicationLifetimeScopeBegun(Action appLifetimeSc public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jsonApiConfiguration) { - var applicationLifetimeScope = _lifetimeScope.BeginLifetimeScope(containerBuilder => + ILifetimeScope applicationLifetimeScope; + if (_lifetimeScope == null) { - var module = new JsonApiAutofacModule(jsonApiConfiguration); - containerBuilder.RegisterModule(module); - }); + var builder = new ContainerBuilder(); + ConfigureApplicationLifetimeScope(jsonApiConfiguration, builder); + applicationLifetimeScope = builder.Build(); + } + else + { + applicationLifetimeScope = _lifetimeScope.BeginLifetimeScope(containerBuilder => + { + ConfigureApplicationLifetimeScope(jsonApiConfiguration, containerBuilder); + }); + } if (_appLifetimeScopeBegunAction != null) _appLifetimeScopeBegunAction(applicationLifetimeScope); @@ -36,5 +55,14 @@ public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jso jsonApiHttpConfiguration.Apply(httpConfiguration); httpConfiguration.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); } + + private void ConfigureApplicationLifetimeScope(IJsonApiConfiguration jsonApiConfiguration, ContainerBuilder containerBuilder) + { + var module = new JsonApiAutofacModule(jsonApiConfiguration); + containerBuilder.RegisterModule(module); + + if (_appLifetimeScopeCreating != null) + _appLifetimeScopeCreating(containerBuilder); + } } } From 9db8a94d8218772a434e3e6a41820a39513f96cd Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 13 Jul 2015 19:09:08 -0400 Subject: [PATCH 131/186] allow customizing DefaultNamingConventions --- JSONAPI/Core/DefaultNamingConventions.cs | 27 +++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/JSONAPI/Core/DefaultNamingConventions.cs b/JSONAPI/Core/DefaultNamingConventions.cs index a0c0dc2f..b2b4f3f4 100644 --- a/JSONAPI/Core/DefaultNamingConventions.cs +++ b/JSONAPI/Core/DefaultNamingConventions.cs @@ -29,7 +29,7 @@ public DefaultNamingConventions(IPluralizationService pluralizationService) /// /// /// - public string GetFieldNameForProperty(PropertyInfo property) + public virtual string GetFieldNameForProperty(PropertyInfo property) { var jsonPropertyAttribute = (JsonPropertyAttribute)property.GetCustomAttributes(typeof(JsonPropertyAttribute)).FirstOrDefault(); return jsonPropertyAttribute != null ? jsonPropertyAttribute.PropertyName : property.Name.Dasherize(); @@ -43,19 +43,30 @@ public string GetFieldNameForProperty(PropertyInfo property) /// /// /// - public string GetResourceTypeNameForType(Type type) + public virtual string GetResourceTypeNameForType(Type type) { - var attrs = type.CustomAttributes.Where(x => x.AttributeType == typeof(JsonObjectAttribute)).ToList(); + var jsonObjectAttribute = type.GetCustomAttributes().OfType().FirstOrDefault(); - string title = type.Name; - if (attrs.Any()) + string title = null; + if (jsonObjectAttribute != null) { - var titles = attrs.First().NamedArguments.Where(arg => arg.MemberName == "Title") - .Select(arg => arg.TypedValue.Value.ToString()).ToList(); - if (titles.Any()) title = titles.First(); + title = jsonObjectAttribute.Title; + } + + if (string.IsNullOrEmpty(title)) + { + title = GetNameForType(type); } return _pluralizationService.Pluralize(title).Dasherize(); } + + /// + /// Gets the name for a CLR type. + /// + protected virtual string GetNameForType(Type type) + { + return type.Name; + } } } \ No newline at end of file From 6b0ccc1bc66d0ed3ada06dae622243b111852eb9 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 14 Jul 2015 13:41:56 -0400 Subject: [PATCH 132/186] add convenience method for constructing 403 errors --- JSONAPI/Documents/Builders/JsonApiException.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Documents/Builders/JsonApiException.cs b/JSONAPI/Documents/Builders/JsonApiException.cs index d702b848..a8b067a3 100644 --- a/JSONAPI/Documents/Builders/JsonApiException.cs +++ b/JSONAPI/Documents/Builders/JsonApiException.cs @@ -58,7 +58,7 @@ public static JsonApiException Create(string title, string detail, HttpStatusCod /// /// Creates a JsonApiException to send a 404 Not Found error. /// - public static JsonApiException CreateForNotFound(string detail) + public static JsonApiException CreateForNotFound(string detail = null) { var error = new Error { @@ -69,5 +69,20 @@ public static JsonApiException CreateForNotFound(string detail) }; return new JsonApiException(error); } + + /// + /// Creates a JsonApiException to send a 403 Forbidden error. + /// + public static JsonApiException CreateForForbidden(string detail = null) + { + var error = new Error + { + Id = Guid.NewGuid().ToString(), + Status = HttpStatusCode.Forbidden, + Title = "Forbidden", + Detail = detail + }; + return new JsonApiException(error); + } } } From e14592ec254235c88376631c4118ffee57e93d4a Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 15 Jul 2015 09:50:20 -0400 Subject: [PATCH 133/186] have MappedDocumentMaterializer punt on created, update, and delete --- .../StarshipDocumentMaterializer.cs | 19 +++++++++++++++++ JSONAPI/Http/MappedDocumentMaterializer.cs | 21 +++++++------------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs index 1b8d9e1b..1f5ad325 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Linq.Expressions; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; using JSONAPI.Core; using JSONAPI.Documents; @@ -46,5 +48,22 @@ protected override IQueryable GetMappedQuery(IQueryable e StarshipClass = s.StarshipClass.Name }); } + + public override Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task DeleteRecord(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 7fa104a4..84521e11 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -87,22 +87,15 @@ public async Task GetRecordById(string id, HttpRequestM return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); } - public Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public abstract Task CreateRecord(ISingleResourceDocument requestDocument, + HttpRequestMessage request, + CancellationToken cancellationToken); - public Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public abstract Task UpdateRecord(string id, ISingleResourceDocument requestDocument, + HttpRequestMessage request, + CancellationToken cancellationToken); - public Task DeleteRecord(string id, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public abstract Task DeleteRecord(string id, CancellationToken cancellationToken); /// /// Returns a list of property paths to be included when constructing a query for this resource type From e8517488f101e72e67ad40b260e9dde5ec7b288f Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 15 Jul 2015 18:31:17 -0400 Subject: [PATCH 134/186] store relationship configurations by name instead of property It breaks for inheritance --- JSONAPI.Autofac/JsonApiAutofacModule.cs | 4 ++-- JSONAPI/Configuration/IResourceTypeConfiguration.cs | 3 +-- JSONAPI/Configuration/ResourceTypeConfiguration.cs | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 2a5dba8d..69881ce1 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -43,7 +43,7 @@ protected override void Load(ContainerBuilder builder) { IResourceTypeRelationshipConfiguration relationshipConfiguration; if (resourceTypeConfiguration.RelationshipConfigurations - .TryGetValue(relationship.Property, out relationshipConfiguration)) + .TryGetValue(relationship.Property.Name, out relationshipConfiguration)) { if (relationshipConfiguration.MaterializerType != null) { @@ -109,7 +109,7 @@ protected override void Load(ContainerBuilder builder) // First, see if they have set an explicit materializer for this relationship IResourceTypeRelationshipConfiguration relationshipConfiguration; - if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property, + if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property.Name, out relationshipConfiguration) && relationshipConfiguration.MaterializerType != null) return (IRelatedResourceDocumentMaterializer)context.Resolve(relationshipConfiguration.MaterializerType, parameters); diff --git a/JSONAPI/Configuration/IResourceTypeConfiguration.cs b/JSONAPI/Configuration/IResourceTypeConfiguration.cs index 98fb7ef8..9ee2a8ee 100644 --- a/JSONAPI/Configuration/IResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/IResourceTypeConfiguration.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using System.Reflection; using JSONAPI.Core; namespace JSONAPI.Configuration @@ -34,7 +33,7 @@ public interface IResourceTypeConfiguration /// /// Configurations for this type's resources /// - IDictionary RelationshipConfigurations { get; } + IDictionary RelationshipConfigurations { get; } /// /// A factory to use to build expressions to filter a collection of resources of this type by ID. diff --git a/JSONAPI/Configuration/ResourceTypeConfiguration.cs b/JSONAPI/Configuration/ResourceTypeConfiguration.cs index ad08449e..5bc0cc14 100644 --- a/JSONAPI/Configuration/ResourceTypeConfiguration.cs +++ b/JSONAPI/Configuration/ResourceTypeConfiguration.cs @@ -19,7 +19,7 @@ public sealed class ResourceTypeConfiguration : IResourceTypeConf internal ResourceTypeConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) { _resourceTypeRegistrar = resourceTypeRegistrar; - RelationshipConfigurations = new ConcurrentDictionary(); + RelationshipConfigurations = new ConcurrentDictionary(); ClrType = typeof (TResourceType); } @@ -27,7 +27,7 @@ internal ResourceTypeConfiguration(IResourceTypeRegistrar resourceTypeRegistrar) public Type ClrType { get; private set; } public Type DocumentMaterializerType { get; private set; } public Func RelatedResourceMaterializerTypeFactory { get; private set; } - public IDictionary RelationshipConfigurations { get; private set; } + public IDictionary RelationshipConfigurations { get; private set; } public Func FilterByIdExpressionFactory { get; private set; } public Func SortByIdExpressionFactory { get; private set; } @@ -43,7 +43,7 @@ public void ConfigureRelationship(Expression> proper var config = new ResourceTypeRelationshipConfiguration(); configurationAction(config); - RelationshipConfigurations[propertyInfo] = config; + RelationshipConfigurations[propertyInfo.Name] = config; } public void UseDocumentMaterializer() From 6bf83ccfb18ec2f4dbed9cf97db5f0c2c0f8dea1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 16 Jul 2015 12:13:04 -0400 Subject: [PATCH 135/186] make GetKeyNames work recursively --- .../DbContextExtensionsTests.cs | 2 +- .../DbContextExtensions.cs | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs index c55dc8b1..6eeb362d 100644 --- a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs +++ b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs @@ -82,7 +82,7 @@ public void GetKeyNamesNotAnEntityTest() { // Act Action action = () => _context.GetKeyNames(typeof (NotAnEntity)); - action.ShouldThrow().Which.Message.Should().Be("The Type NotAnEntity was not found in the DbContext with Type TestDbContext"); + action.ShouldThrow().Which.Message.Should().Be("Failed to identify the key names for NotAnEntity or any of its parent classes."); } } } diff --git a/JSONAPI.EntityFramework/DbContextExtensions.cs b/JSONAPI.EntityFramework/DbContextExtensions.cs index 76b3604d..37724b77 100644 --- a/JSONAPI.EntityFramework/DbContextExtensions.cs +++ b/JSONAPI.EntityFramework/DbContextExtensions.cs @@ -21,16 +21,28 @@ public static class DbContextExtensions /// public static IEnumerable GetKeyNames(this DbContext dbContext, Type type) { - var openMethod = typeof(DbContextExtensions).GetMethod("GetKeyNamesFromGeneric", BindingFlags.Public | BindingFlags.Static); - var method = openMethod.MakeGenericMethod(type); - try - { - return (IEnumerable)method.Invoke(null, new object[] { dbContext }); - } - catch (TargetInvocationException ex) + if (dbContext == null) throw new ArgumentNullException("dbContext"); + if (type == null) throw new ArgumentNullException("type"); + + var originalType = type; + + while (type != null) { - throw ex.InnerException; + var openMethod = typeof(DbContextExtensions).GetMethod("GetKeyNamesFromGeneric", BindingFlags.Public | BindingFlags.Static); + var method = openMethod.MakeGenericMethod(type); + + try + { + return (IEnumerable) method.Invoke(null, new object[] {dbContext}); + } + catch (TargetInvocationException) + { + } + + type = type.BaseType; } + + throw new Exception(string.Format("Failed to identify the key names for {0} or any of its parent classes.", originalType.Name)); } /// From 015b7ffc0797d94f20577847eeb567a2ecf12d7c Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 17 Jul 2015 13:36:02 -0400 Subject: [PATCH 136/186] add test for subclass --- .../DbContextExtensionsTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs index 6eeb362d..4b8d4ed2 100644 --- a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs +++ b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs @@ -30,6 +30,11 @@ private class NotAnEntity public string Temporary { get; set; } } + private class SubPost : Post + { + public string Foo { get; set; } + } + private DbConnection _conn; private TestDbContext _context; @@ -77,6 +82,17 @@ public void GetKeyNamesNonStandardIdTest() keyNames.First().Should().Be("Url"); } + [TestMethod] + public void GetKeyNamesForChildClass() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(SubPost)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("Id"); + } + [TestMethod] public void GetKeyNamesNotAnEntityTest() { From 9e185dfb1b656ee3bec64ae0b6d8882c4ff3fa3d Mon Sep 17 00:00:00 2001 From: = Date: Sat, 18 Jul 2015 01:37:15 +0200 Subject: [PATCH 137/186] Adding globalization for decimal, double and single for conversion. --- .../DefaultFilteringTransformerTests.cs | 42 +++++++++++++++++++ .../Core/ResourceTypeRegistrarTests.cs | 20 +++++++++ .../Core/DecimalAttributeValueConverter.cs | 12 +++++- .../DefaultFilteringTransformer.cs | 13 +++--- 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index e1677758..bf59c926 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Threading; using System.Web.Http; using FluentAssertions; using JSONAPI.ActionFilters; @@ -689,6 +691,19 @@ public void Filters_by_matching_decimal_property() returnedArray[0].Id.Should().Be("170"); } + [TestMethod] + public void Filters_by_matching_decimal_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]=4.03"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("170"); + + Thread.CurrentThread.CurrentCulture = currentCulture; + } + [TestMethod] public void Filters_by_missing_decimal_property() { @@ -1039,6 +1054,20 @@ public void Filters_by_matching_single_property() returnedArray[0].Id.Should().Be("370"); } + [TestMethod] + public void Filters_by_matching_single_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]=21.56901"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("370"); + + Thread.CurrentThread.CurrentCulture = currentCulture; + } + + [TestMethod] public void Filters_by_missing_single_property() { @@ -1074,6 +1103,19 @@ public void Filters_by_matching_double_property() returnedArray[0].Id.Should().Be("390"); } + [TestMethod] + public void Filters_by_matching_double_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]=12.3453489012"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("390"); + + Thread.CurrentThread.CurrentCulture = currentCulture; + } + [TestMethod] public void Filters_by_missing_double_property() { diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs index c63c44d3..3c461c96 100644 --- a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs +++ b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs @@ -7,7 +7,9 @@ using System.Collections.Generic; using System.Collections; using System.Diagnostics; +using System.Globalization; using System.Linq; +using System.Threading; using FluentAssertions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -561,6 +563,24 @@ public void BuildRegistration_sets_up_correct_attribute_for_Decimal_field() AssertAttribute(reg, "decimal-field", null, 0m, "0", g => g.DecimalField); } + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Decimal_field_non_en_US() + { + // Set up non US culture + var culture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); + + Thread.CurrentThread.CurrentCulture = culture; + } + [TestMethod] public void BuildRegistration_sets_up_correct_attribute_for_nullable_Decimal_field() { diff --git a/JSONAPI/Core/DecimalAttributeValueConverter.cs b/JSONAPI/Core/DecimalAttributeValueConverter.cs index 532c9e29..064572bc 100644 --- a/JSONAPI/Core/DecimalAttributeValueConverter.cs +++ b/JSONAPI/Core/DecimalAttributeValueConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -26,7 +27,14 @@ public JToken GetValue(object resource) { var value = _property.GetValue(resource); if (value == null) return null; - return value.ToString(); + try + { + return ((Decimal)value).ToString(CultureInfo.InvariantCulture); + } + catch (InvalidCastException e) + { + throw new JsonSerializationException("Could not serialize decimal value.", e); + } } public void SetValue(object resource, JToken value) @@ -37,7 +45,7 @@ public void SetValue(object resource, JToken value) { var stringTokenValue = value.Value(); Decimal d; - if (!Decimal.TryParse(stringTokenValue, out d)) + if (!Decimal.TryParse(stringTokenValue, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) throw new JsonSerializationException("Could not parse decimal value."); _property.SetValue(resource, d); } diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index 0c8ac0fa..baf37b80 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Net; @@ -248,40 +249,40 @@ private Expression GetPredicateBodyForField(ResourceTypeAttribute resourceTypeAt else if (propertyType == typeof(Single)) { Single value; - expr = Single.TryParse(queryValue, out value) + expr = Single.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) ? GetPropertyExpression(value, prop, param) : Expression.Constant(false); } else if (propertyType == typeof(Single?)) { Single tmp; - var value = Single.TryParse(queryValue, out tmp) ? tmp : (Single?)null; + var value = Single.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Single?)null; expr = GetPropertyExpression(value, prop, param); } else if (propertyType == typeof(Double)) { Double value; - expr = Double.TryParse(queryValue, out value) + expr = Double.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) ? GetPropertyExpression(value, prop, param) : Expression.Constant(false); } else if (propertyType == typeof(Double?)) { Double tmp; - var value = Double.TryParse(queryValue, out tmp) ? tmp : (Double?)null; + var value = Double.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Double?)null; expr = GetPropertyExpression(value, prop, param); } else if (propertyType == typeof(Decimal)) { Decimal value; - expr = Decimal.TryParse(queryValue, out value) + expr = Decimal.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) ? GetPropertyExpression(value, prop, param) : Expression.Constant(false); } else if (propertyType == typeof(Decimal?)) { Decimal tmp; - var value = Decimal.TryParse(queryValue, out tmp) ? tmp : (Decimal?)null; + var value = Decimal.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Decimal?)null; expr = GetPropertyExpression(value, prop, param); } else if (propertyType == typeof(DateTime)) From 74fd025015731108f7ea878765e2dcc4a8f14b3e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 17 Jul 2015 19:58:46 -0400 Subject: [PATCH 138/186] convert linkage objects to using IResourceIdentifiers This is a much more ergonomic solution than passing a JToken around. --- ...PatchWithArrayForToOneLinkageResponse.json | 2 +- ...PatchWithNullForToManyLinkageResponse.json | 4 +- ...tchWithObjectForToManyLinkageResponse.json | 2 +- ...tityFrameworkResourceObjectMaterializer.cs | 45 ++++--------- ...rivenSingleResourceDocumentBuilderTests.cs | 19 +++--- .../Documents/ToManyResourceLinkageTests.cs | 27 +++----- .../Documents/ToOneResourceLinkageTests.cs | 20 ++---- JSONAPI.Tests/JSONAPI.Tests.csproj | 6 +- .../Serialize_empty_toMany_linkage.json | 1 + ...json => Serialize_null_toOne_linkage.json} | 0 .../Serialize_present_toMany_linkage.json | 10 +++ ...n => Serialize_present_toOne_linkage.json} | 2 +- .../Json/ResourceLinkageFormatterTests.cs | 66 ++++++++++++------- JSONAPI/Documents/IResourceLinkage.cs | 15 +++-- JSONAPI/Documents/ToManyResourceLinkage.cs | 19 ++---- JSONAPI/Documents/ToOneResourceLinkage.cs | 17 ++--- JSONAPI/Json/ResourceLinkageFormatter.cs | 32 ++++++++- 17 files changed, 147 insertions(+), 140 deletions(-) create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_empty_toMany_linkage.json rename JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/{Serialize_null_linkage.json => Serialize_null_toOne_linkage.json} (100%) create mode 100644 JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toMany_linkage.json rename JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/{Serialize_ToOneResourceLinkage.json => Serialize_present_toOne_linkage.json} (64%) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json index 30614ce8..d7a626e3 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "400", "title": "Invalid linkage for to-one relationship", - "detail": "Expected an object for to-one linkage, but got Array", + "detail": "Expected an object or null for to-one linkage", "source": { "pointer": "/data/relationships/author/data" } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json index 441ea0b1..fc7f2409 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json @@ -3,8 +3,8 @@ { "id": "{{SOME_GUID}}", "status": "400", - "title": "Null linkage for to-many relationship", - "detail": "Expected an array for to-many linkage, but got Null.", + "title": "Invalid linkage for to-many relationship", + "detail": "Expected an array for to-many linkage.", "source": { "pointer": "/data/relationships/tags/data" } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json index f1879bd7..fc7f2409 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json @@ -4,7 +4,7 @@ "id": "{{SOME_GUID}}", "status": "400", "title": "Invalid linkage for to-many relationship", - "detail": "Expected an array for to-many linkage, but got Object", + "detail": "Expected an array for to-many linkage.", "source": { "pointer": "/data/relationships/tags/data" } diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index ebfe05aa..57c2b29b 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -9,7 +9,6 @@ using JSONAPI.Core; using JSONAPI.Documents; using JSONAPI.Json; -using Newtonsoft.Json.Linq; namespace JSONAPI.EntityFramework { @@ -119,29 +118,17 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO throw new DeserializationException("Missing linkage for to-many relationship", "Expected an array for to-many linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); - if (linkage.LinkageToken == null) - throw new DeserializationException("Null linkage for to-many relationship", - "Expected an array for to-many linkage, but got Null.", - "/data/relationships/" + relationshipValue.Key + "/data"); - - var linkageTokenType = linkage.LinkageToken.Type; - if (linkageTokenType != JTokenType.Array) + if (!linkage.IsToMany) throw new DeserializationException("Invalid linkage for to-many relationship", - "Expected an array for to-many linkage, but got " + linkage.LinkageToken.Type, + "Expected an array for to-many linkage.", "/data/relationships/" + relationshipValue.Key + "/data"); - var linkageArray = (JArray) linkage.LinkageToken; - // TODO: One query per related object is going to be slow. At the very least, we should be able to group the queries by type var newCollection = new List(); - foreach (var resourceIdentifier in linkageArray) + foreach (var resourceIdentifier in linkage.Identifiers) { - var resourceIdentifierObject = (JObject) resourceIdentifier; - var relatedType = resourceIdentifierObject["type"].Value(); - var relatedId = resourceIdentifierObject["id"].Value(); - - var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); - var relatedObject = await GetExistingRecord(relatedObjectRegistration, relatedId, null, cancellationToken); + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(resourceIdentifier.Type); + var relatedObject = await GetExistingRecord(relatedObjectRegistration, resourceIdentifier.Id, null, cancellationToken); newCollection.Add(relatedObject); } @@ -154,25 +141,21 @@ protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceO throw new DeserializationException("Missing linkage for to-one relationship", "Expected an object for to-one linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); - if (linkage.LinkageToken == null) + if (linkage.IsToMany) + throw new DeserializationException("Invalid linkage for to-one relationship", + "Expected an object or null for to-one linkage", + "/data/relationships/" + relationshipValue.Key + "/data"); + + var identifier = linkage.Identifiers.FirstOrDefault(); + if (identifier == null) { typeRelationship.Property.SetValue(material, null); } else { - var linkageTokenType = linkage.LinkageToken.Type; - if (linkageTokenType != JTokenType.Object) - throw new DeserializationException("Invalid linkage for to-one relationship", - "Expected an object for to-one linkage, but got " + linkage.LinkageToken.Type, - "/data/relationships/" + relationshipValue.Key + "/data"); - - var linkageObject = (JObject) linkage.LinkageToken; - var relatedType = linkageObject["type"].Value(); - var relatedId = linkageObject["id"].Value(); - - var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(relatedType); + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(identifier.Type); var relatedObject = - await GetExistingRecord(relatedObjectRegistration, relatedId, null, cancellationToken); + await GetExistingRecord(relatedObjectRegistration, identifier.Id, null, cancellationToken); typeRelationship.Property.SetValue(material, relatedObject); } diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index ebab7a73..70745340 100644 --- a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -194,13 +194,11 @@ public void Returns_correct_document_for_resource() provincesRelationship.Key.Should().Be("provinces"); provincesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/countries/4/relationships/provinces"); provincesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/countries/4/provinces"); - provincesRelationship.Value.Linkage.Should().BeOfType(); - provincesRelationship.Value.Linkage.LinkageToken.Should().BeOfType(); - var provincesArray = (JArray) provincesRelationship.Value.Linkage.LinkageToken; - ((string)provincesArray[0]["type"]).Should().Be("provinces"); - ((string)provincesArray[0]["id"]).Should().Be("506"); - ((string)provincesArray[1]["type"]).Should().Be("provinces"); - ((string)provincesArray[1]["id"]).Should().Be("507"); + provincesRelationship.Value.Linkage.IsToMany.Should().BeTrue(); + provincesRelationship.Value.Linkage.Identifiers[0].Type.Should().Be("provinces"); + provincesRelationship.Value.Linkage.Identifiers[0].Id.Should().Be("506"); + provincesRelationship.Value.Linkage.Identifiers[1].Type.Should().Be("provinces"); + provincesRelationship.Value.Linkage.Identifiers[1].Id.Should().Be("507"); var continentRelationship = document.PrimaryData.Relationships.Skip(2).First(); AssertToOneRelationship(continentRelationship, "continent", @@ -272,9 +270,10 @@ private void AssertToOneRelationship(KeyValuePair r relationshipPair.Key.Should().Be(keyName); relationshipPair.Value.SelfLink.Href.Should().Be(selfLink); relationshipPair.Value.RelatedResourceLink.Href.Should().Be(relatedResourceLink); - relationshipPair.Value.Linkage.Should().BeOfType(); - ((string)relationshipPair.Value.Linkage.LinkageToken["type"]).Should().Be(linkageType); - ((string)relationshipPair.Value.Linkage.LinkageToken["id"]).Should().Be(linkageId); + relationshipPair.Value.Linkage.IsToMany.Should().BeFalse(); + relationshipPair.Value.Linkage.Identifiers.Length.Should().Be(1); + relationshipPair.Value.Linkage.Identifiers[0].Type.Should().Be(linkageType); + relationshipPair.Value.Linkage.Identifiers[0].Id.Should().Be(linkageId); } private void AssertEmptyToOneRelationship(KeyValuePair relationshipPair, string keyName, string selfLink, string relatedResourceLink) diff --git a/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs index 729ef38c..80196eba 100644 --- a/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs +++ b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs @@ -1,9 +1,7 @@ -using System.Linq; using FluentAssertions; using JSONAPI.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Documents { @@ -11,7 +9,7 @@ namespace JSONAPI.Tests.Documents public class ToManyResourceLinkageTests { [TestMethod] - public void Returns_corrent_LinkageToken_for_present_identifiers() + public void Identifiers_is_correct_for_present_identifiers() { var mockIdentifier1 = new Mock(MockBehavior.Strict); mockIdentifier1.Setup(i => i.Type).Returns("countries"); @@ -23,18 +21,11 @@ public void Returns_corrent_LinkageToken_for_present_identifiers() var linkage = new ToManyResourceLinkage(new [] { mockIdentifier1.Object, mockIdentifier2.Object }); - linkage.LinkageToken.Should().BeOfType(); - - var arr = (JArray)linkage.LinkageToken; - arr.Count.Should().Be(2); - - var item1 = arr[0]; - ((string)item1["type"]).Should().Be("countries"); - ((string)item1["id"]).Should().Be("1000"); - - var item2 = arr[1]; - ((string)item2["type"]).Should().Be("cities"); - ((string)item2["id"]).Should().Be("4000"); + linkage.Identifiers.Length.Should().Be(2); + linkage.Identifiers[0].Type.Should().Be("countries"); + linkage.Identifiers[0].Id.Should().Be("1000"); + linkage.Identifiers[1].Type.Should().Be("cities"); + linkage.Identifiers[1].Id.Should().Be("4000"); } [TestMethod] @@ -42,8 +33,7 @@ public void Returns_corrent_LinkageToken_for_null_identifiers() { var linkage = new ToManyResourceLinkage(null); - linkage.LinkageToken.Should().BeOfType(); - linkage.LinkageToken.Count().Should().Be(0); + linkage.Identifiers.Length.Should().Be(0); } [TestMethod] @@ -51,8 +41,7 @@ public void Returns_corrent_LinkageToken_for_empty_identifiers() { var linkage = new ToManyResourceLinkage(new IResourceIdentifier[] { }); - linkage.LinkageToken.Should().BeOfType(); - linkage.LinkageToken.Count().Should().Be(0); + linkage.Identifiers.Length.Should().Be(0); } } } \ No newline at end of file diff --git a/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs index 8dda5337..7c524612 100644 --- a/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs +++ b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs @@ -3,7 +3,6 @@ using JSONAPI.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Documents { @@ -11,7 +10,7 @@ namespace JSONAPI.Tests.Documents public class ToOneResourceLinkageTests { [TestMethod] - public void Returns_LinkageToken_for_present_identifier() + public void Identifiers_is_correct_for_present_identifier() { var mockIdentifier = new Mock(MockBehavior.Strict); mockIdentifier.Setup(i => i.Type).Returns("countries"); @@ -19,24 +18,17 @@ public void Returns_LinkageToken_for_present_identifier() var linkage = new ToOneResourceLinkage(mockIdentifier.Object); - linkage.LinkageToken.Should().BeOfType(); - - var obj = (JObject)linkage.LinkageToken; - obj.Properties().Count().Should().Be(2); - - var type = (string)obj["type"]; - type.Should().Be("countries"); - - var id = (string)obj["id"]; - id.Should().Be("1000"); + linkage.Identifiers.Length.Should().Be(1); + linkage.Identifiers.First().Type.Should().Be("countries"); + linkage.Identifiers.First().Id.Should().Be("1000"); } [TestMethod] - public void Returns_null_LinkageToken_for_missing_identifier() + public void Identifiers_is_correct_for_missing_identifier() { var linkage = new ToOneResourceLinkage(null); - linkage.LinkageToken.Should().BeNull(); + linkage.Identifiers.Length.Should().Be(0); } } } \ No newline at end of file diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 20e8ae3a..53498a51 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -149,6 +149,8 @@ + + @@ -168,9 +170,9 @@ - + - + diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_empty_toMany_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_empty_toMany_linkage.json new file mode 100644 index 00000000..ad47dbb9 --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_empty_toMany_linkage.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_toOne_linkage.json similarity index 100% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_null_toOne_linkage.json diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toMany_linkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toMany_linkage.json new file mode 100644 index 00000000..4872d17c --- /dev/null +++ b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toMany_linkage.json @@ -0,0 +1,10 @@ +[ + { + "type": "countries", + "id": "11000" + }, + { + "type": "cities", + "id": "4100" + } +] diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_ToOneResourceLinkage.json b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toOne_linkage.json similarity index 64% rename from JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_ToOneResourceLinkage.json rename to JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toOne_linkage.json index 035f8f63..fda81544 100644 --- a/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_ToOneResourceLinkage.json +++ b/JSONAPI.Tests/Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toOne_linkage.json @@ -1,4 +1,4 @@ { "type": "countries", - "id": "1000" + "id": "11000" } diff --git a/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs b/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs index 22b5cfcf..35226c4f 100644 --- a/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs +++ b/JSONAPI.Tests/Json/ResourceLinkageFormatterTests.cs @@ -1,12 +1,10 @@ using System; -using System.Linq; using System.Threading.Tasks; using FluentAssertions; using JSONAPI.Documents; using JSONAPI.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Json { @@ -14,23 +12,48 @@ namespace JSONAPI.Tests.Json public class ResourceLinkageFormatterTests : JsonApiFormatterTestsBase { [TestMethod] - public async Task Serialize_linkage() + public async Task Serialize_present_toMany_linkage() { var linkageObject = new Mock(MockBehavior.Strict); - linkageObject.Setup(l => l.LinkageToken).Returns("linkage goes here"); + linkageObject.Setup(l => l.IsToMany).Returns(true); + linkageObject.Setup(l => l.Identifiers) + .Returns(new IResourceIdentifier[] { new ResourceIdentifier("countries", "11000"), new ResourceIdentifier("cities", "4100") }); var formatter = new ResourceLinkageFormatter(); - await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_linkage.json"); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toMany_linkage.json"); } [TestMethod] - public async Task Serialize_null_linkage() + public async Task Serialize_empty_toMany_linkage() { var linkageObject = new Mock(MockBehavior.Strict); - linkageObject.Setup(l => l.LinkageToken).Returns((JToken)null); + linkageObject.Setup(l => l.IsToMany).Returns(true); + linkageObject.Setup(l => l.Identifiers).Returns(new IResourceIdentifier[] { }); var formatter = new ResourceLinkageFormatter(); - await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_null_linkage.json"); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_empty_toMany_linkage.json"); + } + + [TestMethod] + public async Task Serialize_present_toOne_linkage() + { + var linkageObject = new Mock(MockBehavior.Strict); + linkageObject.Setup(l => l.IsToMany).Returns(false); + linkageObject.Setup(l => l.Identifiers).Returns(new IResourceIdentifier[] { new ResourceIdentifier("countries", "11000") }); + + var formatter = new ResourceLinkageFormatter(); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_present_toOne_linkage.json"); + } + + [TestMethod] + public async Task Serialize_null_toOne_linkage() + { + var linkageObject = new Mock(MockBehavior.Strict); + linkageObject.Setup(l => l.IsToMany).Returns(false); + linkageObject.Setup(l => l.Identifiers).Returns(new IResourceIdentifier[] { }); + + var formatter = new ResourceLinkageFormatter(); + await AssertSerializeOutput(formatter, linkageObject.Object, "Json/Fixtures/ResourceLinkageFormatter/Serialize_null_toOne_linkage.json"); } [TestMethod] @@ -45,10 +68,10 @@ public void Deserialize_to_one_linkage() "Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_one_linkage.json").Result; // Assert - var linkageToken = (JObject)linkage.LinkageToken; - linkageToken.Properties().Count().Should().Be(2); - ((string)linkageToken["type"]).Should().Be("people"); - ((string)linkageToken["id"]).Should().Be("abc123"); + linkage.IsToMany.Should().BeFalse(); + linkage.Identifiers.Length.Should().Be(1); + linkage.Identifiers[0].Type.Should().Be("people"); + linkage.Identifiers[0].Id.Should().Be("abc123"); } [TestMethod] @@ -63,7 +86,8 @@ public void Deserialize_null_to_one_linkage() "Json/Fixtures/ResourceLinkageFormatter/Deserialize_null_to_one_linkage.json").Result; // Assert - linkage.LinkageToken.Should().BeNull(); + linkage.IsToMany.Should().BeFalse(); + linkage.Identifiers.Length.Should().Be(0); } [TestMethod] @@ -78,17 +102,11 @@ public void Deserialize_to_many_linkage() "Json/Fixtures/ResourceLinkageFormatter/Deserialize_to_many_linkage.json").Result; // Assert - var linkageToken = (JArray)linkage.LinkageToken; - - var item1 = (JObject) linkageToken[0]; - item1.Properties().Count().Should().Be(2); - ((string)item1["type"]).Should().Be("posts"); - ((string)item1["id"]).Should().Be("12"); - - var item2 = (JObject)linkageToken[1]; - item2.Properties().Count().Should().Be(2); - ((string)item2["type"]).Should().Be("comments"); - ((string)item2["id"]).Should().Be("9510"); + linkage.IsToMany.Should().BeTrue(); + linkage.Identifiers[0].Type.Should().Be("posts"); + linkage.Identifiers[0].Id.Should().Be("12"); + linkage.Identifiers[1].Type.Should().Be("comments"); + linkage.Identifiers[1].Id.Should().Be("9510"); } [TestMethod] diff --git a/JSONAPI/Documents/IResourceLinkage.cs b/JSONAPI/Documents/IResourceLinkage.cs index a5cfe06e..ef3ca445 100644 --- a/JSONAPI/Documents/IResourceLinkage.cs +++ b/JSONAPI/Documents/IResourceLinkage.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json.Linq; - -namespace JSONAPI.Documents +namespace JSONAPI.Documents { /// /// Describes a relationship's linkage @@ -8,8 +6,15 @@ namespace JSONAPI.Documents public interface IResourceLinkage { /// - /// The item determining the linkage + /// Whether the linkage is to-many (true) or to-one (false). + /// + bool IsToMany { get; } + + /// + /// The identifiers this linkage refers to. If IsToMany is true, this + /// property must return an array of length either 0 or 1. If false, + /// the array may be of any length. This property must not return null. /// - JToken LinkageToken { get; } + IResourceIdentifier[] Identifiers { get; } } } \ No newline at end of file diff --git a/JSONAPI/Documents/ToManyResourceLinkage.cs b/JSONAPI/Documents/ToManyResourceLinkage.cs index fa696e98..a9c576b4 100644 --- a/JSONAPI/Documents/ToManyResourceLinkage.cs +++ b/JSONAPI/Documents/ToManyResourceLinkage.cs @@ -1,5 +1,4 @@ using System; -using Newtonsoft.Json.Linq; namespace JSONAPI.Documents { @@ -8,8 +7,6 @@ namespace JSONAPI.Documents /// public class ToManyResourceLinkage : IResourceLinkage { - public JToken LinkageToken { get; private set; } - /// /// Creates a To-many resource linkage object /// @@ -17,18 +14,10 @@ public class ToManyResourceLinkage : IResourceLinkage /// public ToManyResourceLinkage(IResourceIdentifier[] resourceIdentifiers) { - var array = new JArray(); - if (resourceIdentifiers != null) - { - foreach (var resourceIdentifier in resourceIdentifiers) - { - var obj = new JObject(); - obj["type"] = resourceIdentifier.Type; - obj["id"] = resourceIdentifier.Id; - array.Add(obj); - } - } - LinkageToken = array; + Identifiers = resourceIdentifiers ?? new IResourceIdentifier[] {}; } + + public bool IsToMany { get { return true; } } + public IResourceIdentifier[] Identifiers { get; private set; } } } \ No newline at end of file diff --git a/JSONAPI/Documents/ToOneResourceLinkage.cs b/JSONAPI/Documents/ToOneResourceLinkage.cs index 27f5c7a9..d9fd509d 100644 --- a/JSONAPI/Documents/ToOneResourceLinkage.cs +++ b/JSONAPI/Documents/ToOneResourceLinkage.cs @@ -1,27 +1,20 @@ -using Newtonsoft.Json.Linq; - -namespace JSONAPI.Documents +namespace JSONAPI.Documents { /// /// Describes linkage to a single resource /// public class ToOneResourceLinkage : IResourceLinkage { - public JToken LinkageToken { get; private set; } - /// /// Creates a to-one resource linkage object /// /// public ToOneResourceLinkage(IResourceIdentifier resourceIdentifier) { - if (resourceIdentifier != null) - { - LinkageToken = new JObject(); - - LinkageToken["type"] = resourceIdentifier.Type; - LinkageToken["id"] = resourceIdentifier.Id; - } + Identifiers = resourceIdentifier != null ? new[] {resourceIdentifier} : new IResourceIdentifier[] {}; } + + public bool IsToMany { get { return false; } } + public IResourceIdentifier[] Identifiers { get; private set; } } } \ No newline at end of file diff --git a/JSONAPI/Json/ResourceLinkageFormatter.cs b/JSONAPI/Json/ResourceLinkageFormatter.cs index 07c4e9f4..538b5255 100644 --- a/JSONAPI/Json/ResourceLinkageFormatter.cs +++ b/JSONAPI/Json/ResourceLinkageFormatter.cs @@ -13,13 +13,39 @@ public class ResourceLinkageFormatter : IResourceLinkageFormatter { public Task Serialize(IResourceLinkage linkage, JsonWriter writer) { - if (linkage.LinkageToken == null) - writer.WriteNull(); + if (linkage.IsToMany) + { + writer.WriteStartArray(); + foreach (var resourceIdentifier in linkage.Identifiers) + { + WriteResourceIdentifier(resourceIdentifier, writer); + } + writer.WriteEndArray(); + } else - linkage.LinkageToken.WriteTo(writer); + { + var initialIdentifier = linkage.Identifiers.FirstOrDefault(); + if (initialIdentifier == null) + writer.WriteNull(); + else + { + WriteResourceIdentifier(initialIdentifier, writer); + } + + } return Task.FromResult(0); } + private void WriteResourceIdentifier(IResourceIdentifier resourceIdentifier, JsonWriter writer) + { + writer.WriteStartObject(); + writer.WritePropertyName("type"); + writer.WriteValue(resourceIdentifier.Type); + writer.WritePropertyName("id"); + writer.WriteValue(resourceIdentifier.Id); + writer.WriteEndObject(); + } + public Task Deserialize(JsonReader reader, string currentPath) { IResourceLinkage linkage; From fbc430a7a64f0a6f687c194ee936c8019c622747 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 19 Jul 2015 14:55:52 -0400 Subject: [PATCH 139/186] make sure null to-one included relationships are serialized with null linkage Prior to this change, they were just being ignored. --- .../RegistryDrivenSingleResourceDocumentBuilderTests.cs | 3 ++- JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index 70745340..ba130e34 100644 --- a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -281,7 +281,8 @@ private void AssertEmptyToOneRelationship(KeyValuePair Date: Tue, 21 Jul 2015 19:38:34 -0400 Subject: [PATCH 140/186] await tasks in JsonApiFormatter --- JSONAPI/Json/JsonApiFormatter.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 67d7c1f2..4d1ddfa6 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -50,10 +50,10 @@ public override bool CanWriteType(Type t) return true; } - public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) + public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { if (type == typeof(IJsonApiDocument) && value == null) - return Task.FromResult(0); + return; var contentHeaders = content == null ? null : content.Headers; var effectiveEncoding = SelectCharacterEncoding(contentHeaders); @@ -64,15 +64,15 @@ public override Task WriteToStreamAsync(Type type, object value, Stream writeStr var errorDocument = value as IErrorDocument; if (singleResourceDocument != null) { - _singleResourceDocumentFormatter.Serialize(singleResourceDocument, writer); + await _singleResourceDocumentFormatter.Serialize(singleResourceDocument, writer); } else if (resourceCollectionDocument != null) { - _resourceCollectionDocumentFormatter.Serialize(resourceCollectionDocument, writer); + await _resourceCollectionDocumentFormatter.Serialize(resourceCollectionDocument, writer); } else if (errorDocument != null) { - _errorDocumentFormatter.Serialize(errorDocument, writer); + await _errorDocumentFormatter.Serialize(errorDocument, writer); } else { @@ -80,7 +80,7 @@ public override Task WriteToStreamAsync(Type type, object value, Stream writeStr if (error != null) { var httpErrorDocument = _errorDocumentBuilder.BuildFromHttpError(error, HttpStatusCode.InternalServerError); - _errorDocumentFormatter.Serialize(httpErrorDocument, writer); + await _errorDocumentFormatter.Serialize(httpErrorDocument, writer); } else { @@ -89,8 +89,6 @@ public override Task WriteToStreamAsync(Type type, object value, Stream writeStr } writer.Flush(); - - return Task.FromResult(0); } public override async Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) From 8321989a7f3e682fe32e4f2a63d576f4fb196710 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 22 Jul 2015 14:05:24 -0400 Subject: [PATCH 141/186] allow specifying include paths --- ...yableToManyRelatedResourceDocumentMaterializer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs index 08fd04ed..5bd863e6 100644 --- a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs @@ -27,12 +27,22 @@ public async Task GetRelatedResourceDocument(string primaryRes CancellationToken cancellationToken) { var query = await GetRelatedQuery(primaryResourceId, cancellationToken); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); // TODO: allow implementors to specify includes and metadata + var includes = GetIncludePaths(); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken, includes); // TODO: allow implementors to specify metadata } /// /// Gets the query for the related resources /// protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken); + + /// + /// Gets a list of relationship paths to include + /// + /// + protected virtual string[] GetIncludePaths() + { + return null; + } } } \ No newline at end of file From 05cb9635f78af9e620f08c7e8a7aafa0ec86c47a Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 27 Jul 2015 18:14:36 -0400 Subject: [PATCH 142/186] expose PathVisitor --- .vs/config/applicationhost.config | 1046 ++++++++++++++++++++ JSONAPI/Core/PathVisitor.cs | 53 + JSONAPI/Http/MappedDocumentMaterializer.cs | 39 - JSONAPI/JSONAPI.csproj | 1 + 4 files changed, 1100 insertions(+), 39 deletions(-) create mode 100644 .vs/config/applicationhost.config create mode 100644 JSONAPI/Core/PathVisitor.cs diff --git a/.vs/config/applicationhost.config b/.vs/config/applicationhost.config new file mode 100644 index 00000000..bc28afba --- /dev/null +++ b/.vs/config/applicationhost.config @@ -0,0 +1,1046 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JSONAPI/Core/PathVisitor.cs b/JSONAPI/Core/PathVisitor.cs new file mode 100644 index 00000000..74566089 --- /dev/null +++ b/JSONAPI/Core/PathVisitor.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace JSONAPI.Core +{ + /// + /// Utility for converting an property expression into a dot-separated path string + /// + public class PathVisitor : ExpressionVisitor + { + private readonly IResourceTypeRegistry _resourceTypeRegistry; + + /// + /// Creates a new PathVisitor + /// + /// + public PathVisitor(IResourceTypeRegistry resourceTypeRegistry) + { + _resourceTypeRegistry = resourceTypeRegistry; + } + + private readonly Stack _segments = new Stack(); + public string Path { get { return string.Join(".", _segments.ToArray()); } } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "Select") + { + Visit(node.Arguments[1]); + Visit(node.Arguments[0]); + } + return node; + } + + protected override Expression VisitMember(MemberExpression node) + { + var property = node.Member as PropertyInfo; + if (property == null) return node; + + var registration = _resourceTypeRegistry.GetRegistrationForType(property.DeclaringType); + if (registration == null || registration.Relationships == null) return node; + + var relationship = registration.Relationships.FirstOrDefault(r => r.Property == property); + if (relationship == null) return node; + + _segments.Push(relationship.JsonKey); + + return base.VisitMember(node); + } + } +} diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 84521e11..d34f0c3f 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -119,44 +119,5 @@ private string ConvertToJsonKeyPath(Expression> expression) visitor.Visit(expression); return visitor.Path; } - - private class PathVisitor : ExpressionVisitor - { - private readonly IResourceTypeRegistry _resourceTypeRegistry; - - public PathVisitor(IResourceTypeRegistry resourceTypeRegistry) - { - _resourceTypeRegistry = resourceTypeRegistry; - } - - private readonly Stack _segments = new Stack(); - public string Path { get { return string.Join(".", _segments.ToArray()); } } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - if (node.Method.Name == "Select") - { - Visit(node.Arguments[1]); - Visit(node.Arguments[0]); - } - return node; - } - - protected override Expression VisitMember(MemberExpression node) - { - var property = node.Member as PropertyInfo; - if (property == null) return node; - - var registration = _resourceTypeRegistry.GetRegistrationForType(property.DeclaringType); - if (registration == null || registration.Relationships == null) return node; - - var relationship = registration.Relationships.FirstOrDefault(r => r.Property == property); - if (relationship == null) return node; - - _segments.Push(relationship.JsonKey); - - return base.VisitMember(node); - } - } } } diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 6e3552f3..7398c9c5 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -77,6 +77,7 @@ + From 2d442ce57da746abecfde5e034e8cb7b1173897c Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 29 Jul 2015 23:45:19 -0400 Subject: [PATCH 143/186] make GET methods virtual --- JSONAPI/Http/MappedDocumentMaterializer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index d34f0c3f..b73934c1 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -63,7 +63,7 @@ private string ResourceTypeName get { return _resourceTypeRegistry.GetRegistrationForType(typeof (TDto)).ResourceTypeName; } } - public async Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) + public virtual async Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { var entityQuery = GetQuery(); var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; @@ -72,7 +72,7 @@ public async Task GetRecords(HttpRequestMessage req return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths); } - public async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) + public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var entityQuery = GetByIdQuery(id); var includePaths = GetIncludePathsForSingleResource() ?? new Expression>[] { }; From adea1cba5913fedf52ec3e1f13599688bcf5fedd Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 2 Aug 2015 17:01:51 -0400 Subject: [PATCH 144/186] send correct query to GetDocumentMetadata --- .../DefaultQueryableResourceCollectionDocumentBuilder.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index c0e27b34..79061d06 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -1,11 +1,8 @@ -using System; -using System.Linq; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using JSONAPI.ActionFilters; using JSONAPI.Http; -using JSONAPI.Json; using JSONAPI.QueryableTransformers; namespace JSONAPI.Documents.Builders @@ -48,11 +45,11 @@ public async Task BuildDocument(IQueryable qu var sortedQuery = _sortingTransformer.Sort(filteredQuery, request); var paginationResults = _paginationTransformer.ApplyPagination(sortedQuery, request); - query = paginationResults.PagedQuery; + var paginatedQuery = paginationResults.PagedQuery; var linkBaseUrl = _baseUrlService.GetBaseUrl(request); - var results = await _enumerationTransformer.Enumerate(query, cancellationToken); + var results = await _enumerationTransformer.Enumerate(paginatedQuery, cancellationToken); var metadata = await GetDocumentMetadata(query, filteredQuery, sortedQuery, paginationResults, cancellationToken); return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, includes, metadata); } From aa795f05d7e242eaaa55b11b99f32e44abcdfcab Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 13 Aug 2015 17:14:56 -0400 Subject: [PATCH 145/186] make SetValue work with JToken null --- JSONAPI/Core/EnumAttributeValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/Core/EnumAttributeValueConverter.cs b/JSONAPI/Core/EnumAttributeValueConverter.cs index 2d7a8f44..df9a9599 100644 --- a/JSONAPI/Core/EnumAttributeValueConverter.cs +++ b/JSONAPI/Core/EnumAttributeValueConverter.cs @@ -37,7 +37,7 @@ public JToken GetValue(object resource) public void SetValue(object resource, JToken value) { - if (value == null) + if (value == null || value.Type == JTokenType.Null) { if (_isNullable) _property.SetValue(resource, null); From 4037996fa97bd6eebc074c9382bf724784b138cb Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 22 Aug 2015 17:52:43 -0400 Subject: [PATCH 146/186] add .vs folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fd5204b5..d9edd238 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.suo *.user *.sln.docstates +.vs/ # Build results [Dd]ebug/ From d9f859dd56f4297abbb60a9d793b6f3d5b8d8f2d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 22 Aug 2015 18:45:45 -0400 Subject: [PATCH 147/186] allow sorting by non-reference types Apparently sorting by value types such as DateTime, int, etc. never worked. --- .../DefaultSortingTransformerTests.cs | 35 +++++++----- .../DefaultSortingTransformer.cs | 54 ++++++++++++++----- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index a2e7e4b3..806fe18d 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -22,6 +22,8 @@ private class Dummy public string FirstName { get; set; } public string LastName { get; set; } + + public DateTime BirthDate { get; set; } } private IList _fixtures; @@ -32,15 +34,15 @@ public void SetupFixtures() { _fixtures = new List { - new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, - new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, - new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, - new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, - new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, - new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, - new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, - new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, - new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" } + new Dummy {Id = "1", FirstName = "Thomas", LastName = "Paine", BirthDate = new DateTime(1737, 2, 9)}, + new Dummy {Id = "2", FirstName = "Samuel", LastName = "Adams", BirthDate = new DateTime(1722, 9, 27)}, + new Dummy {Id = "3", FirstName = "George", LastName = "Washington", BirthDate = new DateTime(1732, 2, 22)}, + new Dummy {Id = "4", FirstName = "Thomas", LastName = "Jefferson", BirthDate = new DateTime(1743, 4, 13)}, + new Dummy {Id = "5", FirstName = "Martha", LastName = "Washington", BirthDate = new DateTime(1731, 6, 13)}, + new Dummy {Id = "6", FirstName = "Abraham", LastName = "Lincoln", BirthDate = new DateTime(1809, 2, 12)}, + new Dummy {Id = "7", FirstName = "Andrew", LastName = "Jackson", BirthDate = new DateTime(1767, 3, 15)}, + new Dummy {Id = "8", FirstName = "Andrew", LastName = "Johnson", BirthDate = new DateTime(1808, 12, 29)}, + new Dummy {Id = "9", FirstName = "William", LastName = "Harrison", BirthDate = new DateTime(1773, 2, 9)} }; _fixturesQuery = _fixtures.AsQueryable(); } @@ -52,7 +54,7 @@ private DefaultSortingTransformer GetTransformer() {"Dummy", "Dummies"} }); var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService)); - var registration = registrar.BuildRegistration(typeof(Dummy)); + var registration = registrar.BuildRegistration(typeof (Dummy)); var registry = new ResourceTypeRegistry(); registry.AddRegistration(registration); return new DefaultSortingTransformer(registry); @@ -131,13 +133,22 @@ public void Returns_400_if_sort_argument_is_whitespace_descending() [TestMethod] public void Returns_400_if_no_property_exists() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar", + "The attribute \"foobar\" does not exist on type \"dummies\"."); } [TestMethod] public void Returns_400_if_the_same_property_is_specified_more_than_once() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name", "The attribute \"last-name\" was specified more than once."); + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name", + "The attribute \"last-name\" was specified more than once."); + } + + [TestMethod] + public void Can_sort_by_DateTimeOffset() + { + var array = GetArray("http://api.example.com/dummies?sort=birth-date"); + array.Should().BeInAscendingOrder(d => d.BirthDate); } } } diff --git a/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs index 32b012c1..5a231159 100644 --- a/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs @@ -4,7 +4,6 @@ using System.Linq.Expressions; using System.Net.Http; using System.Reflection; -using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; @@ -43,7 +42,7 @@ public IOrderedQueryable Sort(IQueryable query, HttpRequestMessage requ sortExpressions = sortParam.Value.Split(','); } - var selectors = new List>>>(); + var selectors = new List>(); var usedProperties = new Dictionary(); var registration = _resourceTypeRegistry.GetRegistrationForType(typeof (T)); @@ -91,24 +90,55 @@ public IOrderedQueryable Sort(IQueryable query, HttpRequestMessage requ string.Format("The attribute \"{0}\" was specified more than once.", fieldName), "sort"); usedProperties[property] = null; - sortValueExpression = Expression.Property(paramExpr, property); } - var selector = Expression.Lambda>(sortValueExpression, paramExpr); - selectors.Add(Tuple.Create(ascending, selector)); + var selector = GetSelector(paramExpr, sortValueExpression, !ascending); + selectors.Add(selector); } var firstSelector = selectors.First(); - IOrderedQueryable workingQuery = - firstSelector.Item1 - ? query.OrderBy(firstSelector.Item2) - : query.OrderByDescending(firstSelector.Item2); + IOrderedQueryable workingQuery = firstSelector.ApplyInitially(query); + return selectors.Skip(1).Aggregate(workingQuery, (current, selector) => selector.ApplySubsequently(current)); + } + + private ISelector GetSelector(ParameterExpression paramExpr, Expression sortValueExpression, bool isDescending) + { + var lambda = Expression.Lambda(sortValueExpression, paramExpr); + var selectorType = typeof (Selector<,>).MakeGenericType(typeof (T), sortValueExpression.Type); + var selector = Activator.CreateInstance(selectorType, isDescending, lambda); + return (ISelector)selector; + } + } + + internal interface ISelector + { + IOrderedQueryable ApplyInitially(IQueryable unsortedQuery); + IOrderedQueryable ApplySubsequently(IOrderedQueryable currentQuery); + } + + internal class Selector : ISelector + { + private readonly bool _isDescending; + private readonly Expression> _propertyAccessorExpression; + + public Selector(bool isDescending, Expression> propertyAccessorExpression) + { + _isDescending = isDescending; + _propertyAccessorExpression = propertyAccessorExpression; + } - return selectors.Skip(1).Aggregate(workingQuery, - (current, selector) => - selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); + public IOrderedQueryable ApplyInitially(IQueryable unsortedQuery) + { + if (_isDescending) return unsortedQuery.OrderByDescending(_propertyAccessorExpression); + return unsortedQuery.OrderBy(_propertyAccessorExpression); + } + + public IOrderedQueryable ApplySubsequently(IOrderedQueryable currentQuery) + { + if (_isDescending) return currentQuery.ThenByDescending(_propertyAccessorExpression); + return currentQuery.ThenBy(_propertyAccessorExpression); } } } From b1a8c73c3ff3f88847cfd70da61241bf46a53501 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 25 Aug 2015 10:19:34 -0400 Subject: [PATCH 148/186] add test for sorting by resource with int key --- .../DefaultSortingTransformerTests.cs | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 806fe18d..675ffa14 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -26,8 +26,17 @@ private class Dummy public DateTime BirthDate { get; set; } } + private class Dummy2 + { + public int Id { get; set; } + + public string Name { get; set; } + } + private IList _fixtures; private IQueryable _fixturesQuery; + private IList _fixtures2; + private IQueryable _fixtures2Query; [TestInitialize] public void SetupFixtures() @@ -45,25 +54,39 @@ public void SetupFixtures() new Dummy {Id = "9", FirstName = "William", LastName = "Harrison", BirthDate = new DateTime(1773, 2, 9)} }; _fixturesQuery = _fixtures.AsQueryable(); + + _fixtures2 = new List + { + new Dummy2 {Id = 45, Name = "France"}, + new Dummy2 {Id = 52, Name = "Spain"}, + new Dummy2 {Id = 33, Name = "Mongolia"}, + }; + _fixtures2Query = _fixtures2.AsQueryable(); } private DefaultSortingTransformer GetTransformer() { - var pluralizationService = new PluralizationService(new Dictionary - { - {"Dummy", "Dummies"} - }); - var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService)); - var registration = registrar.BuildRegistration(typeof (Dummy)); + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); var registry = new ResourceTypeRegistry(); - registry.AddRegistration(registration); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy), "dummies")); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy2), "dummy2s")); return new DefaultSortingTransformer(registry); } - private Dummy[] GetArray(string uri) + private TFixture[] GetArray(string uri, IQueryable fixturesQuery) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - return GetTransformer().Sort(_fixturesQuery, request).ToArray(); + return GetTransformer().Sort(fixturesQuery, request).ToArray(); + } + + private Dummy[] GetDummyArray(string uri) + { + return GetArray(uri, _fixturesQuery); + } + + private Dummy2[] GetDummy2Array(string uri) + { + return GetArray(uri, _fixtures2Query); } private void RunTransformAndExpectFailure(string uri, string expectedMessage) @@ -81,28 +104,28 @@ private void RunTransformAndExpectFailure(string uri, string expectedMessage) [TestMethod] public void Sorts_by_attribute_ascending() { - var array = GetArray("http://api.example.com/dummies?sort=first-name"); + var array = GetDummyArray("http://api.example.com/dummies?sort=first-name"); array.Should().BeInAscendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_attribute_descending() { - var array = GetArray("http://api.example.com/dummies?sort=-first-name"); + var array = GetDummyArray("http://api.example.com/dummies?sort=-first-name"); array.Should().BeInDescendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_two_ascending_attributes() { - var array = GetArray("http://api.example.com/dummies?sort=last-name,first-name"); + var array = GetDummyArray("http://api.example.com/dummies?sort=last-name,first-name"); array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); } [TestMethod] public void Sorts_by_two_descending_attributes() { - var array = GetArray("http://api.example.com/dummies?sort=-last-name,-first-name"); + var array = GetDummyArray("http://api.example.com/dummies?sort=-last-name,-first-name"); array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); } @@ -147,8 +170,15 @@ public void Returns_400_if_the_same_property_is_specified_more_than_once() [TestMethod] public void Can_sort_by_DateTimeOffset() { - var array = GetArray("http://api.example.com/dummies?sort=birth-date"); + var array = GetDummyArray("http://api.example.com/dummies?sort=birth-date"); array.Should().BeInAscendingOrder(d => d.BirthDate); } + + [TestMethod] + public void Can_sort_by_resource_with_integer_key() + { + var array = GetDummy2Array("http://api.example.com/dummy2s?sort=name"); + array.Should().BeInAscendingOrder(d => d.Name); + } } } From 8bfe89c96abc06f38a1c2c22e91a78e1cd2e1d7b Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 30 Aug 2015 20:37:46 -0400 Subject: [PATCH 149/186] remove .vs file from source control not sure how that snuck in there... --- .vs/config/applicationhost.config | 1046 ----------------------------- 1 file changed, 1046 deletions(-) delete mode 100644 .vs/config/applicationhost.config diff --git a/.vs/config/applicationhost.config b/.vs/config/applicationhost.config deleted file mode 100644 index bc28afba..00000000 --- a/.vs/config/applicationhost.config +++ /dev/null @@ -1,1046 +0,0 @@ - - - - - - - - -
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 9449a980d6176bf4699ca124b2a5233d9ed78ca7 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 7 Sep 2015 18:05:38 -0400 Subject: [PATCH 150/186] write null for JTokenType.Null --- JSONAPI/Core/DecimalAttributeValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/Core/DecimalAttributeValueConverter.cs b/JSONAPI/Core/DecimalAttributeValueConverter.cs index 064572bc..c15c7477 100644 --- a/JSONAPI/Core/DecimalAttributeValueConverter.cs +++ b/JSONAPI/Core/DecimalAttributeValueConverter.cs @@ -39,7 +39,7 @@ public JToken GetValue(object resource) public void SetValue(object resource, JToken value) { - if (value == null) + if (value == null || value.Type == JTokenType.Null) _property.SetValue(resource, null); else { From 4f517a01da72b5233c7a094f5d5957f7fb854345 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 12 Sep 2015 14:13:08 -0400 Subject: [PATCH 151/186] improve error message when attempting to re-register a type --- JSONAPI/Core/ResourceTypeRegistry.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/JSONAPI/Core/ResourceTypeRegistry.cs b/JSONAPI/Core/ResourceTypeRegistry.cs index 7f790e98..57c3cbd0 100644 --- a/JSONAPI/Core/ResourceTypeRegistry.cs +++ b/JSONAPI/Core/ResourceTypeRegistry.cs @@ -57,10 +57,13 @@ public void AddRegistration(IResourceTypeRegistration registration) throw new InvalidOperationException(String.Format("The type `{0}` has already been registered.", registration.Type.FullName)); - if (_registrationsByName.ContainsKey(registration.ResourceTypeName)) + IResourceTypeRegistration existingRegistration; + if (_registrationsByName.TryGetValue(registration.ResourceTypeName, out existingRegistration)) throw new InvalidOperationException( - String.Format("The resource type name `{0}` has already been registered.", - registration.ResourceTypeName)); + String.Format("Could not register `{0} under resource type name `{1}` because `{1}` has already been registered by `{2}`.", + registration.Type.FullName, + registration.ResourceTypeName, + existingRegistration.Type.FullName)); _registrationsByType.Add(registration.Type, registration); _registrationsByName.Add(registration.ResourceTypeName, registration); From 151c82c9fac77b64d77ad606e5092c27a04318e6 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 16 Oct 2015 14:59:32 -0400 Subject: [PATCH 152/186] MappedDocumentMaterializer: allow performing final modifications to resource --- JSONAPI/Http/MappedDocumentMaterializer.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index b73934c1..313d553d 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Net.Http; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using JSONAPI.Core; @@ -83,6 +81,8 @@ public virtual async Task GetRecordById(string id, Http throw JsonApiException.CreateForNotFound( string.Format("No record exists with type `{0}` and ID `{1}`.", ResourceTypeName, id)); + await OnResourceFetched(primaryResource); + var baseUrl = _baseUrlService.GetBaseUrl(request); return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); } @@ -112,7 +112,17 @@ protected virtual Expression>[] GetIncludePathsForSingleResou { return null; } - + + /// + /// Hook for performing any final modifications to the resource before serialization + /// + /// + /// + protected virtual Task OnResourceFetched(TDto resource) + { + return Task.FromResult(0); + } + private string ConvertToJsonKeyPath(Expression> expression) { var visitor = new PathVisitor(_resourceTypeRegistry); From bbc19fb8ad9f04853986ab59ab3b711fe0004892 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 16 Oct 2015 15:19:37 -0400 Subject: [PATCH 153/186] pass cancellation token --- JSONAPI/Http/MappedDocumentMaterializer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 313d553d..9260c960 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -81,7 +81,7 @@ public virtual async Task GetRecordById(string id, Http throw JsonApiException.CreateForNotFound( string.Format("No record exists with type `{0}` and ID `{1}`.", ResourceTypeName, id)); - await OnResourceFetched(primaryResource); + await OnResourceFetched(primaryResource, cancellationToken); var baseUrl = _baseUrlService.GetBaseUrl(request); return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths, null); @@ -112,13 +112,14 @@ protected virtual Expression>[] GetIncludePathsForSingleResou { return null; } - + /// /// Hook for performing any final modifications to the resource before serialization /// /// + /// /// - protected virtual Task OnResourceFetched(TDto resource) + protected virtual Task OnResourceFetched(TDto resource, CancellationToken cancellationToken) { return Task.FromResult(0); } From ee5e35662237f34610e22656ec231d09e6df00a6 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 20 Oct 2015 23:29:48 -0400 Subject: [PATCH 154/186] convert incoming DateTime values to UTC --- JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs | 8 ++++---- JSONAPI/Core/DateTimeAttributeValueConverter.cs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs index 3c461c96..79d30e47 100644 --- a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs +++ b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs @@ -636,8 +636,8 @@ public void BuildRegistration_sets_up_correct_attribute_for_DateTime_field() var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); // Assert - AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); - AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.DateTimeField); AssertAttribute(reg, "date-time-field", null, new DateTime(), "0001-01-01T00:00:00", g => g.DateTimeField); } @@ -651,8 +651,8 @@ public void BuildRegistration_sets_up_correct_attribute_for_nullable_DateTime_fi var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); // Assert - AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); - AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.NullableDateTimeField); AssertAttribute(reg, "nullable-date-time-field", null, null, (DateTime?)null, g => g.NullableDateTimeField); } diff --git a/JSONAPI/Core/DateTimeAttributeValueConverter.cs b/JSONAPI/Core/DateTimeAttributeValueConverter.cs index ca3515ff..94550d07 100644 --- a/JSONAPI/Core/DateTimeAttributeValueConverter.cs +++ b/JSONAPI/Core/DateTimeAttributeValueConverter.cs @@ -41,6 +41,7 @@ public void SetValue(object resource, JToken value) else { var dateTimeValue = value.Value(); + if (dateTimeValue.Kind == DateTimeKind.Local) dateTimeValue = dateTimeValue.ToUniversalTime(); _property.SetValue(resource, dateTimeValue); } } From 62a8b0557816ee66268d2feb41e05d5f2623a61e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 25 Oct 2015 11:30:47 -0400 Subject: [PATCH 155/186] support 64-bit enums --- .../Core/EnumAttributeValueConverterTests.cs | 71 +++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + JSONAPI/Core/EnumAttributeValueConverter.cs | 2 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs diff --git a/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs b/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs new file mode 100644 index 00000000..9f14283f --- /dev/null +++ b/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class EnumAttributeValueConverterTests + { + public enum Int32Enum + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + public enum Int64Enum : long + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + private class Class1 + { + public Int32Enum Value { get; set; } + } + + private class Class2 + { + public Int64Enum Value { get; set; } + } + + [TestMethod] + public void GetValue_for_int32_enum() + { + // Arrange + var property = typeof (Class1).GetProperty("Value"); + var obj = new Class1 + { + Value = Int32Enum.Value1 + }; + + // Act + var converter = new EnumAttributeValueConverter(property, typeof(Int32Enum), false); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + actualValue.Value.Should().Be((long)1); + } + + [TestMethod] + public void GetValue_for_int64_enum() + { + // Arrange + var property = typeof(Class2).GetProperty("Value"); + var obj = new Class2 + { + Value = Int64Enum.Value1 + }; + + // Act + var converter = new EnumAttributeValueConverter(property, typeof(Int64Enum), false); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + actualValue.Value.Should().Be((long)1); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 53498a51..cd1b0632 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -85,6 +85,7 @@ + diff --git a/JSONAPI/Core/EnumAttributeValueConverter.cs b/JSONAPI/Core/EnumAttributeValueConverter.cs index df9a9599..fd893adc 100644 --- a/JSONAPI/Core/EnumAttributeValueConverter.cs +++ b/JSONAPI/Core/EnumAttributeValueConverter.cs @@ -30,7 +30,7 @@ public EnumAttributeValueConverter(PropertyInfo property, Type enumType, bool is public JToken GetValue(object resource) { var value = _property.GetValue(resource); - if (value != null) return (int) value; + if (value != null) return Convert.ToInt64(value); if (_isNullable) return null; return 0; } From 5013fef63d13593a371a94f87b275aac23253151 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 3 Nov 2015 12:32:51 -0500 Subject: [PATCH 156/186] allow specifying resource-level metadata in builders --- .../Builders/FallbackDocumentBuilderTests.cs | 4 ++-- ...tryDrivenSingleResourceDocumentBuilderTests.cs | 4 ++-- ...tQueryableResourceCollectionDocumentBuilder.cs | 2 +- .../Documents/Builders/FallbackDocumentBuilder.cs | 2 +- .../IResourceCollectionDocumentBuilder.cs | 4 +++- .../Builders/ISingleResourceDocumentBuilder.cs | 8 ++++++-- .../Builders/RegistryDrivenDocumentBuilder.cs | 15 +++++++++++---- ...stryDrivenResourceCollectionDocumentBuilder.cs | 5 +++-- ...RegistryDrivenSingleResourceDocumentBuilder.cs | 5 +++-- 9 files changed, 32 insertions(+), 17 deletions(-) diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs index 0ecd1365..8487a0e5 100644 --- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -32,7 +32,7 @@ public async Task Creates_single_resource_document_for_registered_non_collection var mockDocument = new Mock(MockBehavior.Strict); var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); - singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null, null)).Returns(mockDocument.Object); + singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null, null, null)).Returns(mockDocument.Object); var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); @@ -113,7 +113,7 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); mockResourceCollectionDocumentBuilder - .Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny(), It.IsAny())) + .Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny(), It.IsAny(), null)) .Returns(() => (mockDocument.Object)); // Act diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs index ba130e34..f5afb353 100644 --- a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -176,7 +176,7 @@ public void Returns_correct_document_for_resource() // Act var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); - var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }, metadata); + var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }, metadata, null); // Assert document.PrimaryData.Id.Should().Be("4"); @@ -258,7 +258,7 @@ public void Returns_correct_document_for_null_resource() // Act var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); - var document = documentBuilder.BuildDocument(null, "http://www.example.com", null, null); + var document = documentBuilder.BuildDocument(null, "http://www.example.com", null, null, null); // Assert document.PrimaryData.Should().BeNull(); diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index 79061d06..e1a79b66 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -51,7 +51,7 @@ public async Task BuildDocument(IQueryable qu var results = await _enumerationTransformer.Enumerate(paginatedQuery, cancellationToken); var metadata = await GetDocumentMetadata(query, filteredQuery, sortedQuery, paginationResults, cancellationToken); - return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, includes, metadata); + return _resourceCollectionDocumentBuilder.BuildDocument(results, linkBaseUrl, includes, metadata, null); } /// diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs index b93beb9c..accb309f 100644 --- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -80,7 +80,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage var buildDocumentMethod = _openBuildDocumentFromEnumerableMethod.Value.MakeGenericMethod(enumerableElementType); return - (dynamic)buildDocumentMethod.Invoke(_resourceCollectionDocumentBuilder, new[] { obj, linkBaseUrl, new string[] { }, null }); + (dynamic)buildDocumentMethod.Invoke(_resourceCollectionDocumentBuilder, new[] { obj, linkBaseUrl, new string[] { }, null, null }); } // Single resource object diff --git a/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs index 899eb99d..78d06d14 100644 --- a/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/IResourceCollectionDocumentBuilder.cs @@ -15,8 +15,10 @@ public interface IResourceCollectionDocumentBuilder /// A list of dot-separated paths to include in the compound document. /// If this collection is null or empty, no linkage will be included. /// Metadata for the top-level + /// Metadata to associate with individual resource objects /// /// - IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata); + IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata, + IDictionary resourceMetadata = null); } } \ No newline at end of file diff --git a/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs index cd35d6d7..6fc032fa 100644 --- a/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/ISingleResourceDocumentBuilder.cs @@ -1,4 +1,6 @@ -namespace JSONAPI.Documents.Builders +using System.Collections.Generic; + +namespace JSONAPI.Documents.Builders { /// /// Builds a response document from primary data objects @@ -13,7 +15,9 @@ public interface ISingleResourceDocumentBuilder /// A list of dot-separated paths to include in the compound document. /// If this collection is null or empty, no linkage will be included. /// Metadata to serialize at the top level of the document + /// Metadata about individual resource objects /// - ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata); + ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata, + IDictionary resourceMetadata = null); } } diff --git a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs index 0689c759..69b6bbac 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenDocumentBuilder.cs @@ -46,9 +46,10 @@ internal static bool PathExpressionMatchesCurrentPath(string currentPath, string /// /// /// + /// /// protected ResourceObject CreateResourceObject(object modelObject, IDictionary> idDictionariesByType, - string currentPath, string[] includePathExpressions, string linkBaseUrl) + string currentPath, string[] includePathExpressions, string linkBaseUrl, IDictionary resourceObjectMetadata) { if (modelObject == null) return null; @@ -97,7 +98,7 @@ protected ResourceObject CreateResourceObject(object modelObject, IDictionary diff --git a/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs index 2eb68c35..031f9f9b 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs @@ -19,11 +19,12 @@ public RegistryDrivenResourceCollectionDocumentBuilder(IResourceTypeRegistry res { } - public IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata) + public IResourceCollectionDocument BuildDocument(IEnumerable primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata metadata, + IDictionary resourceMetadata = null) { var idDictionariesByType = new Dictionary>(); var primaryDataResources = - primaryData.Select(d => (IResourceObject)CreateResourceObject(d, idDictionariesByType, null, includePathExpressions, linkBaseUrl)) + primaryData.Select(d => (IResourceObject)CreateResourceObject(d, idDictionariesByType, null, includePathExpressions, linkBaseUrl, resourceMetadata)) .ToArray(); var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); diff --git a/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs index 6930a5d3..029897b3 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilder.cs @@ -19,10 +19,11 @@ public RegistryDrivenSingleResourceDocumentBuilder(IResourceTypeRegistry resourc { } - public ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata) + public ISingleResourceDocument BuildDocument(object primaryData, string linkBaseUrl, string[] includePathExpressions, IMetadata topLevelMetadata, + IDictionary resourceMetadata = null) { var idDictionariesByType = new Dictionary>(); - var primaryDataResource = CreateResourceObject(primaryData, idDictionariesByType, null, includePathExpressions, linkBaseUrl); + var primaryDataResource = CreateResourceObject(primaryData, idDictionariesByType, null, includePathExpressions, linkBaseUrl, resourceMetadata); var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); var document = new SingleResourceDocument(primaryDataResource, relatedData, topLevelMetadata); From 4dbb4caf90a999810e22c3cb2972339edd2e775d Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 3 Nov 2015 19:04:42 -0500 Subject: [PATCH 157/186] add test for primitive attribute value converte --- ...imitiveTypeAttributeValueConverterTests.cs | 52 +++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + 2 files changed, 53 insertions(+) create mode 100644 JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs diff --git a/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs b/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs new file mode 100644 index 00000000..eb7b53f2 --- /dev/null +++ b/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class PrimitiveTypeAttributeValueConverterTests + { + private class Class1 + { + public int? NullableIntValue { get; set; } + } + + [TestMethod] + public void GetValue_for_null() + { + // Arrange + var property = typeof (Class1).GetProperty("NullableIntValue"); + var obj = new Class1 + { + NullableIntValue = null + }; + + // Act + var converter = new PrimitiveTypeAttributeValueConverter(property); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + ((object)actualValue).Should().Be(null); + } + + [TestMethod] + public void SetValue_for_null() + { + // Arrange + var property = typeof(Class1).GetProperty("NullableIntValue"); + var obj = new Class1 + { + NullableIntValue = 4 + }; + + // Act + var converter = new PrimitiveTypeAttributeValueConverter(property); + converter.SetValue(obj, JValue.CreateNull()); + + // Assert + obj.NullableIntValue.Should().Be(null); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index cd1b0632..40fa79d5 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -85,6 +85,7 @@ + From 9a996300aa3cd0b77a0012acf479afd982453fc8 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 6 Oct 2015 17:29:52 -0400 Subject: [PATCH 158/186] allow materializers to specify a default sort order --- .../StarshipDocumentMaterializer.cs | 3 +- ...shipOfficersRelatedResourceMaterializer.cs | 4 +- JSONAPI.Autofac/JsonApiAutofacModule.cs | 2 + .../EntityFrameworkDocumentMaterializer.cs | 9 +- ...ManyRelatedResourceDocumentMaterializer.cs | 3 +- .../DefaultSortingTransformerTests.cs | 51 +++++------ .../Builders/FallbackDocumentBuilderTests.cs | 19 +++- .../DefaultSortExpressionExtractorTests.cs | 86 +++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + ...ryableResourceCollectionDocumentBuilder.cs | 4 +- .../Builders/FallbackDocumentBuilder.cs | 7 +- ...ryableResourceCollectionDocumentBuilder.cs | 6 +- .../Http/DefaultSortExpressionExtractor.cs | 21 +++++ JSONAPI/Http/ISortExpressionExtractor.cs | 17 ++++ JSONAPI/Http/MappedDocumentMaterializer.cs | 6 +- ...ManyRelatedResourceDocumentMaterializer.cs | 9 +- JSONAPI/JSONAPI.csproj | 2 + .../DefaultSortingTransformer.cs | 19 +--- .../IQueryableSortingTransformer.cs | 4 +- 19 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs create mode 100644 JSONAPI/Http/DefaultSortExpressionExtractor.cs create mode 100644 JSONAPI/Http/ISortExpressionExtractor.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs index 1f5ad325..17cde210 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs @@ -21,10 +21,11 @@ public StarshipDocumentMaterializer( TestDbContext dbContext, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, IQueryableEnumerationTransformer queryableEnumerationTransformer, IResourceTypeRegistry resourceTypeRegistry) : base( queryableResourceCollectionDocumentBuilder, baseUrlService, singleResourceDocumentBuilder, - queryableEnumerationTransformer, resourceTypeRegistry) + queryableEnumerationTransformer, sortExpressionExtractor, resourceTypeRegistry) { _dbContext = dbContext; } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs index 750a1faa..5cb186b3 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs @@ -6,6 +6,7 @@ using JSONAPI.Core; using JSONAPI.Documents.Builders; using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers { @@ -15,8 +16,9 @@ public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToMany public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, IResourceTypeRegistration primaryTypeRegistration) - : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, primaryTypeRegistration) + : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, primaryTypeRegistration) { _dbContext = dbContext; } diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 69881ce1..e937de35 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -157,6 +157,8 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().SingleInstance(); builder.RegisterType().As(); + // Misc + builder.RegisterType().As().SingleInstance(); } } } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 844ce89a..cbe623ad 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -23,6 +23,7 @@ public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer wher private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; + private readonly ISortExpressionExtractor _sortExpressionExtractor; private readonly IBaseUrlService _baseUrlService; /// @@ -34,6 +35,7 @@ public EntityFrameworkDocumentMaterializer( IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, + ISortExpressionExtractor sortExpressionExtractor, IBaseUrlService baseUrlService) { _dbContext = dbContext; @@ -41,13 +43,15 @@ public EntityFrameworkDocumentMaterializer( _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; + _sortExpressionExtractor = sortExpressionExtractor; _baseUrlService = baseUrlService; } public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { var query = _dbContext.Set().AsQueryable(); - return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken); + var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); + return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken); } public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) @@ -127,8 +131,9 @@ protected async Task GetRelatedToMany(str _resourceTypeRegistration.ResourceTypeName, id)); var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda); + var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, cancellationToken); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken); } /// diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs index e2078758..2a52338e 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs @@ -28,8 +28,9 @@ public EntityFrameworkToManyRelatedResourceDocumentMaterializer( ResourceTypeRelationship relationship, DbContext dbContext, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, IResourceTypeRegistration primaryTypeRegistration) - : base(queryableResourceCollectionDocumentBuilder) + : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor) { _relationship = relationship; _dbContext = dbContext; diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs index 675ffa14..4283b1eb 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using FluentAssertions; using JSONAPI.Core; using JSONAPI.Documents.Builders; @@ -73,30 +72,27 @@ private DefaultSortingTransformer GetTransformer() return new DefaultSortingTransformer(registry); } - private TFixture[] GetArray(string uri, IQueryable fixturesQuery) + private TFixture[] GetArray(string[] sortExpressions, IQueryable fixturesQuery) { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - return GetTransformer().Sort(fixturesQuery, request).ToArray(); + return GetTransformer().Sort(fixturesQuery, sortExpressions).ToArray(); } - private Dummy[] GetDummyArray(string uri) + private Dummy[] GetDummyArray(string[] sortExpressions) { - return GetArray(uri, _fixturesQuery); + return GetArray(sortExpressions, _fixturesQuery); } - private Dummy2[] GetDummy2Array(string uri) + private Dummy2[] GetDummy2Array(string[] sortExpressions) { - return GetArray(uri, _fixtures2Query); + return GetArray(sortExpressions, _fixtures2Query); } - private void RunTransformAndExpectFailure(string uri, string expectedMessage) + private void RunTransformAndExpectFailure(string[] sortExpressions, string expectedMessage) { Action action = () => { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - // ReSharper disable once UnusedVariable - var result = GetTransformer().Sort(_fixturesQuery, request).ToArray(); + var result = GetTransformer().Sort(_fixturesQuery, sortExpressions).ToArray(); }; action.ShouldThrow().Which.Error.Detail.Should().Be(expectedMessage); } @@ -104,80 +100,87 @@ private void RunTransformAndExpectFailure(string uri, string expectedMessage) [TestMethod] public void Sorts_by_attribute_ascending() { - var array = GetDummyArray("http://api.example.com/dummies?sort=first-name"); + var array = GetDummyArray(new [] { "first-name" }); array.Should().BeInAscendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_attribute_descending() { - var array = GetDummyArray("http://api.example.com/dummies?sort=-first-name"); + var array = GetDummyArray(new [] { "-first-name" }); array.Should().BeInDescendingOrder(d => d.FirstName); } [TestMethod] public void Sorts_by_two_ascending_attributes() { - var array = GetDummyArray("http://api.example.com/dummies?sort=last-name,first-name"); + var array = GetDummyArray(new [] { "last-name", "first-name" }); array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); } [TestMethod] public void Sorts_by_two_descending_attributes() { - var array = GetDummyArray("http://api.example.com/dummies?sort=-last-name,-first-name"); + var array = GetDummyArray(new [] { "-last-name", "-first-name" }); array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); } + [TestMethod] + public void Sorts_by_id_when_expressions_are_empty() + { + var array = GetDummyArray(new string[] { }); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.Id)); + } + [TestMethod] public void Returns_400_if_sort_argument_is_empty() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=", "One of the sort expressions is empty."); + RunTransformAndExpectFailure(new[] { "" }, "One of the sort expressions is empty."); } [TestMethod] public void Returns_400_if_sort_argument_is_whitespace() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort= ", "One of the sort expressions is empty."); + RunTransformAndExpectFailure(new [] { " " }, "One of the sort expressions is empty."); } [TestMethod] public void Returns_400_if_sort_argument_is_empty_descending() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=-", "One of the sort expressions is empty."); + RunTransformAndExpectFailure(new [] { "-" }, "One of the sort expressions is empty."); } [TestMethod] public void Returns_400_if_sort_argument_is_whitespace_descending() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=- ", "One of the sort expressions is empty."); + RunTransformAndExpectFailure(new[] { "- " }, "One of the sort expressions is empty."); } [TestMethod] public void Returns_400_if_no_property_exists() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar", + RunTransformAndExpectFailure(new[] { "foobar" }, "The attribute \"foobar\" does not exist on type \"dummies\"."); } [TestMethod] public void Returns_400_if_the_same_property_is_specified_more_than_once() { - RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name", + RunTransformAndExpectFailure(new[] { "last-name", "last-name" }, "The attribute \"last-name\" was specified more than once."); } [TestMethod] public void Can_sort_by_DateTimeOffset() { - var array = GetDummyArray("http://api.example.com/dummies?sort=birth-date"); + var array = GetDummyArray(new [] { "birth-date" }); array.Should().BeInAscendingOrder(d => d.BirthDate); } [TestMethod] public void Can_sort_by_resource_with_integer_key() { - var array = GetDummy2Array("http://api.example.com/dummy2s?sort=name"); + var array = GetDummy2Array(new [] { "name" }); array.Should().BeInAscendingOrder(d => d.Name); } } diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs index 8487a0e5..24943f4a 100644 --- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -43,9 +43,12 @@ public async Task Creates_single_resource_document_for_registered_non_collection var mockBaseUrlService = new Mock(MockBehavior.Strict); mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com"); + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new [] { "id "}); + // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token); // Assert @@ -70,19 +73,24 @@ public async Task Creates_resource_collection_document_for_queryables() var mockBaseUrlService = new Mock(MockBehavior.Strict); mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); + + var sortExpressions = new[] { "id" }; var cancellationTokenSource = new CancellationTokenSource(); var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); mockQueryableDocumentBuilder - .Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token, null)) + .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, null)) .Returns(Task.FromResult(mockDocument.Object)); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions); + // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); // Assert @@ -116,9 +124,12 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera .Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny(), It.IsAny(), null)) .Returns(() => (mockDocument.Object)); + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " }); + // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); // Assert diff --git a/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs b/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs new file mode 100644 index 00000000..85b6003b --- /dev/null +++ b/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs @@ -0,0 +1,86 @@ +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class DefaultSortExpressionExtractorTests + { + [TestMethod] + public void ExtractsSingleSortExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("first-name"); + } + + [TestMethod] + public void ExtractsSingleDescendingSortExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=-first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("-first-name"); + } + + [TestMethod] + public void ExtractsMultipleSortExpressionsFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=last-name,first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("last-name", "first-name"); + } + + [TestMethod] + public void ExtractsMultipleSortExpressionsFromUriWithDifferentDirections() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=last-name,-first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("last-name", "-first-name"); + } + + [TestMethod] + public void ExtractsNothingWhenThereIsNoSortParam() + { + // Arrange + const string uri = "http://api.example.com/dummies"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Length.Should().Be(0); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 40fa79d5..7b2fff70 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -90,6 +90,7 @@ + diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index e1a79b66..d47e93c6 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -38,11 +38,11 @@ public DefaultQueryableResourceCollectionDocumentBuilder( _baseUrlService = baseUrlService; } - public async Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken, + public async Task BuildDocument(IQueryable query, HttpRequestMessage request, string[] sortExpressions, CancellationToken cancellationToken, string[] includes = null) { var filteredQuery = _filteringTransformer.Filter(query, request); - var sortedQuery = _sortingTransformer.Sort(filteredQuery, request); + var sortedQuery = _sortingTransformer.Sort(filteredQuery, sortExpressions); var paginationResults = _paginationTransformer.ApplyPagination(sortedQuery, request); var paginatedQuery = paginationResults.PagedQuery; diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs index accb309f..f6882571 100644 --- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -17,6 +17,7 @@ public class FallbackDocumentBuilder : IFallbackDocumentBuilder private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly IResourceCollectionDocumentBuilder _resourceCollectionDocumentBuilder; + private readonly ISortExpressionExtractor _sortExpressionExtractor; private readonly IBaseUrlService _baseUrlService; private readonly Lazy _openBuildDocumentFromQueryableMethod; private readonly Lazy _openBuildDocumentFromEnumerableMethod; @@ -27,11 +28,13 @@ public class FallbackDocumentBuilder : IFallbackDocumentBuilder public FallbackDocumentBuilder(ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, IBaseUrlService baseUrlService) { _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _resourceCollectionDocumentBuilder = resourceCollectionDocumentBuilder; + _sortExpressionExtractor = sortExpressionExtractor; _baseUrlService = baseUrlService; _openBuildDocumentFromQueryableMethod = @@ -60,8 +63,10 @@ public async Task BuildDocument(object obj, HttpRequestMessage var buildDocumentMethod = _openBuildDocumentFromQueryableMethod.Value.MakeGenericMethod(queryableElementType); + var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(requestMessage); + dynamic materializedQueryTask = buildDocumentMethod.Invoke(_queryableResourceCollectionDocumentBuilder, - new[] { obj, requestMessage, cancellationToken, null }); + new[] { obj, requestMessage, sortExpressions, cancellationToken, null }); return await materializedQueryTask; } diff --git a/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs index c1040d49..0c15cb50 100644 --- a/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/IQueryableResourceCollectionDocumentBuilder.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -16,11 +15,12 @@ public interface IQueryableResourceCollectionDocumentBuilder /// /// The query to materialize to build the response document /// The request containing parameters to determine how to sort/filter/paginate the query + /// An array of paths to sort by /// /// The set of paths to include in the compound document /// /// - Task BuildDocument(IQueryable query, HttpRequestMessage request, CancellationToken cancellationToken, + Task BuildDocument(IQueryable query, HttpRequestMessage request, string[] sortExpressions, CancellationToken cancellationToken, string[] includePaths = null); } } diff --git a/JSONAPI/Http/DefaultSortExpressionExtractor.cs b/JSONAPI/Http/DefaultSortExpressionExtractor.cs new file mode 100644 index 00000000..59cb449a --- /dev/null +++ b/JSONAPI/Http/DefaultSortExpressionExtractor.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.Http +{ + /// + /// Default implementation of + /// + public class DefaultSortExpressionExtractor : ISortExpressionExtractor + { + private const string SortQueryParamKey = "sort"; + + public string[] ExtractSortExpressions(HttpRequestMessage requestMessage) + { + var queryParams = requestMessage.GetQueryNameValuePairs(); + var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); + if (sortParam.Key != SortQueryParamKey) return new string[] {}; + return sortParam.Value.Split(','); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Http/ISortExpressionExtractor.cs b/JSONAPI/Http/ISortExpressionExtractor.cs new file mode 100644 index 00000000..62f9d42f --- /dev/null +++ b/JSONAPI/Http/ISortExpressionExtractor.cs @@ -0,0 +1,17 @@ +using System.Net.Http; + +namespace JSONAPI.Http +{ + /// + /// Service to extract sort expressions from an HTTP request + /// + public interface ISortExpressionExtractor + { + /// + /// Extracts sort expressions from the request + /// + /// + /// + string[] ExtractSortExpressions(HttpRequestMessage requestMessage); + } +} diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 9260c960..432798fd 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -22,6 +22,7 @@ public abstract class MappedDocumentMaterializer : IDocumentMater private readonly IBaseUrlService _baseUrlService; private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IQueryableEnumerationTransformer _queryableEnumerationTransformer; + private readonly ISortExpressionExtractor _sortExpressionExtractor; private readonly IResourceTypeRegistry _resourceTypeRegistry; /// @@ -47,12 +48,14 @@ protected MappedDocumentMaterializer( IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IQueryableEnumerationTransformer queryableEnumerationTransformer, + ISortExpressionExtractor sortExpressionExtractor, IResourceTypeRegistry resourceTypeRegistry) { _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _baseUrlService = baseUrlService; _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _queryableEnumerationTransformer = queryableEnumerationTransformer; + _sortExpressionExtractor = sortExpressionExtractor; _resourceTypeRegistry = resourceTypeRegistry; } @@ -67,7 +70,8 @@ public virtual async Task GetRecords(HttpRequestMes var includePaths = GetIncludePathsForQuery() ?? new Expression>[] { }; var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); var mappedQuery = GetMappedQuery(entityQuery, includePaths); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths); + var sortationPaths = _sortExpressionExtractor.ExtractSortExpressions(request); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, sortationPaths, cancellationToken, jsonApiPaths); } public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs index 5bd863e6..337f5531 100644 --- a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs @@ -14,13 +14,17 @@ namespace JSONAPI.Http public abstract class QueryableToManyRelatedResourceDocumentMaterializer : IRelatedResourceDocumentMaterializer { private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + private readonly ISortExpressionExtractor _sortExpressionExtractor; /// /// Creates a new QueryableRelatedResourceDocumentMaterializer /// - protected QueryableToManyRelatedResourceDocumentMaterializer(IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder) + protected QueryableToManyRelatedResourceDocumentMaterializer( + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor) { _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + _sortExpressionExtractor = sortExpressionExtractor; } public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, @@ -28,7 +32,8 @@ public async Task GetRelatedResourceDocument(string primaryRes { var query = await GetRelatedQuery(primaryResourceId, cancellationToken); var includes = GetIncludePaths(); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken, includes); // TODO: allow implementors to specify metadata + var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); // TODO: allow implementors to specify metadata } /// diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 7398c9c5..466d3411 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -84,9 +84,11 @@ + + diff --git a/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs index 5a231159..2898a58b 100644 --- a/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Net.Http; using System.Reflection; using JSONAPI.Core; using JSONAPI.Documents.Builders; @@ -25,22 +24,10 @@ public DefaultSortingTransformer(IResourceTypeRegistry resourceTypeRegistry) _resourceTypeRegistry = resourceTypeRegistry; } - private const string SortQueryParamKey = "sort"; - - public IOrderedQueryable Sort(IQueryable query, HttpRequestMessage request) + public IOrderedQueryable Sort(IQueryable query, string[] sortExpressions) { - var queryParams = request.GetQueryNameValuePairs(); - var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); - - string[] sortExpressions; - if (sortParam.Key != SortQueryParamKey) - { - sortExpressions = new[] { "id" }; // We have to sort by something, so make it the ID. - } - else - { - sortExpressions = sortParam.Value.Split(','); - } + if (sortExpressions == null || sortExpressions.Length == 0) + sortExpressions = new [] { "id" }; var selectors = new List>(); var usedProperties = new Dictionary(); diff --git a/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs b/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs index 64e06fbf..9583527c 100644 --- a/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs +++ b/JSONAPI/QueryableTransformers/IQueryableSortingTransformer.cs @@ -12,9 +12,9 @@ public interface IQueryableSortingTransformer /// Sorts the provided queryable based on information from the request message. /// /// The input query - /// The request message + /// The expressions to sort by /// The element type of the query /// The sorted query - IOrderedQueryable Sort(IQueryable query, HttpRequestMessage request); + IOrderedQueryable Sort(IQueryable query, string[] sortExpressions); } } \ No newline at end of file From 3198bad7212b23f7ab5d745addae3140d9424cae Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 9 Oct 2015 14:07:27 -0400 Subject: [PATCH 159/186] allow overriding sort expressions in queryable to-many materializer --- ...ryableToManyRelatedResourceDocumentMaterializer.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs index 337f5531..7d190960 100644 --- a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs @@ -33,6 +33,8 @@ public async Task GetRelatedResourceDocument(string primaryRes var query = await GetRelatedQuery(primaryResourceId, cancellationToken); var includes = GetIncludePaths(); var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); + if (sortExpressions == null || sortExpressions.Length < 1) + sortExpressions = GetDefaultSortExpressions(); return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); // TODO: allow implementors to specify metadata } @@ -49,5 +51,14 @@ protected virtual string[] GetIncludePaths() { return null; } + + /// + /// If the client doesn't request any sort expressions, these expressions will be used for sorting instead. + /// + /// + protected virtual string[] GetDefaultSortExpressions() + { + return null; + } } } \ No newline at end of file From 579012a5af062bdc93e9bb720289c5b781aef82c Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 16 Oct 2015 15:10:18 -0400 Subject: [PATCH 160/186] allow overriding sort expressions in MappedDocumentMaterializer --- JSONAPI/Http/MappedDocumentMaterializer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index 432798fd..ee002270 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -71,6 +71,9 @@ public virtual async Task GetRecords(HttpRequestMes var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray(); var mappedQuery = GetMappedQuery(entityQuery, includePaths); var sortationPaths = _sortExpressionExtractor.ExtractSortExpressions(request); + if (sortationPaths == null || !sortationPaths.Any()) + sortationPaths = GetDefaultSortExpressions(); + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, sortationPaths, cancellationToken, jsonApiPaths); } @@ -117,6 +120,15 @@ protected virtual Expression>[] GetIncludePathsForSingleResou return null; } + /// + /// Hook for specifying sort expressions when fetching a collection + /// + /// + protected virtual string[] GetDefaultSortExpressions() + { + return new[] { "id" }; + } + /// /// Hook for performing any final modifications to the resource before serialization /// From 4e4a4ba67d525f546d631f401d68c48d7f583ead Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 3 Dec 2015 20:17:49 -0500 Subject: [PATCH 161/186] add convenience method for 400 errors --- JSONAPI/Documents/Builders/JsonApiException.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/JSONAPI/Documents/Builders/JsonApiException.cs b/JSONAPI/Documents/Builders/JsonApiException.cs index a8b067a3..00472ea0 100644 --- a/JSONAPI/Documents/Builders/JsonApiException.cs +++ b/JSONAPI/Documents/Builders/JsonApiException.cs @@ -55,6 +55,21 @@ public static JsonApiException Create(string title, string detail, HttpStatusCod return new JsonApiException(error); } + /// + /// Creates a JsonApiException to send a 400 Bad Request error. + /// + public static JsonApiException CreateForBadRequest(string detail = null) + { + var error = new Error + { + Id = Guid.NewGuid().ToString(), + Status = HttpStatusCode.BadRequest, + Title = "Bad request", + Detail = detail + }; + return new JsonApiException(error); + } + /// /// Creates a JsonApiException to send a 404 Not Found error. /// From fd406f71f3dd6dc61b018a6c49952d5f6161dabb Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 3 Dec 2015 20:18:13 -0500 Subject: [PATCH 162/186] throw more helpful errors from filtering transformer --- .../DefaultFilteringTransformerTests.cs | 3 ++- .../DefaultFilteringTransformer.cs | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index bf59c926..39850fc4 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -9,6 +9,7 @@ using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.Documents.Builders; using JSONAPI.QueryableTransformers; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -1147,7 +1148,7 @@ public void Filters_by_missing_nullable_double_property() public void Does_not_filter_unknown_type() { Action action = () => GetArray("http://api.example.com/dummies?filter[unknownTypeField]=asdfasd"); - action.ShouldThrow().Which.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + action.ShouldThrow().Which.Error.Status.Should().Be(HttpStatusCode.BadRequest); } #endregion diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index baf37b80..88e09605 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -8,6 +8,7 @@ using System.Web.Http; using JSONAPI.ActionFilters; using JSONAPI.Core; +using JSONAPI.Documents.Builders; namespace JSONAPI.QueryableTransformers { @@ -79,11 +80,14 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress } catch (TypeRegistrationNotFoundException) { - throw new HttpResponseException(HttpStatusCode.BadRequest); + throw JsonApiException.CreateForBadRequest("No registration exists for the specified type"); } var resourceTypeField = registration.GetFieldByName(filterField); - + if (resourceTypeField == null) + throw JsonApiException.CreateForBadRequest( + string.Format("No attribute {0} exists on the specified type.", filterField)); + var queryValue = queryPair.Value; if (string.IsNullOrWhiteSpace(queryValue)) queryValue = null; @@ -100,7 +104,8 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (relationshipModelProperty != null) expr = GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); - if (expr == null) throw new HttpResponseException(HttpStatusCode.BadRequest); + if (expr == null) throw JsonApiException.CreateForBadRequest( + string.Format("The attribute {0} is unsupported for filtering.", filterField)); workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); } @@ -344,7 +349,7 @@ private Expression GetPredicateBodyForRelationship(ResourceTypeRelationship reso } catch (TypeRegistrationNotFoundException) { - throw new HttpResponseException(HttpStatusCode.BadRequest); + throw JsonApiException.CreateForBadRequest("No registration exists for the specified type"); } var prop = resourceTypeProperty.Property; From 33d57054d93575114acf3fa817157a45c8f59e80 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 6 Dec 2015 18:07:32 -0500 Subject: [PATCH 163/186] fix incorrect comment --- JSONAPI/Documents/IResourceLinkage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI/Documents/IResourceLinkage.cs b/JSONAPI/Documents/IResourceLinkage.cs index ef3ca445..b45aef06 100644 --- a/JSONAPI/Documents/IResourceLinkage.cs +++ b/JSONAPI/Documents/IResourceLinkage.cs @@ -11,8 +11,8 @@ public interface IResourceLinkage bool IsToMany { get; } /// - /// The identifiers this linkage refers to. If IsToMany is true, this - /// property must return an array of length either 0 or 1. If false, + /// The identifiers this linkage refers to. If IsToMany is false, this + /// property must return an array of length either 0 or 1. If true, /// the array may be of any length. This property must not return null. /// IResourceIdentifier[] Identifiers { get; } From 202b522e9b814002f0b862f23ed46e061cd5e4ff Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sun, 6 Dec 2015 22:12:43 -0500 Subject: [PATCH 164/186] add EphemeralRelatedResourceReader --- .../DefaultEphemeralRelatedResourceCreator.cs | 18 +++ .../Core/EphemeralRelatedResourceReader.cs | 125 ++++++++++++++++++ .../Core/IEphemeralRelatedResourceCreator.cs | 16 +++ .../Core/IEphemeralRelatedResourceReader.cs | 19 +++ JSONAPI/JSONAPI.csproj | 4 + 5 files changed, 182 insertions(+) create mode 100644 JSONAPI/Core/DefaultEphemeralRelatedResourceCreator.cs create mode 100644 JSONAPI/Core/EphemeralRelatedResourceReader.cs create mode 100644 JSONAPI/Core/IEphemeralRelatedResourceCreator.cs create mode 100644 JSONAPI/Core/IEphemeralRelatedResourceReader.cs diff --git a/JSONAPI/Core/DefaultEphemeralRelatedResourceCreator.cs b/JSONAPI/Core/DefaultEphemeralRelatedResourceCreator.cs new file mode 100644 index 00000000..7eb6f419 --- /dev/null +++ b/JSONAPI/Core/DefaultEphemeralRelatedResourceCreator.cs @@ -0,0 +1,18 @@ +using System; + +namespace JSONAPI.Core +{ + /// + /// Default implementation of , using Activator + /// + public class DefaultEphemeralRelatedResourceCreator : IEphemeralRelatedResourceCreator + { + /// + public object CreateEphemeralResource(IResourceTypeRegistration resourceTypeRegistration, string id) + { + var obj = Activator.CreateInstance(resourceTypeRegistration.Type); + resourceTypeRegistration.SetIdForResource(obj, id); + return obj; + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/EphemeralRelatedResourceReader.cs b/JSONAPI/Core/EphemeralRelatedResourceReader.cs new file mode 100644 index 00000000..9ef242d6 --- /dev/null +++ b/JSONAPI/Core/EphemeralRelatedResourceReader.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Json; + +namespace JSONAPI.Core +{ + /// + /// Populates property values on an ephemeral resource + /// + /// + public class EphemeralRelatedResourceReader : IEphemeralRelatedResourceReader + { + private readonly IResourceTypeRegistry _resourceTypeRegistry; + private readonly IEphemeralRelatedResourceCreator _ephemeralRelatedResourceCreator; + private readonly Lazy _resourceTypeRegistration; + private readonly MethodInfo _openSetToManyRelationshipValueMethod; + + /// + /// Creates a new EphemeralRelatedResourceReader + /// + /// + /// + public EphemeralRelatedResourceReader(IResourceTypeRegistry resourceTypeRegistry, IEphemeralRelatedResourceCreator ephemeralRelatedResourceCreator) + { + _resourceTypeRegistry = resourceTypeRegistry; + _ephemeralRelatedResourceCreator = ephemeralRelatedResourceCreator; + _resourceTypeRegistration = new Lazy(() => _resourceTypeRegistry.GetRegistrationForType(typeof(T))); + _openSetToManyRelationshipValueMethod = GetType() + .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); + } + + public void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject) + { + var relationship = _resourceTypeRegistration.Value.GetFieldByName(jsonKey) as ResourceTypeRelationship; + if (relationship == null) return; + + if (relationship.IsToMany) + SetPropertyForToManyRelationship(ephemeralResource, relationship, relationshipObject.Linkage); + else + SetPropertyForToOneRelationship(ephemeralResource, relationship, relationshipObject.Linkage); + } + + protected virtual void SetPropertyForToOneRelationship(T ephemeralResource, ResourceTypeRelationship relationship, IResourceLinkage linkage) + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-one relationship", + "Expected an object for to-one linkage, but no linkage was specified.", $"/data/relationships/{relationship.JsonKey}"); + + if (linkage.IsToMany) + throw new DeserializationException("Invalid linkage for to-one relationship", + "Expected an object or null for to-one linkage", + $"/data/relationships/{relationship.JsonKey}/data"); + + var identifier = linkage.Identifiers.FirstOrDefault(); + if (identifier == null) + { + relationship.Property.SetValue(ephemeralResource, null); + } + else + { + var relatedObjectRegistration = _resourceTypeRegistry.GetRegistrationForResourceTypeName(identifier.Type); + var relatedObject = _ephemeralRelatedResourceCreator.CreateEphemeralResource(relatedObjectRegistration, identifier.Id); + + relationship.Property.SetValue(ephemeralResource, relatedObject); + } + } + + protected virtual void SetPropertyForToManyRelationship(T ephemeralResource, ResourceTypeRelationship relationship, + IResourceLinkage linkage) + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-many relationship", + "Expected an array for to-many linkage, but no linkage was specified.", $"/data/relationships/{relationship.JsonKey}"); + + if (!linkage.IsToMany) + throw new DeserializationException("Invalid linkage for to-many relationship", + "Expected an array for to-many linkage.", + $"/data/relationships/{relationship.JsonKey}/data"); + + var newCollection = (from resourceIdentifier in linkage.Identifiers + let relatedObjectRegistration = _resourceTypeRegistry.GetRegistrationForResourceTypeName(resourceIdentifier.Type) + select _ephemeralRelatedResourceCreator.CreateEphemeralResource(relatedObjectRegistration, resourceIdentifier.Id)).ToList(); + + var method = _openSetToManyRelationshipValueMethod.MakeGenericMethod(relationship.RelatedType); + method.Invoke(this, new object[] { ephemeralResource, newCollection, relationship }); + } + + /// + /// Sets the value of a to-many relationship + /// + protected void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) + { + var currentValue = relationship.Property.GetValue(material); + var typedArray = relatedObjects.Select(o => (TRelated)o).ToArray(); + if (relationship.Property.PropertyType.IsAssignableFrom(typeof(List))) + { + if (currentValue == null) + { + relationship.Property.SetValue(material, typedArray.ToList()); + } + else + { + var listCurrentValue = (ICollection)currentValue; + var itemsToAdd = typedArray.Except(listCurrentValue); + var itemsToRemove = listCurrentValue.Except(typedArray).ToList(); + + foreach (var related in itemsToAdd) + listCurrentValue.Add(related); + + foreach (var related in itemsToRemove) + listCurrentValue.Remove(related); + } + } + else + { + relationship.Property.SetValue(material, typedArray); + } + } + } +} diff --git a/JSONAPI/Core/IEphemeralRelatedResourceCreator.cs b/JSONAPI/Core/IEphemeralRelatedResourceCreator.cs new file mode 100644 index 00000000..5fdc47c5 --- /dev/null +++ b/JSONAPI/Core/IEphemeralRelatedResourceCreator.cs @@ -0,0 +1,16 @@ +namespace JSONAPI.Core +{ + /// + /// Service for creating an instance of a registered resource type + /// + public interface IEphemeralRelatedResourceCreator + { + /// + /// Creates an instance of the specified resource type, with the given ID + /// + /// The type to create the instance for + /// The ID for the resource + /// A new instance of the specified resource type + object CreateEphemeralResource(IResourceTypeRegistration resourceTypeRegistration, string id); + } +} \ No newline at end of file diff --git a/JSONAPI/Core/IEphemeralRelatedResourceReader.cs b/JSONAPI/Core/IEphemeralRelatedResourceReader.cs new file mode 100644 index 00000000..827b455a --- /dev/null +++ b/JSONAPI/Core/IEphemeralRelatedResourceReader.cs @@ -0,0 +1,19 @@ +using JSONAPI.Documents; + +namespace JSONAPI.Core +{ + /// + /// Populates property values on an ephemeral resource from a relationship object + /// + /// + public interface IEphemeralRelatedResourceReader + { + /// + /// Sets the property on the ephemeral resource that corresponds to the given property + /// + /// + /// + /// + void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject); + } +} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 466d3411..cfb6b1d5 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -74,7 +74,11 @@ + + + + From bab795a1664f459691cb0122aad079ed98decf8b Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Sat, 26 Mar 2016 18:17:54 -0400 Subject: [PATCH 165/186] support serializing complex attributes to types other than string --- .../Core/ResourceTypeRegistrarTests.cs | 98 ++++++++++++++++--- JSONAPI.Tests/Models/AttributeGrabBag.cs | 16 +++ .../Core/ComplexAttributeValueConverter.cs | 2 +- .../ObjectComplexAttributeValueConverter.cs | 39 ++++++++ JSONAPI/Core/ResourceTypeRegistrar.cs | 12 ++- JSONAPI/JSONAPI.csproj | 1 + 6 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 JSONAPI/Core/ObjectComplexAttributeValueConverter.cs diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs index 79d30e47..b1b242e9 100644 --- a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs +++ b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs @@ -187,25 +187,38 @@ private AttributeGrabBag InitializeGrabBag() private void AssertAttribute(IResourceTypeRegistration reg, string attributeName, JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func getPropertyFunc) + { + AssertAttributeHelper(reg, attributeName, tokenToSet, grabBag => + { + var propertyValueAfterSet = getPropertyFunc(grabBag); + propertyValueAfterSet.Should().Be(expectedPropertyValue); + }, token => + { + if (expectedTokenAfterSet == null) + token.Should().BeNull(); + else + { + var convertedTokenValue = token.Value(); + convertedTokenValue.Should().Be(expectedTokenAfterSet); + } + }); + } + + private void AssertAttributeHelper(IResourceTypeRegistration reg, string attributeName, + JToken tokenToSet, Action testPropertyValueAfterSet, + Action testTokenAfterSetAndGet) { var grabBag = InitializeGrabBag(); var field = reg.GetFieldByName(attributeName); - var attribute = (ResourceTypeAttribute) field; + var attribute = (ResourceTypeAttribute)field; attribute.JsonKey.Should().Be(attributeName); attribute.SetValue(grabBag, tokenToSet); - var propertyValueAfterSet = getPropertyFunc(grabBag); - propertyValueAfterSet.Should().Be(expectedPropertyValue); - + testPropertyValueAfterSet(grabBag); + var convertedToken = attribute.GetValue(grabBag); - if (expectedTokenAfterSet == null) - convertedToken.Should().BeNull(); - else - { - var convertedTokenValue = convertedToken.Value(); - convertedTokenValue.Should().Be(expectedTokenAfterSet); - } + testTokenAfterSetAndGet(convertedToken); } [TestMethod] @@ -733,5 +746,68 @@ public void BuildRegistration_sets_up_correct_attribute_for_nullable_enum_field( AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField); AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField); } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_to_one_complex_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttributeHelper(reg, "to-one-complex-type-field", + new JObject { { "intProp", 32 }, { "StringProp", "qux" } }, + grabBag => + { + grabBag.ToOneComplexTypeField.Should().NotBeNull(); + grabBag.ToOneComplexTypeField.IntProp.Should().Be(32); + grabBag.ToOneComplexTypeField.StringProp.Should().Be("qux"); + }, + token => + { + ((int)token["intProp"]).Should().Be(32); + ((string)token["StringProp"]).Should().Be("qux"); + }); + AssertAttribute(reg, "to-one-complex-type-field", null, null, (SampleComplexType)null, g => g.ToOneComplexTypeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_to_many_complex_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttributeHelper(reg, "to-many-complex-type-field", + new JArray + { + new JObject { { "intProp", 49 }, { "StringProp", "blue" } }, + new JObject { { "intProp", 67 }, { "StringProp", "orange" } } + }, + grabBag => + { + var result = grabBag.ToManyComplexTypeField.ToArray(); + result.Length.Should().Be(2); + result[0].IntProp.Should().Be(49); + result[0].StringProp.Should().Be("blue"); + result[1].IntProp.Should().Be(67); + result[1].StringProp.Should().Be("orange"); + }, + token => + { + var jarray = (JArray) token; + jarray.Count.Should().Be(2); + ((int)jarray[0]["intProp"]).Should().Be(49); + ((string)jarray[0]["StringProp"]).Should().Be("blue"); + ((int)jarray[1]["intProp"]).Should().Be(67); + ((string)jarray[1]["StringProp"]).Should().Be("orange"); + }); + AssertAttribute(reg, "to-many-complex-type-field", null, null, (SampleComplexType[])null, g => g.ToManyComplexTypeField); + } } } diff --git a/JSONAPI.Tests/Models/AttributeGrabBag.cs b/JSONAPI.Tests/Models/AttributeGrabBag.cs index 85490cef..dca40e34 100644 --- a/JSONAPI.Tests/Models/AttributeGrabBag.cs +++ b/JSONAPI.Tests/Models/AttributeGrabBag.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using JSONAPI.Attributes; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JSONAPI.Tests.Models { @@ -9,6 +12,13 @@ public enum SampleEnum Value2 = 2 } + public class SampleComplexType + { + [JsonProperty("intProp")] + public Int32 IntProp { get; set; } + public string StringProp { get; set; } + } + public class AttributeGrabBag { public string Id { get; set; } @@ -48,5 +58,11 @@ public class AttributeGrabBag [SerializeAsComplex] public string ComplexAttributeField { get; set; } + + [SerializeAsComplex] + public SampleComplexType ToOneComplexTypeField { get; set; } + + [SerializeAsComplex] + public virtual ICollection ToManyComplexTypeField { get; set; } } } diff --git a/JSONAPI/Core/ComplexAttributeValueConverter.cs b/JSONAPI/Core/ComplexAttributeValueConverter.cs index ab892c97..e26726d0 100644 --- a/JSONAPI/Core/ComplexAttributeValueConverter.cs +++ b/JSONAPI/Core/ComplexAttributeValueConverter.cs @@ -5,7 +5,7 @@ namespace JSONAPI.Core { /// /// Implementation of suitable for - /// use with complex attributes. + /// use with complex attributes that deserialize to strings. /// public class ComplexAttributeValueConverter : IAttributeValueConverter { diff --git a/JSONAPI/Core/ObjectComplexAttributeValueConverter.cs b/JSONAPI/Core/ObjectComplexAttributeValueConverter.cs new file mode 100644 index 00000000..c6c73af0 --- /dev/null +++ b/JSONAPI/Core/ObjectComplexAttributeValueConverter.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Core +{ + /// + /// Implementation of suitable for + /// use with complex attributes that deserialize to custom types. + /// + public class ObjectComplexAttributeValueConverter : IAttributeValueConverter + { + private readonly PropertyInfo _property; + private readonly bool _isToMany; + + /// + /// Creates a new ComplexAttributeValueConverter + /// + /// + /// + public ObjectComplexAttributeValueConverter(PropertyInfo property, bool isToMany) + { + _property = property; + _isToMany = isToMany; + } + + public JToken GetValue(object resource) + { + var value = _property.GetValue(resource); + if (value == null) return null; + return _isToMany ? (JToken)JArray.FromObject(value) : JObject.FromObject(value); + } + + public void SetValue(object resource, JToken value) + { + var deserialized = value?.ToObject(_property.PropertyType); + _property.SetValue(resource, deserialized); + } + } +} \ No newline at end of file diff --git a/JSONAPI/Core/ResourceTypeRegistrar.cs b/JSONAPI/Core/ResourceTypeRegistrar.cs index 6ae7b057..55337a07 100644 --- a/JSONAPI/Core/ResourceTypeRegistrar.cs +++ b/JSONAPI/Core/ResourceTypeRegistrar.cs @@ -101,7 +101,15 @@ protected virtual IAttributeValueConverter GetValueConverterForProperty(Property { var serializeAsComplexAttribute = prop.GetCustomAttribute(); if (serializeAsComplexAttribute != null) - return new ComplexAttributeValueConverter(prop); + { + if (prop.PropertyType == typeof (string)) + return new ComplexAttributeValueConverter(prop); + + var isToMany = + prop.PropertyType.IsArray || + (prop.PropertyType.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && prop.PropertyType.IsGenericType); + return new ObjectComplexAttributeValueConverter(prop, isToMany); + } if (prop.PropertyType == typeof(DateTime)) return new DateTimeAttributeValueConverter(prop, false); @@ -150,7 +158,7 @@ protected virtual ResourceTypeField CreateResourceTypeField(PropertyInfo prop) var type = prop.PropertyType; - if (prop.PropertyType.CanWriteAsJsonApiAttribute()) + if (prop.PropertyType.CanWriteAsJsonApiAttribute() || prop.GetCustomAttributes().Any()) { var converter = GetValueConverterForProperty(prop); return new ResourceTypeAttribute(converter, prop, jsonKey); diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index cfb6b1d5..2412f4cc 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -81,6 +81,7 @@ + From c573d2653b33b77f00a87122ce529e48e77601a1 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 29 Mar 2016 22:49:58 -0400 Subject: [PATCH 166/186] do not serialize related data that already exists in the primary data --- .../RegistryDrivenResourceCollectionDocumentBuilder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs index 031f9f9b..0508dd4a 100644 --- a/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/RegistryDrivenResourceCollectionDocumentBuilder.cs @@ -27,8 +27,14 @@ public IResourceCollectionDocument BuildDocument(IEnumerable pri primaryData.Select(d => (IResourceObject)CreateResourceObject(d, idDictionariesByType, null, includePathExpressions, linkBaseUrl, resourceMetadata)) .ToArray(); + var primaryResourceIdentifiers = primaryDataResources.Select(r => new { r.Id, r.Type }).ToArray(); + var relatedData = idDictionariesByType.Values.SelectMany(d => d.Values).Cast().ToArray(); - var document = new ResourceCollectionDocument(primaryDataResources, relatedData, metadata); + var relatedDataNotInPrimaryData = relatedData + .Where(r => !primaryResourceIdentifiers.Any(pri => pri.Id == r.Id && pri.Type == r.Type)) + .ToArray(); + + var document = new ResourceCollectionDocument(primaryDataResources, relatedDataNotInPrimaryData, metadata); return document; } } From 949953e3a325336233a4c563f6dbdda4e4f81dcf Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Mon, 4 Apr 2016 11:34:29 -0400 Subject: [PATCH 167/186] allow property injection for formatters --- JSONAPI/Json/LinkFormatter.cs | 29 ++++++- JSONAPI/Json/RelationshipObjectFormatter.cs | 65 ++++++++++++--- .../ResourceCollectionDocumentFormatter.cs | 61 +++++++++++--- JSONAPI/Json/ResourceObjectFormatter.cs | 80 ++++++++++++++++--- .../Json/SingleResourceDocumentFormatter.cs | 56 +++++++++++-- 5 files changed, 254 insertions(+), 37 deletions(-) diff --git a/JSONAPI/Json/LinkFormatter.cs b/JSONAPI/Json/LinkFormatter.cs index 7c88dae3..8f0331b5 100644 --- a/JSONAPI/Json/LinkFormatter.cs +++ b/JSONAPI/Json/LinkFormatter.cs @@ -10,10 +10,18 @@ namespace JSONAPI.Json /// public class LinkFormatter : ILinkFormatter { - private readonly IMetadataFormatter _metadataFormatter; + private IMetadataFormatter _metadataFormatter; private const string HrefKeyName = "href"; private const string MetaKeyName = "meta"; + /// + /// Constructs a LinkFormatter + /// + public LinkFormatter() + { + + } + /// /// Constructs a LinkFormatter /// @@ -23,6 +31,23 @@ public LinkFormatter(IMetadataFormatter metadataFormatter) _metadataFormatter = metadataFormatter; } + /// + /// The formatter to use for metadata on the link object + /// + /// + public IMetadataFormatter MetadataFormatter + { + get + { + return _metadataFormatter ?? (_metadataFormatter = new MetadataFormatter()); + } + set + { + if (_metadataFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _metadataFormatter = value; + } + } + public Task Serialize(ILink link, JsonWriter writer) { if (link.Metadata == null) @@ -35,7 +60,7 @@ public Task Serialize(ILink link, JsonWriter writer) writer.WritePropertyName(HrefKeyName); writer.WriteValue(link.Href); writer.WritePropertyName(MetaKeyName); - _metadataFormatter.Serialize(link.Metadata, writer); + MetadataFormatter.Serialize(link.Metadata, writer); writer.WriteEndObject(); } return Task.FromResult(0); diff --git a/JSONAPI/Json/RelationshipObjectFormatter.cs b/JSONAPI/Json/RelationshipObjectFormatter.cs index e598894c..a41c8411 100644 --- a/JSONAPI/Json/RelationshipObjectFormatter.cs +++ b/JSONAPI/Json/RelationshipObjectFormatter.cs @@ -16,9 +16,17 @@ public class RelationshipObjectFormatter : IRelationshipObjectFormatter private const string LinkageKeyName = "data"; private const string MetaKeyName = "meta"; - private readonly ILinkFormatter _linkFormatter; - private readonly IResourceLinkageFormatter _resourceLinkageFormatter; - private readonly IMetadataFormatter _metadataFormatter; + private ILinkFormatter _linkFormatter; + private IResourceLinkageFormatter _resourceLinkageFormatter; + private IMetadataFormatter _metadataFormatter; + + /// + /// Creates a new RelationshipObjectFormatter + /// + public RelationshipObjectFormatter () + { + + } /// /// Creates a new RelationshipObjectFormatter @@ -30,6 +38,45 @@ public RelationshipObjectFormatter(ILinkFormatter linkFormatter, IResourceLinkag _metadataFormatter = metadataFormatter; } + private ILinkFormatter LinkFormatter + { + get + { + return _linkFormatter ?? (_linkFormatter = new LinkFormatter()); + } + set + { + if (_linkFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _linkFormatter = value; + } + } + + private IResourceLinkageFormatter ResourceLinkageFormatter + { + get + { + return _resourceLinkageFormatter ?? (_resourceLinkageFormatter = new ResourceLinkageFormatter()); + } + set + { + if (_resourceLinkageFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _resourceLinkageFormatter = value; + } + } + + private IMetadataFormatter MetadataFormatter + { + get + { + return _metadataFormatter ?? (_metadataFormatter = new MetadataFormatter()); + } + set + { + if (_metadataFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _metadataFormatter = value; + } + } + public Task Serialize(IRelationshipObject relationshipObject, JsonWriter writer) { if (relationshipObject.Metadata == null && relationshipObject.SelfLink == null && @@ -60,12 +107,12 @@ protected virtual void SerializeLinks(IRelationshipObject relationshipObject, Js if (relationshipObject.SelfLink != null) { writer.WritePropertyName(SelfLinkKeyName); - _linkFormatter.Serialize(relationshipObject.SelfLink, writer); + LinkFormatter.Serialize(relationshipObject.SelfLink, writer); } if (relationshipObject.RelatedResourceLink != null) { writer.WritePropertyName(RelatedLinkKeyName); - _linkFormatter.Serialize(relationshipObject.RelatedResourceLink, writer); + LinkFormatter.Serialize(relationshipObject.RelatedResourceLink, writer); } writer.WriteEndObject(); @@ -80,7 +127,7 @@ protected virtual void SerializeLinkage(IRelationshipObject relationshipObject, if (relationshipObject.Linkage != null) { writer.WritePropertyName(LinkageKeyName); - _resourceLinkageFormatter.Serialize(relationshipObject.Linkage, writer); + ResourceLinkageFormatter.Serialize(relationshipObject.Linkage, writer); } } @@ -92,7 +139,7 @@ protected virtual void SerializeMetadata(IRelationshipObject relationshipObject, if (relationshipObject.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataFormatter.Serialize(relationshipObject.Metadata, writer); + MetadataFormatter.Serialize(relationshipObject.Metadata, writer); } } @@ -114,10 +161,10 @@ public async Task Deserialize(JsonReader reader, string cur switch (propertyName) { case LinkageKeyName: - linkage = await _resourceLinkageFormatter.Deserialize(reader, currentPath + "/" + LinkageKeyName); + linkage = await ResourceLinkageFormatter.Deserialize(reader, currentPath + "/" + LinkageKeyName); break; case MetaKeyName: - metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await MetadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; } } diff --git a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs index db2d40e9..79cd75d8 100644 --- a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs +++ b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -11,14 +12,22 @@ namespace JSONAPI.Json /// public class ResourceCollectionDocumentFormatter : IResourceCollectionDocumentFormatter { - private readonly IResourceObjectFormatter _resourceObjectFormatter; - private readonly IMetadataFormatter _metadataFormatter; + private IResourceObjectFormatter _resourceObjectFormatter; + private IMetadataFormatter _metadataFormatter; private const string PrimaryDataKeyName = "data"; private const string RelatedDataKeyName = "included"; private const string MetaKeyName = "meta"; - + + /// + /// Creates a ResourceCollectionDocumentFormatter + /// + public ResourceCollectionDocumentFormatter() + { + + } + /// - /// Creates a SingleResourceDocumentFormatter + /// Creates a ResourceCollectionDocumentFormatter /// /// /// @@ -28,6 +37,40 @@ public ResourceCollectionDocumentFormatter(IResourceObjectFormatter resourceObje _metadataFormatter = metadataFormatter; } + /// + /// The formatter to use for resource objects found in this document + /// + /// + public IResourceObjectFormatter ResourceObjectFormatter + { + get + { + return _resourceObjectFormatter ?? (_resourceObjectFormatter = new ResourceObjectFormatter()); + } + set + { + if (_resourceObjectFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _resourceObjectFormatter = value; + } + } + + /// + /// The formatter to use for document-level metadata + /// + /// + public IMetadataFormatter MetadataFormatter + { + get + { + return _metadataFormatter ?? (_metadataFormatter = new MetadataFormatter()); + } + set + { + if (_metadataFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _metadataFormatter = value; + } + } + public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) { writer.WriteStartObject(); @@ -37,7 +80,7 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) writer.WriteStartArray(); foreach (var resourceObject in document.PrimaryData) { - _resourceObjectFormatter.Serialize(resourceObject, writer); + ResourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); @@ -47,7 +90,7 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) writer.WriteStartArray(); foreach (var resourceObject in document.RelatedData) { - _resourceObjectFormatter.Serialize(resourceObject, writer); + ResourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); } @@ -55,7 +98,7 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) if (document.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataFormatter.Serialize(document.Metadata, writer); + MetadataFormatter.Serialize(document.Metadata, writer); } writer.WriteEndObject(); @@ -91,7 +134,7 @@ public async Task Deserialize(JsonReader reader, st primaryData = await DeserializePrimaryData(reader, currentPath + "/" + PrimaryDataKeyName); break; case MetaKeyName: - metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await MetadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; default: reader.Skip(); @@ -115,7 +158,7 @@ private async Task DeserializePrimaryData(JsonReader reader, if (reader.TokenType == JsonToken.EndArray) break; - var resourceObject = await _resourceObjectFormatter.Deserialize(reader, currentPath + "/" + index); + var resourceObject = await ResourceObjectFormatter.Deserialize(reader, currentPath + "/" + index); primaryData.Add(resourceObject); index++; diff --git a/JSONAPI/Json/ResourceObjectFormatter.cs b/JSONAPI/Json/ResourceObjectFormatter.cs index 076f0e0f..18907217 100644 --- a/JSONAPI/Json/ResourceObjectFormatter.cs +++ b/JSONAPI/Json/ResourceObjectFormatter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using JSONAPI.Documents; @@ -12,9 +13,9 @@ namespace JSONAPI.Json /// public class ResourceObjectFormatter : IResourceObjectFormatter { - private readonly IRelationshipObjectFormatter _relationshipObjectFormatter; - private readonly ILinkFormatter _linkFormatter; - private readonly IMetadataFormatter _metadataFormatter; + private IRelationshipObjectFormatter _relationshipObjectFormatter; + private ILinkFormatter _linkFormatter; + private IMetadataFormatter _metadataFormatter; private const string TypeKeyName = "type"; private const string IdKeyName = "id"; private const string AttributesKeyName = "attributes"; @@ -24,7 +25,15 @@ public class ResourceObjectFormatter : IResourceObjectFormatter private const string SelfLinkKeyName = "self"; /// - /// Constructs a new resourceObjectFormatter + /// Constructs a new ResourceObjectFormatter + /// + public ResourceObjectFormatter() + { + + } + + /// + /// Constructs a new ResourceObjectFormatter /// /// The formatter to use for relationship objects /// The formatter to use for links @@ -36,6 +45,57 @@ public ResourceObjectFormatter(IRelationshipObjectFormatter relationshipObjectFo _metadataFormatter = metadataFormatter; } + /// + /// The formatter to use for relationship objects belonging to this resource + /// + /// + public IRelationshipObjectFormatter RelationshipObjectFormatter + { + get + { + return _relationshipObjectFormatter ?? (_relationshipObjectFormatter = new RelationshipObjectFormatter()); + } + set + { + if (_relationshipObjectFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _relationshipObjectFormatter = value; + } + } + + /// + /// The formatter to use for links + /// + /// + public ILinkFormatter LinkFormatter + { + get + { + return _linkFormatter ?? (_linkFormatter = new LinkFormatter()); + } + set + { + if (_linkFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _linkFormatter = value; + } + } + + /// + /// The formatter to use for metadata that belongs to this resource object + /// + /// + public IMetadataFormatter MetadataFormatter + { + get + { + return _metadataFormatter ?? (_metadataFormatter = new MetadataFormatter()); + } + set + { + if (_metadataFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _metadataFormatter = value; + } + } + public Task Serialize(IResourceObject resourceObject, JsonWriter writer) { if (resourceObject == null) @@ -88,7 +148,7 @@ public Task Serialize(IResourceObject resourceObject, JsonWriter writer) { if (relationship.Value == null) continue; writer.WritePropertyName(relationship.Key); - _relationshipObjectFormatter.Serialize(relationship.Value, writer); + RelationshipObjectFormatter.Serialize(relationship.Value, writer); } writer.WriteEndObject(); } @@ -99,14 +159,14 @@ public Task Serialize(IResourceObject resourceObject, JsonWriter writer) writer.WritePropertyName(LinksKeyName); writer.WriteStartObject(); writer.WritePropertyName(SelfLinkKeyName); - _linkFormatter.Serialize(resourceObject.SelfLink, writer); + LinkFormatter.Serialize(resourceObject.SelfLink, writer); writer.WriteEndObject(); } if (resourceObject.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataFormatter.Serialize(resourceObject.Metadata, writer); + MetadataFormatter.Serialize(resourceObject.Metadata, writer); } writer.WriteEndObject(); @@ -142,7 +202,7 @@ public async Task Deserialize(JsonReader reader, string current id = (string) reader.Value; break; case MetaKeyName: - metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await MetadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; case AttributesKeyName: attributes = DeserializeAttributes(reader, currentPath + "/" + AttributesKeyName); @@ -202,7 +262,7 @@ private async Task> DeserializeRelation var relationshipName = (string)reader.Value; reader.Read(); - var relationship = await _relationshipObjectFormatter.Deserialize(reader, currentPath + "/" + relationshipName); + var relationship = await RelationshipObjectFormatter.Deserialize(reader, currentPath + "/" + relationshipName); relationships.Add(relationshipName, relationship); } diff --git a/JSONAPI/Json/SingleResourceDocumentFormatter.cs b/JSONAPI/Json/SingleResourceDocumentFormatter.cs index e0414c11..816e963c 100644 --- a/JSONAPI/Json/SingleResourceDocumentFormatter.cs +++ b/JSONAPI/Json/SingleResourceDocumentFormatter.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Threading.Tasks; using JSONAPI.Documents; @@ -10,12 +11,19 @@ namespace JSONAPI.Json /// public class SingleResourceDocumentFormatter : ISingleResourceDocumentFormatter { - private readonly IResourceObjectFormatter _resourceObjectFormatter; - private readonly IMetadataFormatter _metadataFormatter; + private IResourceObjectFormatter _resourceObjectFormatter; + private IMetadataFormatter _metadataFormatter; private const string PrimaryDataKeyName = "data"; private const string RelatedDataKeyName = "included"; private const string MetaKeyName = "meta"; + /// + /// Creates a SingleResourceDocumentFormatter with default parameters + /// + public SingleResourceDocumentFormatter() + { + } + /// /// Creates a SingleResourceDocumentFormatter /// @@ -27,13 +35,47 @@ public SingleResourceDocumentFormatter(IResourceObjectFormatter resourceObjectFo _metadataFormatter = metadataFormatter; } + /// + /// The formatter to use for resource objects found in this document + /// + /// + public IResourceObjectFormatter ResourceObjectFormatter + { + get + { + return _resourceObjectFormatter ?? (_resourceObjectFormatter = new ResourceObjectFormatter()); + } + set + { + if (_resourceObjectFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _resourceObjectFormatter = value; + } + } + + /// + /// The formatter to use for document-level metadata + /// + /// + public IMetadataFormatter MetadataFormatter + { + get + { + return _metadataFormatter ?? (_metadataFormatter = new MetadataFormatter()); + } + set + { + if (_metadataFormatter != null) throw new InvalidOperationException("This property can only be set once."); + _metadataFormatter = value; + } + } + public Task Serialize(ISingleResourceDocument document, JsonWriter writer) { writer.WriteStartObject(); writer.WritePropertyName(PrimaryDataKeyName); - _resourceObjectFormatter.Serialize(document.PrimaryData, writer); + ResourceObjectFormatter.Serialize(document.PrimaryData, writer); if (document.RelatedData != null && document.RelatedData.Any()) { @@ -41,7 +83,7 @@ public Task Serialize(ISingleResourceDocument document, JsonWriter writer) writer.WriteStartArray(); foreach (var resourceObject in document.RelatedData) { - _resourceObjectFormatter.Serialize(resourceObject, writer); + ResourceObjectFormatter.Serialize(resourceObject, writer); } writer.WriteEndArray(); } @@ -49,7 +91,7 @@ public Task Serialize(ISingleResourceDocument document, JsonWriter writer) if (document.Metadata != null) { writer.WritePropertyName(MetaKeyName); - _metadataFormatter.Serialize(document.Metadata, writer); + MetadataFormatter.Serialize(document.Metadata, writer); } writer.WriteEndObject(); @@ -83,7 +125,7 @@ public async Task Deserialize(JsonReader reader, string primaryData = await DeserializePrimaryData(reader, currentPath + "/" + PrimaryDataKeyName); break; case MetaKeyName: - metadata = await _metadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); + metadata = await MetadataFormatter.Deserialize(reader, currentPath + "/" + MetaKeyName); break; default: reader.Skip(); @@ -98,7 +140,7 @@ private async Task DeserializePrimaryData(JsonReader reader, st { if (reader.TokenType == JsonToken.Null) return null; - var primaryData = await _resourceObjectFormatter.Deserialize(reader, currentPath); + var primaryData = await ResourceObjectFormatter.Deserialize(reader, currentPath); return primaryData; } } From 9e305ecff354035c8b1fc67c792ffff2daa5457e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 12 Apr 2016 19:16:35 -0400 Subject: [PATCH 168/186] add HttpRequestMessage to DeleteRecord handler (#107) --- .../DocumentMaterializers/StarshipDocumentMaterializer.cs | 2 +- .../Http/EntityFrameworkDocumentMaterializer.cs | 2 +- JSONAPI/Http/IDocumentMaterializer.cs | 2 +- JSONAPI/Http/JsonApiController.cs | 2 +- JSONAPI/Http/MappedDocumentMaterializer.cs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs index 17cde210..9eb3fa47 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs @@ -62,7 +62,7 @@ public override Task UpdateRecord(string id, ISingleRes throw new NotImplementedException(); } - public override Task DeleteRecord(string id, CancellationToken cancellationToken) + public override Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index cbe623ad..db2aaf29 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -86,7 +86,7 @@ public virtual async Task UpdateRecord(string id, ISing return returnDocument; } - public virtual async Task DeleteRecord(string id, CancellationToken cancellationToken) + public virtual async Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var singleResource = await _dbContext.Set().FindAsync(cancellationToken, id); _dbContext.Set().Remove(singleResource); diff --git a/JSONAPI/Http/IDocumentMaterializer.cs b/JSONAPI/Http/IDocumentMaterializer.cs index efe4d9bb..b4eb4bce 100644 --- a/JSONAPI/Http/IDocumentMaterializer.cs +++ b/JSONAPI/Http/IDocumentMaterializer.cs @@ -39,6 +39,6 @@ Task UpdateRecord(string id, ISingleResourceDocument re /// /// Deletes the record corresponding to the given id. /// - Task DeleteRecord(string id, CancellationToken cancellationToken); + Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/JSONAPI/Http/JsonApiController.cs b/JSONAPI/Http/JsonApiController.cs index 78f5b66a..09deb1b1 100644 --- a/JSONAPI/Http/JsonApiController.cs +++ b/JSONAPI/Http/JsonApiController.cs @@ -78,7 +78,7 @@ public virtual async Task Patch(string resourceType, string i public virtual async Task Delete(string resourceType, string id, CancellationToken cancellationToken) { var materializer = _documentMaterializerLocator.GetMaterializerByResourceTypeName(resourceType); - var document = await materializer.DeleteRecord(id, cancellationToken); + var document = await materializer.DeleteRecord(id, Request, cancellationToken); return Ok(document); } } diff --git a/JSONAPI/Http/MappedDocumentMaterializer.cs b/JSONAPI/Http/MappedDocumentMaterializer.cs index ee002270..4c53c2e0 100644 --- a/JSONAPI/Http/MappedDocumentMaterializer.cs +++ b/JSONAPI/Http/MappedDocumentMaterializer.cs @@ -102,8 +102,8 @@ public abstract Task UpdateRecord(string id, ISingleRes HttpRequestMessage request, CancellationToken cancellationToken); - public abstract Task DeleteRecord(string id, CancellationToken cancellationToken); - + public abstract Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken); + /// /// Returns a list of property paths to be included when constructing a query for this resource type /// From c96ec02ec28ab4cb0fe0a0a9e3901178b5b6d4a3 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 26 Apr 2016 14:09:29 -0400 Subject: [PATCH 169/186] allow filtering by ID --- .../DefaultFilteringTransformerTests.cs | 15 +++++ .../DefaultFilteringTransformer.cs | 58 ++++++++++--------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 39850fc4..6b458e2a 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -559,6 +559,21 @@ private Dummy[] GetArray(string uri) #region String + [TestMethod] + public void Filters_by_matching_string_id_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[id]=100"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + + [TestMethod] + public void Filters_by_missing_string_id_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[id]="); + returnedArray.Length.Should().Be(0); + } + [TestMethod] public void Filters_by_matching_string_property() { diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index 88e09605..f449d1bb 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -2,11 +2,8 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.Net; using System.Net.Http; using System.Reflection; -using System.Web.Http; -using JSONAPI.ActionFilters; using JSONAPI.Core; using JSONAPI.Documents.Builders; @@ -83,41 +80,50 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress throw JsonApiException.CreateForBadRequest("No registration exists for the specified type"); } - var resourceTypeField = registration.GetFieldByName(filterField); - if (resourceTypeField == null) - throw JsonApiException.CreateForBadRequest( - string.Format("No attribute {0} exists on the specified type.", filterField)); + var expr = GetPredicate(filterField, registration, param, queryPair.Value); + workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); + } - var queryValue = queryPair.Value; - if (string.IsNullOrWhiteSpace(queryValue)) - queryValue = null; + return workingExpr ?? Expression.Constant(true); // No filters, so return everything + } - Expression expr = null; + private Expression GetPredicate(string filterField, IResourceTypeRegistration registration, ParameterExpression param, string queryValue) + { + if (filterField == "id") + return GetPredicateBodyForProperty(registration.IdProperty, queryValue, param); - // See if it is a field property - var fieldModelProperty = resourceTypeField as ResourceTypeAttribute; - if (fieldModelProperty != null) - expr = GetPredicateBodyForField(fieldModelProperty, queryValue, param); + var resourceTypeField = registration.GetFieldByName(filterField); + if (resourceTypeField == null) + throw JsonApiException.CreateForBadRequest( + string.Format("No attribute {0} exists on the specified type.", filterField)); - // See if it is a relationship property - var relationshipModelProperty = resourceTypeField as ResourceTypeRelationship; - if (relationshipModelProperty != null) - expr = GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); + if (string.IsNullOrWhiteSpace(queryValue)) + queryValue = null; - if (expr == null) throw JsonApiException.CreateForBadRequest( - string.Format("The attribute {0} is unsupported for filtering.", filterField)); + // See if it is a field property + var fieldModelProperty = resourceTypeField as ResourceTypeAttribute; + if (fieldModelProperty != null) + return GetPredicateBodyForField(fieldModelProperty, queryValue, param); - workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); - } + // See if it is a relationship property + var relationshipModelProperty = resourceTypeField as ResourceTypeRelationship; + if (relationshipModelProperty != null) + return GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); - return workingExpr ?? Expression.Constant(true); // No filters, so return everything + throw JsonApiException.CreateForBadRequest( + string.Format("The attribute {0} is unsupported for filtering.", filterField)); + } + + private Expression GetPredicateBodyForField(ResourceTypeAttribute resourceTypeAttribute, string queryValue, + ParameterExpression param) + { + return GetPredicateBodyForProperty(resourceTypeAttribute.Property, queryValue, param); } // ReSharper disable once FunctionComplexityOverflow // TODO: should probably break this method up - private Expression GetPredicateBodyForField(ResourceTypeAttribute resourceTypeAttribute, string queryValue, ParameterExpression param) + private Expression GetPredicateBodyForProperty(PropertyInfo prop, string queryValue, ParameterExpression param) { - var prop = resourceTypeAttribute.Property; var propertyType = prop.PropertyType; Expression expr; From 7f6431fb09b2c61878d252afe39fde51d09ef5a6 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 29 Apr 2016 15:42:51 -0400 Subject: [PATCH 170/186] add null checks in ResourceTypeRegistration --- JSONAPI/Core/ResourceTypeRegistration.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Core/ResourceTypeRegistration.cs b/JSONAPI/Core/ResourceTypeRegistration.cs index 5eef1b6c..7521b428 100644 --- a/JSONAPI/Core/ResourceTypeRegistration.cs +++ b/JSONAPI/Core/ResourceTypeRegistration.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JSONAPI.Http; namespace JSONAPI.Core { @@ -22,6 +21,9 @@ internal ResourceTypeRegistration(Type type, PropertyInfo idProperty, string res Func filterByIdExpressionFactory, Func sortByIdExpressionFactory) { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (idProperty == null) throw new ArgumentNullException(nameof(idProperty)); + if (resourceTypeName == null) throw new ArgumentNullException(nameof(resourceTypeName)); IdProperty = idProperty; Type = type; ResourceTypeName = resourceTypeName; @@ -44,11 +46,13 @@ internal ResourceTypeRegistration(Type type, PropertyInfo idProperty, string res public string GetIdForResource(object resource) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); return IdProperty.GetValue(resource).ToString(); } public void SetIdForResource(object resource, string id) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); IdProperty.SetValue(resource, id); // TODO: handle classes with non-string ID types } From 6a2f9682b50a2e1f136bfca0da3ef57f04a8b79f Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 29 Apr 2016 15:56:47 -0400 Subject: [PATCH 171/186] provide more helpful error when the resource ID is null --- JSONAPI/Core/ResourceTypeRegistration.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/JSONAPI/Core/ResourceTypeRegistration.cs b/JSONAPI/Core/ResourceTypeRegistration.cs index 7521b428..ad35ef2f 100644 --- a/JSONAPI/Core/ResourceTypeRegistration.cs +++ b/JSONAPI/Core/ResourceTypeRegistration.cs @@ -47,7 +47,9 @@ internal ResourceTypeRegistration(Type type, PropertyInfo idProperty, string res public string GetIdForResource(object resource) { if (resource == null) throw new ArgumentNullException(nameof(resource)); - return IdProperty.GetValue(resource).ToString(); + var resourceId = IdProperty.GetValue(resource); + if (resourceId == null) throw new ArgumentException($"The ID for the provided `{ResourceTypeName}` resource is null."); + return resourceId.ToString(); } public void SetIdForResource(object resource, string id) From 9768449bffa7fc51f1cf5a4506d538c7bbc4bc8a Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 8 Jun 2016 18:47:37 -0400 Subject: [PATCH 172/186] add non-generic version of IEphemeralRelatedResourceReader that has a generic method instead --- .../Core/EphemeralRelatedResourceReader.cs | 37 +++++++++++++++---- .../Core/IEphemeralRelatedResourceReader.cs | 18 ++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/JSONAPI/Core/EphemeralRelatedResourceReader.cs b/JSONAPI/Core/EphemeralRelatedResourceReader.cs index 9ef242d6..7f40ef7f 100644 --- a/JSONAPI/Core/EphemeralRelatedResourceReader.cs +++ b/JSONAPI/Core/EphemeralRelatedResourceReader.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using JSONAPI.Documents; using JSONAPI.Json; @@ -13,11 +11,34 @@ namespace JSONAPI.Core /// Populates property values on an ephemeral resource /// /// + [Obsolete] public class EphemeralRelatedResourceReader : IEphemeralRelatedResourceReader + { + private readonly IEphemeralRelatedResourceReader _ephemeralRelatedResourceReader; + + /// + /// Creates a new EphemeralRelatedResourceReader + /// + /// + public EphemeralRelatedResourceReader(IEphemeralRelatedResourceReader ephemeralRelatedResourceReader) + { + _ephemeralRelatedResourceReader = ephemeralRelatedResourceReader; + } + + public void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject) + { + _ephemeralRelatedResourceReader.SetProperty(ephemeralResource, jsonKey, relationshipObject); + } + } + + /// + /// Populates property values on an ephemeral resource + /// + /// + public class EphemeralRelatedResourceReader : IEphemeralRelatedResourceReader { private readonly IResourceTypeRegistry _resourceTypeRegistry; private readonly IEphemeralRelatedResourceCreator _ephemeralRelatedResourceCreator; - private readonly Lazy _resourceTypeRegistration; private readonly MethodInfo _openSetToManyRelationshipValueMethod; /// @@ -29,14 +50,14 @@ public EphemeralRelatedResourceReader(IResourceTypeRegistry resourceTypeRegistry { _resourceTypeRegistry = resourceTypeRegistry; _ephemeralRelatedResourceCreator = ephemeralRelatedResourceCreator; - _resourceTypeRegistration = new Lazy(() => _resourceTypeRegistry.GetRegistrationForType(typeof(T))); _openSetToManyRelationshipValueMethod = GetType() .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); } - public void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject) + public void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject) { - var relationship = _resourceTypeRegistration.Value.GetFieldByName(jsonKey) as ResourceTypeRelationship; + var resourceTypeRegistration = new Lazy(() => _resourceTypeRegistry.GetRegistrationForType(typeof(T))); + var relationship = resourceTypeRegistration.Value.GetFieldByName(jsonKey) as ResourceTypeRelationship; if (relationship == null) return; if (relationship.IsToMany) @@ -45,7 +66,7 @@ public void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject SetPropertyForToOneRelationship(ephemeralResource, relationship, relationshipObject.Linkage); } - protected virtual void SetPropertyForToOneRelationship(T ephemeralResource, ResourceTypeRelationship relationship, IResourceLinkage linkage) + protected virtual void SetPropertyForToOneRelationship(T ephemeralResource, ResourceTypeRelationship relationship, IResourceLinkage linkage) { if (linkage == null) throw new DeserializationException("Missing linkage for to-one relationship", @@ -70,7 +91,7 @@ protected virtual void SetPropertyForToOneRelationship(T ephemeralResource, Reso } } - protected virtual void SetPropertyForToManyRelationship(T ephemeralResource, ResourceTypeRelationship relationship, + protected virtual void SetPropertyForToManyRelationship(T ephemeralResource, ResourceTypeRelationship relationship, IResourceLinkage linkage) { if (linkage == null) diff --git a/JSONAPI/Core/IEphemeralRelatedResourceReader.cs b/JSONAPI/Core/IEphemeralRelatedResourceReader.cs index 827b455a..95a8fade 100644 --- a/JSONAPI/Core/IEphemeralRelatedResourceReader.cs +++ b/JSONAPI/Core/IEphemeralRelatedResourceReader.cs @@ -1,4 +1,5 @@ -using JSONAPI.Documents; +using System; +using JSONAPI.Documents; namespace JSONAPI.Core { @@ -6,6 +7,7 @@ namespace JSONAPI.Core /// Populates property values on an ephemeral resource from a relationship object /// /// + [Obsolete] public interface IEphemeralRelatedResourceReader { /// @@ -16,4 +18,18 @@ public interface IEphemeralRelatedResourceReader /// void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject); } + + /// + /// Populates property values on an ephemeral resource from a relationship object + /// + public interface IEphemeralRelatedResourceReader + { + /// + /// Sets the property on the ephemeral resource that corresponds to the given property + /// + /// + /// + /// + void SetProperty(T ephemeralResource, string jsonKey, IRelationshipObject relationshipObject); + } } \ No newline at end of file From 8a2477f7bff02c51a892e869aeaa5714352a0b15 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 24 Jun 2016 15:56:08 -0400 Subject: [PATCH 173/186] add attribute to allow not serializing a relationship --- JSONAPI/Attributes/LinkSettingsAttribute.cs | 25 +++++++++++++++++++ JSONAPI/Core/ResourceTypeRegistrar.cs | 15 +++++++---- JSONAPI/Core/ResourceTypeRelationship.cs | 17 ++++++++++++- .../Core/ToManyResourceTypeRelationship.cs | 4 +-- JSONAPI/Core/ToOneResourceTypeRelationship.cs | 4 +-- JSONAPI/Documents/DefaultLinkConventions.cs | 4 +++ JSONAPI/JSONAPI.csproj | 1 + 7 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 JSONAPI/Attributes/LinkSettingsAttribute.cs diff --git a/JSONAPI/Attributes/LinkSettingsAttribute.cs b/JSONAPI/Attributes/LinkSettingsAttribute.cs new file mode 100644 index 00000000..4c2b96cf --- /dev/null +++ b/JSONAPI/Attributes/LinkSettingsAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace JSONAPI.Attributes +{ + /// + /// This attribute should be added to a property to override DefaultLinkConventions's default + /// behavior for serializing links. + /// + [AttributeUsage(AttributeTargets.Property)] + public class LinkSettingsAttribute : Attribute + { + internal bool SerializeRelationshipLink; + + internal bool SerializeRelatedResourceLink; + + /// + /// Creates a new LinkSettingsAttribute. + /// + public LinkSettingsAttribute(bool serializeRelationshipLink = true, bool serializeRelatedResourceLink = true) + { + SerializeRelationshipLink = serializeRelationshipLink; + SerializeRelatedResourceLink = serializeRelatedResourceLink; + } + } +} diff --git a/JSONAPI/Core/ResourceTypeRegistrar.cs b/JSONAPI/Core/ResourceTypeRegistrar.cs index 55337a07..742e025c 100644 --- a/JSONAPI/Core/ResourceTypeRegistrar.cs +++ b/JSONAPI/Core/ResourceTypeRegistrar.cs @@ -4,7 +4,6 @@ using System.Linq.Expressions; using System.Reflection; using JSONAPI.Attributes; -using JSONAPI.Configuration; using JSONAPI.Extensions; using Newtonsoft.Json; @@ -165,17 +164,23 @@ protected virtual ResourceTypeField CreateResourceTypeField(PropertyInfo prop) } var selfLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); - var selfLinkTemplate = selfLinkTemplateAttribute == null ? null : selfLinkTemplateAttribute.TemplateString; + var selfLinkTemplate = selfLinkTemplateAttribute?.TemplateString; var relatedResourceLinkTemplateAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); - var relatedResourceLinkTemplate = relatedResourceLinkTemplateAttribute == null ? null : relatedResourceLinkTemplateAttribute.TemplateString; + var relatedResourceLinkTemplate = relatedResourceLinkTemplateAttribute?.TemplateString; var isToMany = type.IsArray || (type.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && type.IsGenericType); - if (!isToMany) return new ToOneResourceTypeRelationship(prop, jsonKey, type, selfLinkTemplate, relatedResourceLinkTemplate); + var linkSettingsAttribute = prop.GetCustomAttributes().OfType().FirstOrDefault(); + var serializeRelationshipLink = linkSettingsAttribute == null || linkSettingsAttribute.SerializeRelationshipLink; + var serializeRelatedResourceLink = linkSettingsAttribute == null || linkSettingsAttribute.SerializeRelatedResourceLink; + + if (!isToMany) return new ToOneResourceTypeRelationship(prop, jsonKey, type, selfLinkTemplate, relatedResourceLinkTemplate, + serializeRelationshipLink, serializeRelatedResourceLink); var relatedType = type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType(); - return new ToManyResourceTypeRelationship(prop, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate); + return new ToManyResourceTypeRelationship(prop, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, + serializeRelationshipLink, serializeRelatedResourceLink); } /// diff --git a/JSONAPI/Core/ResourceTypeRelationship.cs b/JSONAPI/Core/ResourceTypeRelationship.cs index d5ba5576..2955d94d 100644 --- a/JSONAPI/Core/ResourceTypeRelationship.cs +++ b/JSONAPI/Core/ResourceTypeRelationship.cs @@ -9,13 +9,16 @@ namespace JSONAPI.Core public abstract class ResourceTypeRelationship : ResourceTypeField { internal ResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate, bool isToMany) + string selfLinkTemplate, string relatedResourceLinkTemplate, bool isToMany, + bool serializeRelationshipLink = true, bool serializeRelatedResourceLink = true) : base(property, jsonKey) { RelatedType = relatedType; SelfLinkTemplate = selfLinkTemplate; RelatedResourceLinkTemplate = relatedResourceLinkTemplate; IsToMany = isToMany; + SerializeRelationshipLink = serializeRelationshipLink; + SerializeRelatedResourceLink = serializeRelatedResourceLink; } /// @@ -41,5 +44,17 @@ internal ResourceTypeRelationship(PropertyInfo property, string jsonKey, Type re /// relationship belongs to. /// public string RelatedResourceLinkTemplate { get; private set; } + + /// + /// Whether to include a link to the relationship URL when serializing relationship objects for + /// this relationship + /// + public bool SerializeRelationshipLink { get; private set; } + + /// + /// Whether to include a link to the related resource URL when serializing relationship objects for + /// this relationship + /// + public bool SerializeRelatedResourceLink { get; private set; } } } \ No newline at end of file diff --git a/JSONAPI/Core/ToManyResourceTypeRelationship.cs b/JSONAPI/Core/ToManyResourceTypeRelationship.cs index c61b9e5b..0337ed85 100644 --- a/JSONAPI/Core/ToManyResourceTypeRelationship.cs +++ b/JSONAPI/Core/ToManyResourceTypeRelationship.cs @@ -9,8 +9,8 @@ namespace JSONAPI.Core public sealed class ToManyResourceTypeRelationship : ResourceTypeRelationship { internal ToManyResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate) - : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, true) + string selfLinkTemplate, string relatedResourceLinkTemplate, bool serializeRelationshipLink = true, bool serializeRelatedResourceLink = true) + : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, true, serializeRelationshipLink, serializeRelatedResourceLink) { } } diff --git a/JSONAPI/Core/ToOneResourceTypeRelationship.cs b/JSONAPI/Core/ToOneResourceTypeRelationship.cs index cb9a57c4..25d148c8 100644 --- a/JSONAPI/Core/ToOneResourceTypeRelationship.cs +++ b/JSONAPI/Core/ToOneResourceTypeRelationship.cs @@ -9,8 +9,8 @@ namespace JSONAPI.Core public sealed class ToOneResourceTypeRelationship : ResourceTypeRelationship { internal ToOneResourceTypeRelationship(PropertyInfo property, string jsonKey, Type relatedType, - string selfLinkTemplate, string relatedResourceLinkTemplate) - : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, false) + string selfLinkTemplate, string relatedResourceLinkTemplate, bool serializeRelationshipLink = true, bool serializeRelatedResourceLink = true) + : base(property, jsonKey, relatedType, selfLinkTemplate, relatedResourceLinkTemplate, false, serializeRelationshipLink, serializeRelatedResourceLink) { } } diff --git a/JSONAPI/Documents/DefaultLinkConventions.cs b/JSONAPI/Documents/DefaultLinkConventions.cs index 6ca5f30e..88d1c500 100644 --- a/JSONAPI/Documents/DefaultLinkConventions.cs +++ b/JSONAPI/Documents/DefaultLinkConventions.cs @@ -10,6 +10,8 @@ public class DefaultLinkConventions : ILinkConventions { public ILink GetRelationshipLink(TResource relationshipOwner, IResourceTypeRegistry resourceTypeRegistry, ResourceTypeRelationship property, string baseUrl) { + if (!property.SerializeRelationshipLink) return null; + var url = BuildRelationshipUrl(relationshipOwner, resourceTypeRegistry, property, baseUrl); var metadata = GetMetadataForRelationshipLink(relationshipOwner, property); return new Link(url, metadata); @@ -17,6 +19,8 @@ public ILink GetRelationshipLink(TResource relationshipOwner, IResour public ILink GetRelatedResourceLink(TResource relationshipOwner, IResourceTypeRegistry resourceTypeRegistry, ResourceTypeRelationship property, string baseUrl) { + if (!property.SerializeRelatedResourceLink) return null; + var url = BuildRelatedResourceUrl(relationshipOwner, resourceTypeRegistry, property, baseUrl); var metadata = GetMetadataForRelatedResourceLink(relationshipOwner, property); return new Link(url, metadata); diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 2412f4cc..d6ed5c23 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -67,6 +67,7 @@ + From 44600295adc50aa012fd17d3ddf87ae43e34e3e0 Mon Sep 17 00:00:00 2001 From: Clint Good Date: Fri, 22 Jul 2016 01:58:52 +1000 Subject: [PATCH 174/186] Fix to allow filtering to work with entities that have integer keys (#109) * Add Master and Child test entities that use integer keys Add failing test retrieving children related to a master * Fix to allow Get_related_to_many_integer_key to pass --- .../Data/Child.csv | 7 + .../Data/Master.csv | 5 + .../FetchingResourcesTests.cs | 13 + ..._related_to_many_integer_key_response.json | 34 ++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 7 + ...anceTests.EntityFrameworkTestWebApp.csproj | 412 +++++++++--------- .../Models/Child.cs | 21 + .../Models/Master.cs | 16 + .../Models/TestDbContext.cs | 2 + .../Startup.cs | 7 +- JSONAPI/Core/ResourceTypeRegistrar.cs | 2 +- 11 files changed, 319 insertions(+), 207 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv new file mode 100644 index 00000000..b529aab6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv @@ -0,0 +1,7 @@ +Id,ChildDescription,MasterId +7500,"Child 1 Description",1500 +7501,"Child 2 Description",1501 +7502,"Child 3 Description",1501 +7503,"Child 4 Description",1503 +7504,"Child 5 Description",1503 +7505,"Child 6 Description",1503 \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv new file mode 100644 index 00000000..2a582cc4 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv @@ -0,0 +1,5 @@ +Id,Description +1500,"Master 1 Description" +1501,"Master 2 Description" +1502,"Master 3 Description" +1503,"Master 4 Description" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs index 5ec6cc98..36321862 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs @@ -183,5 +183,18 @@ await AssertResponseContent(response, HttpStatusCode.OK); } } + + [TestMethod] + [DeploymentItem(@"Data\Master.csv", @"Data")] + [DeploymentItem(@"Data\Child.csv", @"Data")] + public async Task Get_related_to_many_integer_key() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "masters/1501/children"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_integer_key_response.json", HttpStatusCode.OK); + } + } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json new file mode 100644 index 00000000..f99bab69 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "type": "children", + "id": "7501", + "attributes": { + "child-description": "Child 2 Description" + }, + "relationships": { + "master": { + "links": { + "self": "https://www.example.com/children/7501/relationships/master", + "related": "https://www.example.com/children/7501/master" + } + } + } + }, + { + "type": "children", + "id": "7502", + "attributes": { + "child-description": "Child 3 Description" + }, + "relationships": { + "master": { + "links": { + "self": "https://www.example.com/children/7502/relationships/master", + "related": "https://www.example.com/children/7502/master" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index 7f2a1699..f0cfb3c7 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -123,6 +123,12 @@ + + Always + + + Always + Always @@ -233,6 +239,7 @@ + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index 26e9df66..d7b1469b 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -1,212 +1,214 @@ - - - - - Debug - AnyCPU - - - 2.0 - {76DEE472-723B-4BE6-8B97-428AC326E30F} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp - JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp - v4.5 - true - - - - - ..\ - true - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - False - ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - - False - ..\packages\Autofac.Owin.3.1.0\lib\net45\Autofac.Integration.Owin.dll - - - False - ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll - - - False - ..\packages\Autofac.WebApi2.Owin.3.2.0\lib\net45\Autofac.Integration.WebApi.Owin.dll - - - False - ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - - - False - ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - - - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - - False - ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Owin.1.0\lib\net40\Owin.dll - - - - False - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll - - - - - - - - - - - - False - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll - - - ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.2\lib\net45\System.Web.Http.Owin.dll - - - - - - - - - - - - - Web.config - - - Web.config - - - - - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {64abe648-efcb-46ee-9e1a-e163f52bf372} - JSONAPI.Autofac.EntityFramework - - - {af7861f3-550b-4f70-a33e-1e5f48d39333} - JSONAPI.Autofac - - - {e906356c-93f6-41f6-9a0d-73b8a99aa53c} - JSONAPI.EntityFramework - - - {52b19fd6-efaa-45b5-9c3e-a652e27608d1} - JSONAPI - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 9283 - / - http://localhost:9283/ - False - False - - - False - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + + + Debug + AnyCPU + + + 2.0 + {76DEE472-723B-4BE6-8B97-428AC326E30F} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + v4.5 + true + + + + + ..\ + true + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + False + ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + + + False + ..\packages\Autofac.Owin.3.1.0\lib\net45\Autofac.Integration.Owin.dll + + + False + ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll + + + False + ..\packages\Autofac.WebApi2.Owin.3.2.0\lib\net45\Autofac.Integration.WebApi.Owin.dll + + + False + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + False + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + False + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + + + + + + + + + + + + False + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.2\lib\net45\System.Web.Http.Owin.dll + + + + + + + + + + + + + Web.config + + + Web.config + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {64abe648-efcb-46ee-9e1a-e163f52bf372} + JSONAPI.Autofac.EntityFramework + + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + + + {e906356c-93f6-41f6-9a0d-73b8a99aa53c} + JSONAPI.EntityFramework + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 9283 + / + http://localhost:9283/ + False + False + + + False + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + --> \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs new file mode 100644 index 00000000..b3aefcc7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Child + { + public int Id { get; set; } + + public string ChildDescription { get; set; } + + [Required] + [JsonIgnore] + public int MasterId { get; set; } + + [ForeignKey("MasterId")] + public Master Master { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs new file mode 100644 index 00000000..5ad2015c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Master + { + public int Id { get; set; } + + public string Description { get; set; } + + public virtual ICollection Children { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs index 7efbb549..d0d8aec8 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs @@ -47,5 +47,7 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) public DbSet StarshipClasses { get; set; } public DbSet Officers { get; set; } public DbSet StarshipOfficerLinks { get; set; } + public DbSet Masters { get; set; } + public DbSet Children { get; set; } } } \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index ae14df36..b4fee0ec 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -13,6 +13,7 @@ using JSONAPI.EntityFramework; using JSONAPI.EntityFramework.Configuration; using Owin; +using System.Collections.Generic; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp { @@ -33,7 +34,9 @@ public Startup(Func dbContextFactory) public void Configuration(IAppBuilder app) { - var configuration = new JsonApiConfiguration(); + var configuration = new JsonApiConfiguration( + new Core.PluralizationService( + new Dictionary { { "Child", "Children" } })); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); @@ -58,6 +61,8 @@ public void Configuration(IAppBuilder app) rc => rc.UseMaterializer()); }); // Example of a resource that is mapped from a DB entity configuration.RegisterResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); var configurator = new JsonApiHttpAutofacConfigurator(); configurator.OnApplicationLifetimeScopeCreating(builder => diff --git a/JSONAPI/Core/ResourceTypeRegistrar.cs b/JSONAPI/Core/ResourceTypeRegistrar.cs index 742e025c..5b960a91 100644 --- a/JSONAPI/Core/ResourceTypeRegistrar.cs +++ b/JSONAPI/Core/ResourceTypeRegistrar.cs @@ -77,7 +77,7 @@ public IResourceTypeRegistration BuildRegistration(Type type, string resourceTyp filterByIdFactory = (param, id) => { var propertyExpr = Expression.Property(param, idProperty); - var idExpr = Expression.Constant(id); + var idExpr = Expression.Constant(Convert.ChangeType(id, idProperty.PropertyType)); return Expression.Equal(propertyExpr, idExpr); }; } From ef5cb3c10017f04c3520da5c2060bdc551a569dc Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 4 Aug 2016 20:22:11 +0200 Subject: [PATCH 175/186] Fix: for non string keys with some tests / allow uppercase id (#110) * Fix: - allow ID to be in upper case (Oracle generates this by default in upper) - allow the id be long or decimal (Oracle numbers are mapped to decimal) * Made StringExtensions public it's handy to use in custom NamingConvention --- .../CreatingResourcesTests.cs | 46 ++++++++++++++ .../Data/PostID.csv | 5 ++ .../Data/PostLongId.csv | 5 ++ .../DeletingResourcesTests.cs | 62 ++++++++++++++++--- ...ostID_with_client_provided_id_Request.json | 11 ++++ ...ongId_with_client_provided_id_Request.json | 11 ++++ ...stID_with_client_provided_id_Response.json | 11 ++++ ...ngId_with_client_provided_id_Response.json | 11 ++++ .../PatchWithAttributeUpdateRequestID.json | 9 +++ ...PatchWithAttributeUpdateRequestLongId.json | 9 +++ .../PatchWithAttributeUpdateResponseID.json | 11 ++++ ...atchWithAttributeUpdateResponseLongId.json | 11 ++++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 14 +++++ .../UpdatingResourcesTests.cs | 46 ++++++++++++++ ...anceTests.EntityFrameworkTestWebApp.csproj | 14 +++-- .../Models/PostID.cs | 21 +++++++ .../Models/PostLongId.cs | 21 +++++++ .../Models/TestDbContext.cs | 2 + .../Startup.cs | 2 + .../DbContextExtensionsTests.cs | 27 ++++++++ .../JSONAPI.EntityFramework.Tests.csproj | 1 + .../Models/PostID.cs | 21 +++++++ ...tityFrameworkResourceObjectMaterializer.cs | 10 +-- .../EntityFrameworkDocumentMaterializer.cs | 2 +- JSONAPI/Core/ResourceTypeRegistrar.cs | 11 +++- JSONAPI/Extensions/StringExtensions.cs | 2 +- 26 files changed, 373 insertions(+), 23 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs create mode 100644 JSONAPI.EntityFramework.Tests/Models/PostID.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs index 40a973df..e1b5a308 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs @@ -39,6 +39,52 @@ public async Task Post_with_client_provided_id() } } + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task PostID_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "post-i-ds", @"Fixtures\CreatingResources\Requests\PostID_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\PostID_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.ID == "205"); + actualPost.ID.Should().Be("205"); + actualPost.Title.Should().Be("Added post"); + actualPost.Content.Should().Be("Added post content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task PostLongId_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "post-long-ids", @"Fixtures\CreatingResources\Requests\PostLongId_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\PostLongId_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == 205); + actualPost.Id.Should().Be(205); + actualPost.Title.Should().Be("Added post"); + actualPost.Content.Should().Be("Added post content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); + } + } + } + [TestMethod] [DeploymentItem(@"Data\Comment.csv", @"Data")] [DeploymentItem(@"Data\Post.csv", @"Data")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv new file mode 100644 index 00000000..544b39c7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv @@ -0,0 +1,5 @@ +ID,Title,Content,Created,AuthorId +"201","Post 1","Post 1 content","2015-01-31T14:00Z" +"202","Post 2","Post 2 content","2015-02-05T08:10Z" +"203","Post 3","Post 3 content","2015-02-07T11:11Z" +"204","Post 4","Post 4 content","2015-02-08T06:59Z" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv new file mode 100644 index 00000000..ac3f5a42 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv @@ -0,0 +1,5 @@ +Id,Title,Content,Created,AuthorId +201,"Post 1","Post 1 content","2015-01-31T14:00Z" +202,"Post 2","Post 2 content","2015-02-05T08:10Z" +203,"Post 3","Post 3 content","2015-02-07T11:11Z" +204,"Post 4","Post 4 content","2015-02-08T06:59Z" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs index c5902256..f77715f5 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs @@ -11,11 +11,11 @@ namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests public class DeletingResourcesTests : AcceptanceTestsBase { [TestMethod] - [DeploymentItem(@"Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Data\User.csv", @"Acceptance\Data")] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] public async Task Delete() { using (var effortConnection = GetEffortConnection()) @@ -28,10 +28,54 @@ public async Task Delete() using (var dbContext = new TestDbContext(effortConnection, false)) { - var allTodos = dbContext.Posts.ToArray(); - allTodos.Length.Should().Be(3); - var actualTodo = allTodos.FirstOrDefault(t => t.Id == "203"); - actualTodo.Should().BeNull(); + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.Id == "203"); + actualPosts.Should().BeNull(); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task DeleteID() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "post-i-ds/203"); + + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.ID == "203"); + actualPosts.Should().BeNull(); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task DeleteLongId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "post-long-ids/203"); + + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.Id == 203); + actualPosts.Should().BeNull(); } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json new file mode 100644 index 00000000..41424e02 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json new file mode 100644 index 00000000..b52fe5c4 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json new file mode 100644 index 00000000..7122e9e9 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json new file mode 100644 index 00000000..dc5ee0f8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json new file mode 100644 index 00000000..ac7fbb4b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "post-i-ds", + "id": "202", + "attributes": { + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json new file mode 100644 index 00000000..7358d7a8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "post-long-ids", + "id": "202", + "attributes": { + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json new file mode 100644 index 00000000..b0448cb5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json new file mode 100644 index 00000000..fea5fa0a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index f0cfb3c7..f7a3c706 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -132,6 +132,12 @@ Always + + Always + + + Always + Always @@ -240,6 +246,14 @@ + + + + + + + + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs index 9d898537..48df90f9 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs @@ -41,6 +41,52 @@ public async Task PatchWithAttributeUpdate() } } + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task PatchWithAttributeUpdateID() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "post-i-ds/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequestID.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponseID.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID; + allPosts.Count().Should().Be(4); + var actualPost = allPosts.First(t => t.ID == "202"); + actualPost.ID.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task PatchWithAttributeUpdateLongId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "post-long-ids/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequestLongId.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponseLongId.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId; + allPosts.Count().Should().Be(4); + var actualPost = allPosts.First(t => t.Id == 202); + actualPost.Id.Should().Be(202); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + } + } + } + [TestMethod] [DeploymentItem(@"Data\Comment.csv", @"Data")] [DeploymentItem(@"Data\Post.csv", @"Data")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index d7b1469b..0a5148ea 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -138,6 +138,8 @@ + + @@ -204,11 +206,11 @@ - \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs new file mode 100644 index 00000000..4e7bd60f --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class PostID + { + [Key] + public string ID { get; set; } + + public string Title { get; set; } + + public string Content { get; set; } + + public DateTimeOffset Created { get; set; } + + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs new file mode 100644 index 00000000..24e830d5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class PostLongId + { + [Key] + public long Id { get; set; } + + public string Title { get; set; } + + public string Content { get; set; } + + public DateTimeOffset Created { get; set; } + + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs index d0d8aec8..4850d0a0 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs @@ -40,6 +40,8 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) public DbSet Comments { get; set; } public DbSet Languages { get; set; } public DbSet Posts { get; set; } + public DbSet PostsID { get; set; } + public DbSet PostsLongId { get; set; } public DbSet Tags { get; set; } public DbSet Users { get; set; } public DbSet LanguageUserLinks { get; set; } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index b4fee0ec..371b9a22 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -48,6 +48,8 @@ public void Configuration(IAppBuilder app) c.OverrideDefaultSortById(LanguageUserLinkSortByIdFactory); }); configuration.RegisterResourceType(); + configuration.RegisterResourceType(); + configuration.RegisterResourceType(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); diff --git a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs index 4b8d4ed2..8548b6bd 100644 --- a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs +++ b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs @@ -17,6 +17,7 @@ private class TestDbContext : DbContext { public DbSet Backlinks { get; set; } public DbSet Posts { get; set; } + public DbSet PostIDs { get; set; } public TestDbContext(DbConnection conn) : base(conn, true) { @@ -34,6 +35,10 @@ private class SubPost : Post { public string Foo { get; set; } } + private class SubPostID : PostID + { + public string Foo { get; set; } + } private DbConnection _conn; private TestDbContext _context; @@ -71,6 +76,17 @@ public void GetKeyNamesStandardIdTest() keyNames.First().Should().Be("Id"); } + [TestMethod] + public void GetKeyNamesStandardIDTest() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(PostID)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("ID"); + } + [TestMethod] public void GetKeyNamesNonStandardIdTest() { @@ -93,6 +109,17 @@ public void GetKeyNamesForChildClass() keyNames.First().Should().Be("Id"); } + [TestMethod] + public void GetKeyNamesForChildClassID() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(SubPostID)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("ID"); + } + [TestMethod] public void GetKeyNamesNotAnEntityTest() { diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 2938cf78..6e341345 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -119,6 +119,7 @@ + diff --git a/JSONAPI.EntityFramework.Tests/Models/PostID.cs b/JSONAPI.EntityFramework.Tests/Models/PostID.cs new file mode 100644 index 00000000..420e047a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Models/PostID.cs @@ -0,0 +1,21 @@ +namespace JSONAPI.EntityFramework.Tests.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations.Schema; + + public partial class PostID + { + public PostID() + { + this.Comments = new HashSet(); + } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid ID { get; set; } + public string Title { get; set; } + + public virtual Author Author { get; set; } + public virtual ICollection Comments { get; set; } + } +} diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs index 57c2b29b..b0b26ea9 100644 --- a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -69,8 +69,10 @@ public async Task MaterializeResourceObject(IResourceObject resourceObje /// protected virtual Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration) { - typeRegistration.IdProperty.SetValue(newObject, resourceObject.Id); - + if (resourceObject.Id != null) + { + typeRegistration.IdProperty.SetValue(newObject, Convert.ChangeType(resourceObject.Id, typeRegistration.IdProperty.PropertyType)); + } return Task.FromResult(0); } @@ -82,7 +84,7 @@ protected virtual async Task GetExistingRecord(IResourceTypeRegistration ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) { var method = _openGetExistingRecordGenericMethod.MakeGenericMethod(registration.Type); - var result = (dynamic) method.Invoke(this, new object[] {registration, id, relationshipsToInclude, cancellationToken}); + var result = (dynamic) method.Invoke(this, new object[] {registration, id, relationshipsToInclude, cancellationToken}); // no convert needed => see GetExistingRecordGeneric => filterByIdFactory will do it return await result; } @@ -170,7 +172,7 @@ protected async Task GetExistingRecordGeneric(IResourceTypeReg string id, ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) where TRecord : class { var param = Expression.Parameter(registration.Type); - var filterExpression = registration.GetFilterByIdExpression(param, id); + var filterExpression = registration.GetFilterByIdExpression(param, id); // no conversion of id => filterByIdFactory will do it var lambda = Expression.Lambda>(filterExpression, param); var query = _dbContext.Set().AsQueryable() .Where(lambda); diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index db2aaf29..d7b3ce73 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -88,7 +88,7 @@ public virtual async Task UpdateRecord(string id, ISing public virtual async Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) { - var singleResource = await _dbContext.Set().FindAsync(cancellationToken, id); + var singleResource = await _dbContext.Set().FindAsync(cancellationToken, Convert.ChangeType(id, _resourceTypeRegistration.IdProperty.PropertyType)); _dbContext.Set().Remove(singleResource); await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/JSONAPI/Core/ResourceTypeRegistrar.cs b/JSONAPI/Core/ResourceTypeRegistrar.cs index 5b960a91..bedd6e21 100644 --- a/JSONAPI/Core/ResourceTypeRegistrar.cs +++ b/JSONAPI/Core/ResourceTypeRegistrar.cs @@ -77,7 +77,14 @@ public IResourceTypeRegistration BuildRegistration(Type type, string resourceTyp filterByIdFactory = (param, id) => { var propertyExpr = Expression.Property(param, idProperty); - var idExpr = Expression.Constant(Convert.ChangeType(id, idProperty.PropertyType)); + object obj = id; + if (obj == null) + { + var t = propertyExpr.Type; + if (t.IsValueType) + obj = Activator.CreateInstance(t); + } + var idExpr = Expression.Constant(Convert.ChangeType(obj, propertyExpr.Type)); return Expression.Equal(propertyExpr, idExpr); }; } @@ -194,7 +201,7 @@ protected virtual PropertyInfo CalculateIdProperty(Type type) type .GetProperties() .FirstOrDefault(p => p.CustomAttributes.Any(attr => attr.AttributeType == typeof(UseAsIdAttribute))) - ?? type.GetProperty("Id"); + ?? type.GetProperty("Id") ?? type.GetProperty("ID"); } } } \ No newline at end of file diff --git a/JSONAPI/Extensions/StringExtensions.cs b/JSONAPI/Extensions/StringExtensions.cs index 89826301..7f4e9ad4 100644 --- a/JSONAPI/Extensions/StringExtensions.cs +++ b/JSONAPI/Extensions/StringExtensions.cs @@ -2,7 +2,7 @@ namespace JSONAPI.Extensions { - internal static class StringExtensions + public static class StringExtensions { private static readonly Regex PascalizeRegex = new Regex(@"(?:^|_|\-|\.)(.)"); From 5ae40e89f2be28b408a23be808a3dd9e237d2066 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Fri, 5 Aug 2016 16:48:32 +0200 Subject: [PATCH 176/186] Feat: context path for api (#114) * extended JsonApiHttpConfiguration to allow BaseUrlService with configured context path * Feat: context path for api - option to configure context path for api which will be respected in routing and for urls in response - option to configure the public origin address which will be used in response urls closes #112 --- .../AcceptanceTestsBase.cs | 41 ++- .../BaseUrlTest.cs | 166 ++++++++++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 17 ++ .../packages.config | 3 + .../Properties/AssemblyInfo.cs | 2 + .../Startup.cs | 51 +++- JSONAPI.Autofac/JsonApiAutofacModule.cs | 9 +- .../JsonApiHttpAutofacConfigurator.cs | 2 +- JSONAPI.Tests/Http/BaseUrlServiceTest.cs | 287 ++++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + JSONAPI/Configuration/JsonApiConfiguration.cs | 6 + .../Configuration/JsonApiHttpConfiguration.cs | 15 +- JSONAPI/Http/BaseUrlService.cs | 103 ++++++- JSONAPI/Http/IBaseUrlService.cs | 6 + README.md | 39 +++ 15 files changed, 726 insertions(+), 22 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs create mode 100644 JSONAPI.Tests/Http/BaseUrlServiceTest.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs index 40aa3945..133548dc 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -1,5 +1,6 @@ using System; using System.Data.Common; +using System.IO; using System.Net; using System.Net.Http; using System.Text; @@ -10,6 +11,7 @@ using JSONAPI.Json; using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Owin; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { @@ -20,19 +22,18 @@ public abstract class AcceptanceTestsBase private static readonly Regex GuidRegex = new Regex(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase); //private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace"":[\s]*""[\w\:\\\.\s\,\-]*"""); private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace""[\s]*:[\s]*"".*?"""); - private static readonly Uri BaseUri = new Uri("https://www.example.com"); + protected static Uri BaseUri = new Uri("https://www.example.com"); protected static DbConnection GetEffortConnection() { return TestHelpers.GetEffortConnection(@"Data"); } - protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) + protected virtual async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) { var responseContent = await response.Content.ReadAsStringAsync(); - var expectedResponse = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + var expectedResponse = ExpectedResponse(expectedResponseTextResourcePath); string actualResponse; if (redactErrorData) { @@ -51,6 +52,13 @@ protected static async Task AssertResponseContent(HttpResponseMessage response, response.StatusCode.Should().Be(expectedStatusCode); } + protected virtual string ExpectedResponse(string expectedResponseTextResourcePath) + { + var expectedResponse = + JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + return expectedResponse; + } + #region GET protected async Task SubmitGet(DbConnection effortConnection, string requestPath) @@ -58,7 +66,7 @@ protected async Task SubmitGet(DbConnection effortConnectio using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -75,7 +83,7 @@ protected async Task SubmitPost(DbConnection effortConnecti using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -100,7 +108,7 @@ protected async Task SubmitPatch(DbConnection effortConnect using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -124,7 +132,7 @@ protected async Task SubmitDelete(DbConnection effortConnec using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -137,5 +145,22 @@ protected async Task SubmitDelete(DbConnection effortConnec } #endregion + + + + #region configure startup + + /// + /// Startup process was divided into 4 steps to support better acceptance tests. + /// This method can be overridden by subclass to change behavior of setup. + /// + /// + /// + protected virtual void StartupConfiguration(Startup startup, IAppBuilder app) + { + startup.Configuration(app); + } + + #endregion } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs new file mode 100644 index 00000000..cda8ccf8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs @@ -0,0 +1,166 @@ +using System; +using System.Data.SqlTypes; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Owin; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class BaseUrlTest : AcceptanceTestsBase + { + [TestInitialize] + public void TestInit() + { + if (!BaseUri.AbsoluteUri.EndsWith("api/")) + { + BaseUri = new Uri(BaseUri.AbsoluteUri + "api/"); + } + } + [TestCleanup] + public void TestCleanup() + { + if (BaseUri.AbsoluteUri.EndsWith("api/")) + { + BaseUri = new Uri(BaseUri.AbsoluteUri.Substring(0,BaseUri.AbsoluteUri.Length -4)); + } + } + + // custom startup process for this test + protected override void StartupConfiguration(Startup startup, IAppBuilder app) + { + + var configuration = startup.BuildConfiguration(); + // here we add the custom BaseUrlServcie + configuration.CustomBaseUrlService = new BaseUrlService("api"); + var configurator = startup.BuildAutofacConfigurator(app); + var httpConfig = startup.BuildHttpConfiguration(); + startup.MergeAndSetupConfiguration(app, configurator, httpConfig, configuration); + } + + // custom expected response method + protected override string ExpectedResponse(string expectedResponseTextResourcePath) + { + var expected = base.ExpectedResponse(expectedResponseTextResourcePath); + return Regex.Replace(expected, @"www\.example\.com\/", @"www.example.com/api/"); + } + + // copied some tests in here + + // copied from ComputedIdTests + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Language.csv", @"Data")] + [DeploymentItem(@"Data\LanguageUserLink.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_resource_with_computed_id_by_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "language-user-links/9001_402"); + + await AssertResponseContent(response, @"Fixtures\ComputedId\Responses\Get_resource_with_computed_id_by_id_Response.json", HttpStatusCode.OK); + } + } + + + // copied from CreatingResourcesTests + + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task PostLongId_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "post-long-ids", @"Fixtures\CreatingResources\Requests\PostLongId_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\PostLongId_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == 205); + actualPost.Id.Should().Be(205); + actualPost.Title.Should().Be("Added post"); + actualPost.Content.Should().Be("Added post content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); + } + } + } + + + + // copied from DeletingResourcesTests + + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task DeleteID() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "post-i-ds/203"); + + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.ID == "203"); + actualPosts.Should().BeNull(); + } + } + } + + + + // copied from FetchingResourcesTests + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetWithFilterResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetById() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/202"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetByIdResponse.json", HttpStatusCode.OK); + } + } + + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index f7a3c706..b335e6c5 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -61,6 +61,10 @@ ..\packages\Microsoft.Owin.Testing.3.0.0\lib\net45\Microsoft.Owin.Testing.dll + + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + ..\packages\NMemory.1.0.1\lib\net45\NMemory.dll @@ -71,7 +75,15 @@ + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + True + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + True + @@ -98,6 +110,7 @@ + @@ -107,6 +120,10 @@ {76dee472-723b-4be6-8b97-428ac326e30f} JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + + {AF7861F3-550B-4F70-A33E-1E5F48D39333} + JSONAPI.Autofac + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} JSONAPI diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config index f704445b..0243217e 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config @@ -3,9 +3,12 @@ + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs index 02097fc4..130abbb8 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -32,3 +33,4 @@ // by using the '*' as shown below: [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index 371b9a22..aec594d2 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -33,9 +33,35 @@ public Startup(Func dbContextFactory) } public void Configuration(IAppBuilder app) + { + /* these steps are divided in multiple methods to support better acceptance tests + * in production all the steps can be made inside the Configuration method. + */ + var configuration = BuildConfiguration(); + + var configurator = BuildAutofacConfigurator(app); + + var httpConfig = BuildHttpConfiguration(); + + MergeAndSetupConfiguration(app, configurator, httpConfig, configuration); + } + + internal void MergeAndSetupConfiguration(IAppBuilder app, JsonApiHttpAutofacConfigurator configurator, + HttpConfiguration httpConfig, JsonApiConfiguration configuration) + { + configurator.Apply(httpConfig, configuration); + app.UseWebApi(httpConfig); + app.UseAutofacWebApi(httpConfig); + } + + /// + /// Build up the which registers all the model types and their mappings. + /// + /// + internal JsonApiConfiguration BuildConfiguration() { var configuration = new JsonApiConfiguration( - new Core.PluralizationService( + new Core.PluralizationService( new Dictionary { { "Child", "Children" } })); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); @@ -65,7 +91,16 @@ public void Configuration(IAppBuilder app) configuration.RegisterResourceType(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); + return configuration; + } + /// + /// Build up the which registers , modules and materializer + /// + /// + /// + internal JsonApiHttpAutofacConfigurator BuildAutofacConfigurator(IAppBuilder app) + { var configurator = new JsonApiHttpAutofacConfigurator(); configurator.OnApplicationLifetimeScopeCreating(builder => { @@ -83,7 +118,15 @@ public void Configuration(IAppBuilder app) // TODO: is this a candidate for spinning into a JSONAPI.Autofac.WebApi.Owin package? Yuck app.UseAutofacMiddleware(applicationLifetimeScope); }); + return configurator; + } + /// + /// Build up the with additional routes + /// + /// + internal HttpConfiguration BuildHttpConfiguration() + { var httpConfig = new HttpConfiguration { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always @@ -93,12 +136,10 @@ public void Configuration(IAppBuilder app) httpConfig.Routes.MapHttpRoute("Samples", "samples", new { Controller = "Samples" }); httpConfig.Routes.MapHttpRoute("Search", "search", new { Controller = "Search" }); httpConfig.Routes.MapHttpRoute("Trees", "trees", new { Controller = "Trees" }); - - configurator.Apply(httpConfig, configuration); - app.UseWebApi(httpConfig); - app.UseAutofacWebApi(httpConfig); + return httpConfig; } + private BinaryExpression LanguageUserLinkFilterByIdFactory(ParameterExpression param, string id) { var split = id.Split('_'); diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index e937de35..d388dafc 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -126,7 +126,14 @@ protected override void Load(ContainerBuilder builder) }); builder.RegisterType().SingleInstance(); - builder.RegisterType().As().SingleInstance(); + if (_jsonApiConfiguration.CustomBaseUrlService != null) + { + builder.Register(c => _jsonApiConfiguration.CustomBaseUrlService).As().SingleInstance(); + } + else + { + builder.RegisterType().As().SingleInstance(); + } builder.RegisterType().As().InstancePerRequest(); // Serialization diff --git a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs index 2fa79374..035f8c09 100644 --- a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs +++ b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs @@ -52,7 +52,7 @@ public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jso _appLifetimeScopeBegunAction(applicationLifetimeScope); var jsonApiHttpConfiguration = applicationLifetimeScope.Resolve(); - jsonApiHttpConfiguration.Apply(httpConfiguration); + jsonApiHttpConfiguration.Apply(httpConfiguration, jsonApiConfiguration); httpConfiguration.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); } diff --git a/JSONAPI.Tests/Http/BaseUrlServiceTest.cs b/JSONAPI.Tests/Http/BaseUrlServiceTest.cs new file mode 100644 index 00000000..304ae60f --- /dev/null +++ b/JSONAPI.Tests/Http/BaseUrlServiceTest.cs @@ -0,0 +1,287 @@ +using System; +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class BaseUrlServiceTest + { + [TestMethod] + public void BaseUrlRootTest() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(); + + // Act + var baseUrl =baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlOneLevelTest() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlOneLevelSlashTest() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("/api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlOneLevelSlash2Test() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlTwoLevelTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/superapi"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlTwoLevelSlashTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/superapi/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlTwoLevelSlash2Test() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("/api/superapi/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlConflictingNameTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi?sort=api-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + + [TestMethod] + public void BaseUrlPublicOriginTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginNoSlashTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsHighPortTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com:12443/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com:12443/"); + } + + [TestMethod] + public void BaseUrlPublicOriginInternalPortTest() + { + // Arrange + const string uri = "http://wwwhost123:8080/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + + + [TestMethod] + public void BaseUrlPublicOriginContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginNoSlashContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com"), "/api/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), "/api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsHighPortContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com:12443/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com:12443/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginInternalPortContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123:8080/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 7b2fff70..4a86feae 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -90,6 +90,7 @@ + diff --git a/JSONAPI/Configuration/JsonApiConfiguration.cs b/JSONAPI/Configuration/JsonApiConfiguration.cs index 22722ba4..27752c1a 100644 --- a/JSONAPI/Configuration/JsonApiConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiConfiguration.cs @@ -14,6 +14,7 @@ public class JsonApiConfiguration : IJsonApiConfiguration private readonly IResourceTypeRegistrar _resourceTypeRegistrar; public ILinkConventions LinkConventions { get; private set; } public IEnumerable ResourceTypeConfigurations { get { return _resourceTypeConfigurations; } } + public IBaseUrlService CustomBaseUrlService { get; set; } private readonly IList _resourceTypeConfigurations; @@ -104,5 +105,10 @@ public interface IJsonApiConfiguration /// by the ResourceTypeRegistrar /// IEnumerable ResourceTypeConfigurations { get; } + + /// + /// A custom configured + /// + IBaseUrlService CustomBaseUrlService { get; } } } diff --git a/JSONAPI/Configuration/JsonApiHttpConfiguration.cs b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs index 32ce73b1..4451eda8 100644 --- a/JSONAPI/Configuration/JsonApiHttpConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs @@ -34,7 +34,8 @@ public JsonApiHttpConfiguration(JsonApiFormatter formatter, /// Applies the running configuration to an HttpConfiguration instance /// /// The HttpConfiguration to apply this JsonApiHttpConfiguration to - public void Apply(HttpConfiguration httpConfig) + /// configuration holding BaseUrlService wich could provide a context path. + public void Apply(HttpConfiguration httpConfig, IJsonApiConfiguration jsonApiConfiguration) { httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(_formatter); @@ -42,10 +43,16 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Filters.Add(_fallbackDocumentBuilderAttribute); httpConfig.Filters.Add(_jsonApiExceptionFilterAttribute); + var contextPath = jsonApiConfiguration.CustomBaseUrlService?.GetContextPath(); + if (contextPath != null && !contextPath.Equals(string.Empty)) + { + contextPath += "/"; + } + // Web API routes - httpConfig.Routes.MapHttpRoute("ResourceCollection", "{resourceType}", new { controller = "Main" }); - httpConfig.Routes.MapHttpRoute("Resource", "{resourceType}/{id}", new { controller = "Main" }); - httpConfig.Routes.MapHttpRoute("RelatedResource", "{resourceType}/{id}/{relationshipName}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("ResourceCollection", contextPath + "{resourceType}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("Resource", contextPath + "{resourceType}/{id}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("RelatedResource", contextPath + "{resourceType}/{id}/{relationshipName}", new { controller = "Main" }); } } } diff --git a/JSONAPI/Http/BaseUrlService.cs b/JSONAPI/Http/BaseUrlService.cs index 174e83e5..df1ee1a1 100644 --- a/JSONAPI/Http/BaseUrlService.cs +++ b/JSONAPI/Http/BaseUrlService.cs @@ -8,11 +8,108 @@ namespace JSONAPI.Http /// public class BaseUrlService : IBaseUrlService { + private string _contextPath = string.Empty; + private Uri _publicOrigin; + + /// + /// Default constructor + /// + public BaseUrlService() { } + + /// + /// Constructor which provides a context path for the routes of JSONAPI.NET + /// + /// context path for the routes + public BaseUrlService(string contextPath) + { + CleanContextPath(contextPath); + } + + /// + /// Constructor which provides a public origin host and a context path for the routes of JSONAPI.NET. + /// If only public origin is desired provide emtpy string to contextPath. + /// + /// public hostname + /// context path for the routes + public BaseUrlService(Uri publicOrigin, string contextPath) + { + CleanContextPath(contextPath); + this._publicOrigin = publicOrigin; + } + + /// + /// Retrieve the base path to provide in responses. + /// + /// + /// public virtual string GetBaseUrl(HttpRequestMessage requestMessage) { - return - new Uri(requestMessage.RequestUri.AbsoluteUri.Replace(requestMessage.RequestUri.PathAndQuery, - String.Empty)).ToString(); + string pathAndQuery; + string absolutUri = requestMessage.RequestUri.AbsoluteUri; + if (_publicOrigin != null) + { + var publicUriBuilder = new UriBuilder(absolutUri) + { + Host = _publicOrigin.Host, + Scheme = _publicOrigin.Scheme, + Port = _publicOrigin.Port + }; + absolutUri = publicUriBuilder.Uri.AbsoluteUri; + pathAndQuery = publicUriBuilder.Uri.PathAndQuery; + } + else + { + pathAndQuery = requestMessage.RequestUri.PathAndQuery; + } + pathAndQuery = RemoveFromBegin(pathAndQuery, GetContextPath()); + pathAndQuery= pathAndQuery.TrimStart('/'); + var baseUrl = RemoveFromEnd(absolutUri, pathAndQuery); + return baseUrl; + } + + /// + /// Provides the context path to serve JSONAPI.NET without leading and trailing slash. + /// + /// + public string GetContextPath() + { + return _contextPath; + } + + /// + /// Makes sure thre are no slashes at the beginnig or end. + /// + /// + private void CleanContextPath(string contextPath) + { + if (!string.IsNullOrEmpty(contextPath) && !contextPath.EndsWith("/")) + { + contextPath = contextPath.TrimEnd('/'); + } + if (!string.IsNullOrEmpty(contextPath) && contextPath.StartsWith("/")) + { + contextPath = contextPath.TrimStart('/'); + } + _contextPath = contextPath; + } + + + private string RemoveFromEnd(string input, string suffix) + { + if (input.EndsWith(suffix)) + { + return input.Substring(0, input.Length - suffix.Length); + } + return input; + } + private string RemoveFromBegin(string input, string prefix) + { + prefix = "/" + prefix; + if (input.StartsWith(prefix)) + { + return input.Substring(prefix.Length, input.Length - prefix.Length); + } + return input; } } } \ No newline at end of file diff --git a/JSONAPI/Http/IBaseUrlService.cs b/JSONAPI/Http/IBaseUrlService.cs index ac5aa9f0..dd9f3177 100644 --- a/JSONAPI/Http/IBaseUrlService.cs +++ b/JSONAPI/Http/IBaseUrlService.cs @@ -11,5 +11,11 @@ public interface IBaseUrlService /// Gets the base URL for a request /// string GetBaseUrl(HttpRequestMessage requestMessage); + + /// + /// Gets the context path JSONAPI is served under without slashes at the beginning and end. + /// + /// + string GetContextPath(); } } diff --git a/README.md b/README.md index b75c7ca0..b930f0e6 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,42 @@ A `JSONAPI.IMaterializer` object can be added to that `ApiController` to broker # Didn't I read something about using Entity Framework? The classes in the `JSONAPI.EntityFramework` namespace take great advantage of the patterns set out in the `JSONAPI` namespace. The `EntityFrameworkMaterializer` is an `IMaterializer` that can operate with your own `DbContext` class to retrieve objects by Id/Primary Key, and can retrieve and update existing objects from your context in a way that Entity Framework expects for change tracking…that means, in theory, you can use the provided `JSONAPI.EntityFramework.ApiController` base class to handle GET, PUT, POST, and DELETE without writing any additional code! You will still almost certainly subclass `ApiController` to implement your business logic, but that means you only have to worry about your business logic--not implementing the JSON API spec or messing with your persistence layer. + + + +# Configuration JSONAPI.EntityFramework + +- [ ] Add some hints about the configuration of JSONAPI.EntityFramework + +## Set the context path of JSONAPI.EntityFramework + +Per default the routes created for the registered models from EntityFramework will appear in root folder. This can conflict with folders of the file system or other routes you may want to serve from the same project. +To solve the issue we can create an instance of the `BaseUrlService` and put it in the configuration. +The `BaseUrlService` can be created with the context path parameter which will be used to register the routes and put into responses. + +```C# +var configuration = new JsonApiConfiguration(); +configuration.RegisterEntityFrameworkResourceType(); +// ... registration stuff you need + +// this makes JSONAPI.NET create the route 'api/posts' +configuration.CustomBaseUrlService = new BaseUrlService("api"); +``` + +## Set the public origin host of JSONAPI +Since JSONAPI.NET returns urls it could result wrong links if JSONAPI runs behind a reverse proxy. Configure the public origin address as follows: +```C# +var configuration = new JsonApiConfiguration(); +configuration.RegisterEntityFrameworkResourceType(); +// ... registration stuff you need + +// this makes JSONAPI.NET to set the urls in responses to https://api.example.com/posts +// Important: don't leave the second string parameter! see below. +configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), ""); + +// this can also be combined with the context paht for routing like that: +// this makes JSONAPI.NET create the route 'api/posts' and response urls to be https://api.example.com:9443/api/posts +configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com:9443/"), "api"); + +``` + From 8dd97a9db251cf3d4afe509f4ed69b4ebeb934b5 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Wed, 10 Aug 2016 13:47:32 +0200 Subject: [PATCH 177/186] Feat: added handler methods to intercept jsonapi entity framework (#116) * Feat: added handler methods to intercept jsonapi entity framework for custom entity manipulations * make the dbcontext accessible to subclasses for interception * @csantero implemented your suggestions * implemented as void method --- .../EntityFrameworkDocumentMaterializer.cs | 64 ++++++++++++++----- README.md | 36 +++++++++++ 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index d7b3ce73..0b86166f 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -18,7 +18,7 @@ namespace JSONAPI.EntityFramework.Http /// public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer where T : class { - private readonly DbContext _dbContext; + protected readonly DbContext DbContext; private readonly IResourceTypeRegistration _resourceTypeRegistration; private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; @@ -38,7 +38,7 @@ public EntityFrameworkDocumentMaterializer( ISortExpressionExtractor sortExpressionExtractor, IBaseUrlService baseUrlService) { - _dbContext = dbContext; + DbContext = dbContext; _resourceTypeRegistration = resourceTypeRegistration; _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _singleResourceDocumentBuilder = singleResourceDocumentBuilder; @@ -49,7 +49,7 @@ public EntityFrameworkDocumentMaterializer( public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { - var query = _dbContext.Set().AsQueryable(); + var query = DbContext.Set().AsQueryable(); var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken); } @@ -68,29 +68,33 @@ public virtual async Task CreateRecord(ISingleResourceD HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); - await _dbContext.SaveChangesAsync(cancellationToken); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null, null); + var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); + await OnCreate(newRecord); + await DbContext.SaveChangesAsync(cancellationToken); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null); return returnDocument; } + public virtual async Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var newRecord = await MaterializeAsync(requestDocument.PrimaryData, cancellationToken); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(newRecord, apiBaseUrl, null, null); - await _dbContext.SaveChangesAsync(cancellationToken); + var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); + await OnUpdate(newRecord); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null); + await DbContext.SaveChangesAsync(cancellationToken); return returnDocument; } public virtual async Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) { - var singleResource = await _dbContext.Set().FindAsync(cancellationToken, Convert.ChangeType(id, _resourceTypeRegistration.IdProperty.PropertyType)); - _dbContext.Set().Remove(singleResource); - await _dbContext.SaveChangesAsync(cancellationToken); + var singleResource = DbContext.Set().FindAsync(cancellationToken, Convert.ChangeType(id, _resourceTypeRegistration.IdProperty.PropertyType)); + await OnDelete(singleResource); + DbContext.Set().Remove(await singleResource); + await DbContext.SaveChangesAsync(cancellationToken); return null; } @@ -107,9 +111,9 @@ protected string GetBaseUrlFromRequest(HttpRequestMessage request) /// Convert a resource object into a material record managed by EntityFramework. /// /// - protected virtual async Task MaterializeAsync(IResourceObject resourceObject, CancellationToken cancellationToken) + protected virtual async Task MaterializeAsync(IResourceObject resourceObject, CancellationToken cancellationToken) { - return await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken); + return (T) await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken); } /// @@ -155,10 +159,40 @@ protected async Task GetRelatedToOne(string i return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null, null); } + + /// + /// Manipulate entity before create. + /// + /// + /// + protected virtual async Task OnCreate(Task record) + { + await record; + } + + /// + /// Manipulate entity before update. + /// + /// + protected virtual async Task OnUpdate(Task record) + { + await record; + } + + /// + /// Manipulate entity before delete. + /// + /// + /// + protected virtual async Task OnDelete(Task record) + { + await record; + } + private IQueryable Filter(Expression> predicate, params Expression>[] includes) where TResource : class { - IQueryable query = _dbContext.Set(); + IQueryable query = DbContext.Set(); if (includes != null && includes.Any()) query = includes.Aggregate(query, (current, include) => current.Include(include)); diff --git a/README.md b/README.md index b930f0e6..129060b7 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,42 @@ The classes in the `JSONAPI.EntityFramework` namespace take great advantage of t - [ ] Add some hints about the configuration of JSONAPI.EntityFramework +## Manipulate entities before JSONAPI.EntityFramework persists them +To change your entities before they get persisted you can extend the `EntityFrameworkDocumentMaterializer` class. You need to register your custom DocumentMaterializer in your `JsonApiConfiguration` like that: +```C# +configuration.RegisterEntityFrameworkResourceType(c =>c.UseDocumentMaterializer()); +``` +Afterwards you can override the `OnCreate`, `OnUpdate` or `OnDelete` methods in your `CustomDocumentMaterializer`. + +```C# +protected override async Task OnCreate(Task record) +{ + await base.OnUpdate(record); + var entity = await record; + entity.CreatedOn = DateTime.Now; + entity.CreatedBy = Principal?.Identity; +} +``` + +> :information_source: HINT: To get the `Principal` you can add the following part into your `Startup.cs` which registers the `Principal` in Autofac and define a constructor Parameter on your `CustomDocumentMaterializer` of type `IPrincipal`. + +```C# + +configurator.OnApplicationLifetimeScopeCreating(builder => +{ +// ... +builder.Register(ctx => HttpContext.Current.GetOwinContext()).As(); +builder.Register((c, p) => + { + var owin = c.Resolve(); + return owin.Authentication.User; + }) + .As() + .InstancePerRequest(); +} +``` + + ## Set the context path of JSONAPI.EntityFramework Per default the routes created for the registered models from EntityFramework will appear in root folder. This can conflict with folders of the file system or other routes you may want to serve from the same project. From 90a399d30a7acd56ac29fe03787a538fa119a727 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Wed, 17 Aug 2016 20:11:15 +0200 Subject: [PATCH 178/186] changed AcceptanceTestBase to print out proper formatted and valid json to make compare and copy/paste to test files easier (#119) --- .../AcceptanceTestsBase.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs index 133548dc..d0594337 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -1,8 +1,10 @@ using System; using System.Data.Common; +using System.Globalization; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Formatting; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -11,6 +13,9 @@ using JSONAPI.Json; using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; using Owin; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests @@ -39,13 +44,28 @@ protected virtual async Task AssertResponseContent(HttpResponseMessage response, { var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); actualResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); + actualResponse.Should().Be(expectedResponse); } else { actualResponse = responseContent; + JsonSerializerSettings settings = new JsonSerializerSettings + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff+00:00", + Culture = CultureInfo.InvariantCulture, + Formatting = Formatting.Indented + }; + + var actualResponseJObject = JsonConvert.DeserializeObject(actualResponse) as JObject; + var expectedResponseJObject = JsonConvert.DeserializeObject(expectedResponse) as JObject; + var equals = JToken.DeepEquals(actualResponseJObject, expectedResponseJObject); + if (!equals) + { + Assert.Fail("should be: " + JsonConvert.SerializeObject(expectedResponseJObject, settings) + "\n but was: " + JsonConvert.SerializeObject(actualResponseJObject, settings)); + } } - actualResponse.Should().Be(expectedResponse); response.Content.Headers.ContentType.MediaType.Should().Be(JsonApiContentType); response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); From 4635c82e3bf1dd685caa7d74a5f49df8a29abd54 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Mon, 12 Sep 2016 19:25:23 +0200 Subject: [PATCH 179/186] Feat: implementation of includes (#118) * added basic IncludeExpressionExractor and introduced includes on EFDocumentMaterializer * added includes to FallbackDocumentBuilder * added tests for DefaultIncludeExpressionExtractor / fixed ugly typo * added includes in ToManyRelatedResourceDocument * removed obsolete method * includes on create and update * reverse deletion of GetDefaultSortExpressions * fixed typo * removed related to one and related to many methods from EntityFrameworkDocumentMaterializer because they are never used and replaced with EntityFrameworkToManyRelatedResourceDocumentMaterializer and EntityFrameworkToOneRelatedResourceDocumentMaterializer * a try for the GetIncludes method (renamed to GetNavigationPropertiesIncludes) * GetNavigationPropertiesIncludes added in EntityFrameworkToManyRelatedResourceDocumentMaterializer * bring back get default sort expression --- .../CreatingResourcesTests.cs | 29 ++++ .../FetchingResourcesTests.cs | 34 +++++ ..._empty_id_and_include_author_Response.json | 65 ++++++++ .../Get_included_to_one_response.json | 65 ++++++++ .../Get_related_to_many_include_response.json | 140 ++++++++++++++++++ ...ithAttributeUpdateWithIncludeResponse.json | 65 ++++++++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 4 + .../UpdatingResourcesTests.cs | 29 ++++ ...shipOfficersRelatedResourceMaterializer.cs | 3 +- JSONAPI.Autofac/JsonApiAutofacModule.cs | 1 + .../EntityFrameworkDocumentMaterializer.cs | 94 +++++------- ...ManyRelatedResourceDocumentMaterializer.cs | 34 ++++- .../Builders/FallbackDocumentBuilderTests.cs | 24 ++- .../DefaultIncludeExpressionExtractorTests.cs | 57 +++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + .../Builders/FallbackDocumentBuilder.cs | 10 +- .../Http/DefaultIncludeExpressionExtractor.cs | 21 +++ JSONAPI/Http/IIncludeExpressionExtractor.cs | 17 +++ JSONAPI/Http/JsonApiController.cs | 2 +- ...ManyRelatedResourceDocumentMaterializer.cs | 24 +-- JSONAPI/JSONAPI.csproj | 2 + 21 files changed, 641 insertions(+), 80 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json create mode 100644 JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs create mode 100644 JSONAPI/Http/DefaultIncludeExpressionExtractor.cs create mode 100644 JSONAPI/Http/IIncludeExpressionExtractor.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs index e1b5a308..b189e933 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs @@ -112,5 +112,34 @@ public async Task Post_with_empty_id() } } } + + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Post_with_empty_id_and_include() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts?include=author", @"Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_empty_id_and_include_author_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == "230"); + actualPost.Id.Should().Be("230"); + actualPost.Title.Should().Be("New post"); + actualPost.Content.Should().Be("The server generated my ID"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 04, 13, 12, 09, 0, new TimeSpan(0, 3, 0, 0))); + actualPost.AuthorId.Should().Be("401"); + } + } + } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs index 36321862..18e83042 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs @@ -99,6 +99,23 @@ public async Task Get_related_to_many() } } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_many_included() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/comments?include=author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_include_response.json", HttpStatusCode.OK); + } + } + [TestMethod] [DeploymentItem(@"Data\Comment.csv", @"Data")] [DeploymentItem(@"Data\Post.csv", @"Data")] @@ -115,6 +132,23 @@ public async Task Get_related_to_one() } } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_included_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201?include=author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_included_to_one_response.json", HttpStatusCode.OK); + } + } + [TestMethod] [DeploymentItem(@"Data\Comment.csv", @"Data")] [DeploymentItem(@"Data\Post.csv", @"Data")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json new file mode 100644 index 00000000..f9996946 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "230", + "attributes": { + "content": "The server generated my ID", + "created": "2015-04-13T09:09:00.0000000+00:00", + "title": "New post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/230/relationships/author", + "related": "https://www.example.com/posts/230/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/230/relationships/comments", + "related": "https://www.example.com/posts/230/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/230/relationships/tags", + "related": "https://www.example.com/posts/230/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json new file mode 100644 index 00000000..98fcd282 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json new file mode 100644 index 00000000..e1a01091 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json @@ -0,0 +1,140 @@ +{ + "data": [ + { + "type": "comments", + "id": "101", + "attributes": { + "created": "2015-01-31T14:30:00.0000000+00:00", + "text": "Comment 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/101/relationships/author", + "related": "https://www.example.com/comments/101/author" + }, + "data": { + "type": "users", + "id": "403" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/101/relationships/post", + "related": "https://www.example.com/comments/101/post" + } + } + } + }, + { + "type": "comments", + "id": "102", + "attributes": { + "created": "2015-01-31T14:35:00.0000000+00:00", + "text": "Comment 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/102/relationships/author", + "related": "https://www.example.com/comments/102/author" + }, + "data": { + "type": "users", + "id": "402" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/102/relationships/post", + "related": "https://www.example.com/comments/102/post" + } + } + } + }, + { + "type": "comments", + "id": "103", + "attributes": { + "created": "2015-01-31T14:41:00.0000000+00:00", + "text": "Comment 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/103/relationships/author", + "related": "https://www.example.com/comments/103/author" + }, + "data": { + "type": "users", + "id": "403" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/103/relationships/post", + "related": "https://www.example.com/comments/103/post" + } + } + } + } + ], + "included": [ + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json new file mode 100644 index 00000000..6bff40d9 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index b335e6c5..b8546fbf 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -271,6 +271,10 @@ + + + + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs index 48df90f9..e24ae49b 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs @@ -41,6 +41,35 @@ public async Task PatchWithAttributeUpdate() } } + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task PatchWithAttributeUpdateAndInclude() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202?include=author", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateWithIncludeResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + [TestMethod] [DeploymentItem(@"Data\PostID.csv", @"Data")] public async Task PatchWithAttributeUpdateID() diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs index 5cb186b3..9fb646b3 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs @@ -17,8 +17,9 @@ public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToMany public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, IResourceTypeRegistration primaryTypeRegistration) - : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, primaryTypeRegistration) + : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor, primaryTypeRegistration) { _dbContext = dbContext; } diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index d388dafc..9c57ca4a 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -166,6 +166,7 @@ protected override void Load(ContainerBuilder builder) // Misc builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); } } } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs index 0b86166f..114f7877 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -9,6 +9,7 @@ using JSONAPI.Core; using JSONAPI.Documents; using JSONAPI.Documents.Builders; +using JSONAPI.Extensions; using JSONAPI.Http; namespace JSONAPI.EntityFramework.Http @@ -24,6 +25,7 @@ public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer wher private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; private readonly ISortExpressionExtractor _sortExpressionExtractor; + private readonly IIncludeExpressionExtractor _includeExpressionExtractor; private readonly IBaseUrlService _baseUrlService; /// @@ -36,6 +38,7 @@ public EntityFrameworkDocumentMaterializer( ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, IBaseUrlService baseUrlService) { DbContext = dbContext; @@ -44,24 +47,27 @@ public EntityFrameworkDocumentMaterializer( _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; _sortExpressionExtractor = sortExpressionExtractor; + _includeExpressionExtractor = includeExpressionExtractor; _baseUrlService = baseUrlService; } public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) { - var query = DbContext.Set().AsQueryable(); var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); - return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var query = QueryIncludeNavigationProperties(null, GetNavigationPropertiesIncludes(includes)); + return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); } public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) { var apiBaseUrl = GetBaseUrlFromRequest(request); - var singleResource = await FilterById(id, _resourceTypeRegistration).FirstOrDefaultAsync(cancellationToken); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var singleResource = await FilterById(id, _resourceTypeRegistration, GetNavigationPropertiesIncludes(includes)).FirstOrDefaultAsync(cancellationToken); if (singleResource == null) throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", _resourceTypeRegistration.ResourceTypeName, id)); - return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null); + return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, includes, null); } public virtual async Task CreateRecord(ISingleResourceDocument requestDocument, @@ -71,7 +77,8 @@ public virtual async Task CreateRecord(ISingleResourceD var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); await OnCreate(newRecord); await DbContext.SaveChangesAsync(cancellationToken); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null); return returnDocument; } @@ -83,8 +90,9 @@ public virtual async Task UpdateRecord(string id, ISing var apiBaseUrl = GetBaseUrlFromRequest(request); var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); await OnUpdate(newRecord); - var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null); await DbContext.SaveChangesAsync(cancellationToken); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null); return returnDocument; } @@ -116,50 +124,7 @@ protected virtual async Task MaterializeAsync(IResourceObject resourceObject, return (T) await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken); } - /// - /// Generic method for getting the related resources for a to-many relationship - /// - protected async Task GetRelatedToMany(string id, - ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) - { - var param = Expression.Parameter(typeof(T)); - var accessorExpr = Expression.Property(param, relationship.Property); - var lambda = Expression.Lambda>>(accessorExpr, param); - - var primaryEntityQuery = FilterById(id, _resourceTypeRegistration); - - // We have to see if the resource even exists, so we can throw a 404 if it doesn't - var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); - if (relatedResource == null) - throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", - _resourceTypeRegistration.ResourceTypeName, id)); - - var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda); - var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); - - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken); - } - - /// - /// Generic method for getting the related resources for a to-one relationship - /// - protected async Task GetRelatedToOne(string id, - ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken) - { - var param = Expression.Parameter(typeof(T)); - var accessorExpr = Expression.Property(param, relationship.Property); - var lambda = Expression.Lambda>(accessorExpr, param); - - var primaryEntityQuery = FilterById(id, _resourceTypeRegistration); - var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken); - if (!primaryEntityExists) - throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", - _resourceTypeRegistration.ResourceTypeName, id)); - var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); - return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null, null); - } - - + /// /// Manipulate entity before create. /// @@ -189,11 +154,34 @@ protected virtual async Task OnDelete(Task record) await record; } - private IQueryable Filter(Expression> predicate, + /// + /// This method allows to include into query. + /// This can reduce the number of queries (eager loading) + /// + /// + /// + /// + protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) + { + List>> list = new List>>(); + foreach (var include in includes) + { + var incl = include.Pascalize(); + var param = Expression.Parameter(typeof(TResource)); + var lambda = + Expression.Lambda>( + Expression.PropertyOrField(param, incl),param); + list.Add(lambda); + } + return list.ToArray(); + } + + + private IQueryable QueryIncludeNavigationProperties(Expression> predicate, params Expression>[] includes) where TResource : class { IQueryable query = DbContext.Set(); - if (includes != null && includes.Any()) + if (includes != null && includes.Any()) // eager loading query = includes.Aggregate(query, (current, include) => current.Include(include)); if (predicate != null) @@ -208,7 +196,7 @@ private IQueryable FilterById(string id, IResourceTypeRegi var param = Expression.Parameter(typeof(TResource)); var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); var predicate = Expression.Lambda>(filterByIdExpression, param); - return Filter(predicate, includes); + return QueryIncludeNavigationProperties(predicate, includes); } } } diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs index 2a52338e..50004e16 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using JSONAPI.Core; using JSONAPI.Documents.Builders; +using JSONAPI.Extensions; using JSONAPI.Http; namespace JSONAPI.EntityFramework.Http @@ -29,8 +30,9 @@ public EntityFrameworkToManyRelatedResourceDocumentMaterializer( DbContext dbContext, IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, IResourceTypeRegistration primaryTypeRegistration) - : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor) + : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor) { _relationship = relationship; _dbContext = dbContext; @@ -43,8 +45,7 @@ protected override async Task> GetRelatedQuery(string prima var param = Expression.Parameter(typeof (TPrimaryResource)); var accessorExpr = Expression.Property(param, _relationship.Property); var lambda = Expression.Lambda>>(accessorExpr, param); - - var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration, GetNavigationPropertiesIncludes(Includes)); // We have to see if the resource even exists, so we can throw a 404 if it doesn't var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); @@ -56,6 +57,29 @@ protected override async Task> GetRelatedQuery(string prima return primaryEntityQuery.SelectMany(lambda); } + + /// + /// This method allows to include into query. + /// This can reduce the number of queries (eager loading) + /// + /// + /// + /// + protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) + { + List>> list = new List>>(); + foreach (var include in includes) + { + var incl = include.Pascalize(); + var param = Expression.Parameter(typeof(TResource)); + var lambda = + Expression.Lambda>( + Expression.PropertyOrField(param, incl), param); + list.Add(lambda); + } + return list.ToArray(); + } + private IQueryable FilterById(string id, IResourceTypeRegistration resourceTypeRegistration, params Expression>[] includes) where TResource : class @@ -63,10 +87,10 @@ private IQueryable FilterById(string id, var param = Expression.Parameter(typeof (TResource)); var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); var predicate = Expression.Lambda>(filterByIdExpression, param); - return Filter(predicate, includes); + return QueryIncludeNavigationProperties(predicate, includes); } - private IQueryable Filter(Expression> predicate, + private IQueryable QueryIncludeNavigationProperties(Expression> predicate, params Expression>[] includes) where TResource : class { IQueryable query = _dbContext.Set(); diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs index 24943f4a..a6b9f14d 100644 --- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -30,9 +30,10 @@ public async Task Creates_single_resource_document_for_registered_non_collection var objectContent = new Fruit { Id = "984", Name = "Kiwi" }; var mockDocument = new Mock(MockBehavior.Strict); + var includePathExpression = new string[] {}; var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); - singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null, null, null)).Returns(mockDocument.Object); + singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), includePathExpression, null, null)).Returns(mockDocument.Object); var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); @@ -44,11 +45,14 @@ public async Task Creates_single_resource_document_for_registered_non_collection mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com"); var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); - mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new [] { "id "}); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " }); + + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includePathExpression); // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token); // Assert @@ -75,12 +79,14 @@ public async Task Creates_resource_collection_document_for_queryables() mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); var sortExpressions = new[] { "id" }; + + var includeExpressions = new string[] { }; var cancellationTokenSource = new CancellationTokenSource(); var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); mockQueryableDocumentBuilder - .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, null)) + .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, includeExpressions)) .Returns(Task.FromResult(mockDocument.Object)); var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); @@ -88,9 +94,12 @@ public async Task Creates_resource_collection_document_for_queryables() var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions); + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includeExpressions); + // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); // Assert @@ -127,9 +136,12 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " }); + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(new string[] { }); + // Act var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, - mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object); + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); // Assert diff --git a/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs new file mode 100644 index 00000000..0fb65380 --- /dev/null +++ b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs @@ -0,0 +1,57 @@ +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class DefaultIncludeExpressionExtractorTests + { + [TestMethod] + public void ExtractsSingleIncludeExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?include=boss"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpressions = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpressions.Should().BeEquivalentTo("boss"); + } + + + [TestMethod] + public void ExtractsMultipleIncludeExpressionsFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?include=boss,office-address"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpressions = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpressions.Should().BeEquivalentTo("boss", "office-address"); + } + + [TestMethod] + public void ExtractsNothingWhenThereIsNoIncludeParam() + { + // Arrange + const string uri = "http://api.example.com/dummies"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpression = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpression.Length.Should().Be(0); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 4a86feae..7f487003 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -91,6 +91,7 @@ + diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs index f6882571..8208f274 100644 --- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs @@ -18,6 +18,7 @@ public class FallbackDocumentBuilder : IFallbackDocumentBuilder private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; private readonly IResourceCollectionDocumentBuilder _resourceCollectionDocumentBuilder; private readonly ISortExpressionExtractor _sortExpressionExtractor; + private readonly IIncludeExpressionExtractor _includeExpressionExtractor; private readonly IBaseUrlService _baseUrlService; private readonly Lazy _openBuildDocumentFromQueryableMethod; private readonly Lazy _openBuildDocumentFromEnumerableMethod; @@ -29,12 +30,14 @@ public FallbackDocumentBuilder(ISingleResourceDocumentBuilder singleResourceDocu IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, IBaseUrlService baseUrlService) { _singleResourceDocumentBuilder = singleResourceDocumentBuilder; _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _resourceCollectionDocumentBuilder = resourceCollectionDocumentBuilder; _sortExpressionExtractor = sortExpressionExtractor; + _includeExpressionExtractor = includeExpressionExtractor; _baseUrlService = baseUrlService; _openBuildDocumentFromQueryableMethod = @@ -53,6 +56,9 @@ public async Task BuildDocument(object obj, HttpRequestMessage { var type = obj.GetType(); + // TODO: test includes + var includeExpressions = _includeExpressionExtractor.ExtractIncludeExpressions(requestMessage); + var queryableInterfaces = type.GetInterfaces(); var queryableInterface = queryableInterfaces.FirstOrDefault( @@ -66,7 +72,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(requestMessage); dynamic materializedQueryTask = buildDocumentMethod.Invoke(_queryableResourceCollectionDocumentBuilder, - new[] { obj, requestMessage, sortExpressions, cancellationToken, null }); + new[] { obj, requestMessage, sortExpressions, cancellationToken, includeExpressions }); return await materializedQueryTask; } @@ -89,7 +95,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage } // Single resource object - return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, null, null); + return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, includeExpressions, null); } private static Type GetEnumerableElementType(Type collectionType) diff --git a/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs b/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs new file mode 100644 index 00000000..cb040b19 --- /dev/null +++ b/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.Http +{ + /// + /// Default implementation of + /// + public class DefaultIncludeExpressionExtractor: IIncludeExpressionExtractor + { + private const string IncludeQueryParamKey = "include"; + + public string[] ExtractIncludeExpressions(HttpRequestMessage requestMessage) + { + var queryParams = requestMessage.GetQueryNameValuePairs(); + var includeParam = queryParams.FirstOrDefault(kvp => kvp.Key == IncludeQueryParamKey); + if (includeParam.Key != IncludeQueryParamKey) return new string[] { }; + return includeParam.Value.Split(','); + } + } +} diff --git a/JSONAPI/Http/IIncludeExpressionExtractor.cs b/JSONAPI/Http/IIncludeExpressionExtractor.cs new file mode 100644 index 00000000..c8cf1358 --- /dev/null +++ b/JSONAPI/Http/IIncludeExpressionExtractor.cs @@ -0,0 +1,17 @@ +using System.Net.Http; + +namespace JSONAPI.Http +{ + /// + /// Service to extract include expressions from an HTTP request + /// + public interface IIncludeExpressionExtractor + { + /// + /// Extracts include expressions from the request + /// + /// + /// + string[] ExtractIncludeExpressions(HttpRequestMessage requestMessage); + } +} diff --git a/JSONAPI/Http/JsonApiController.cs b/JSONAPI/Http/JsonApiController.cs index 09deb1b1..8bc9bf22 100644 --- a/JSONAPI/Http/JsonApiController.cs +++ b/JSONAPI/Http/JsonApiController.cs @@ -63,7 +63,7 @@ public virtual async Task Post(string resourceType, [FromBody } /// - /// Updates the record with the given ID with data from the request payloaad. + /// Updates the record with the given ID with data from the request payload. /// public virtual async Task Patch(string resourceType, string id, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken) { diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs index 7d190960..6aa2286f 100644 --- a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs @@ -15,27 +15,36 @@ public abstract class QueryableToManyRelatedResourceDocumentMaterializer + /// List of includes given by url. + /// + protected string[] Includes = {}; /// /// Creates a new QueryableRelatedResourceDocumentMaterializer /// protected QueryableToManyRelatedResourceDocumentMaterializer( IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, - ISortExpressionExtractor sortExpressionExtractor) + ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor) { _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; _sortExpressionExtractor = sortExpressionExtractor; + _includeExpressionExtractor = includeExpressionExtractor; } public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request, CancellationToken cancellationToken) { + Includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); var query = await GetRelatedQuery(primaryResourceId, cancellationToken); - var includes = GetIncludePaths(); var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); if (sortExpressions == null || sortExpressions.Length < 1) sortExpressions = GetDefaultSortExpressions(); - return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); // TODO: allow implementors to specify metadata + + + return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, Includes); // TODO: allow implementors to specify metadata } /// @@ -43,15 +52,6 @@ public async Task GetRelatedResourceDocument(string primaryRes /// protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken); - /// - /// Gets a list of relationship paths to include - /// - /// - protected virtual string[] GetIncludePaths() - { - return null; - } - /// /// If the client doesn't request any sort expressions, these expressions will be used for sorting instead. /// diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index d6ed5c23..4336261f 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -90,10 +90,12 @@ + + From 4374719e79c24778f2af2f3229fdb541b1106caf Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 15 Sep 2016 20:09:25 +0200 Subject: [PATCH 180/186] Feat: added metadata on pagination responses (#115) * Feat: added pagination with metadata - metadata to indicate the count of pages and total count of records - acceptance tests pagination in combination with filters and sorting * moved total-pages and total-count to Entity Framework specific subclass. reduced the number of count calls to save roundtrips * awaits corrected --- .../Pagination/GetAllResponsePaged-2-2.json | 66 ++++++++++++++++ .../GetFilterPaged-1-2-sorted-desc.json | 64 +++++++++++++++ .../Pagination/GetFilterPaged-1-2-sorted.json | 64 +++++++++++++++ .../Pagination/GetFilterPaged-2-1.json | 36 +++++++++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 6 ++ .../PaginationTests.cs | 77 +++++++++++++++++++ .../Startup.cs | 4 + ...ryableResourceCollectionDocumentBuilder.cs | 55 +++++++++++++ .../JSONAPI.EntityFramework.csproj | 1 + ...tionDocument_for_all_possible_members.json | 20 ++--- ...nt_for_primary_data_only_and_metadata.json | 10 +-- ...ryableResourceCollectionDocumentBuilder.cs | 2 +- JSONAPI/Documents/Metadata.cs | 16 ++++ JSONAPI/JSONAPI.csproj | 1 + .../ResourceCollectionDocumentFormatter.cs | 12 +-- README.md | 10 +++ 16 files changed, 423 insertions(+), 21 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs create mode 100644 JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs create mode 100644 JSONAPI/Documents/Metadata.cs diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json new file mode 100644 index 00000000..7bf2f5ba --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json @@ -0,0 +1,66 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 4 + }, + "data": [ + { + "type": "posts", + "id": "203", + "attributes": { + "content": "Post 3 content", + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + } + } + } + }, + { + "type": "posts", + "id": "204", + "attributes": { + "content": "Post 4 content", + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json new file mode 100644 index 00000000..b65cea48 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json @@ -0,0 +1,64 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json new file mode 100644 index 00000000..ee562655 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json @@ -0,0 +1,64 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json new file mode 100644 index 00000000..16e92908 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json @@ -0,0 +1,36 @@ +{ + "meta": { + "total-pages": 3, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index b8546fbf..e9cb8a2c 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -106,6 +106,7 @@ + @@ -275,8 +276,13 @@ + + + + + diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs new file mode 100644 index 00000000..0db20f57 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs @@ -0,0 +1,77 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class PaginationTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetPage2Post2() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?page[number]=1&page[size]=2"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetAllResponsePaged-2-2.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=1&page[size]=1"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-2-1.json", HttpStatusCode.OK); + } + } + + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilterSortedAscTest() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=first-name"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted.json", HttpStatusCode.OK); + } + } + + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilterSortedDescTest() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=-first-name"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted-desc.json", HttpStatusCode.OK); + } + } + + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index aec594d2..678f8532 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -14,6 +14,8 @@ using JSONAPI.EntityFramework.Configuration; using Owin; using System.Collections.Generic; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Documents.Builders; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp { @@ -112,6 +114,8 @@ internal JsonApiHttpAutofacConfigurator BuildAutofacConfigurator(IAppBuilder app builder.RegisterType() .As(); builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + builder.RegisterType().As(); + }); configurator.OnApplicationLifetimeScopeBegun(applicationLifetimeScope => { diff --git a/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs new file mode 100644 index 00000000..1bd9e2c8 --- /dev/null +++ b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.EntityFramework.Documents.Builders +{ + /// + /// Provides a entity framework implementation of an IQueryableResourceCollectionDocumentBuilder + /// + public class EntityFrameworkQueryableResourceCollectionDocumentBuilder: DefaultQueryableResourceCollectionDocumentBuilder + { + /// + /// Creates a new EntityFrameworkQueryableResourceCollectionDocumentBuilder + /// + public EntityFrameworkQueryableResourceCollectionDocumentBuilder( + IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, + IQueryableEnumerationTransformer enumerationTransformer, + IQueryableFilteringTransformer filteringTransformer, + IQueryableSortingTransformer sortingTransformer, + IQueryablePaginationTransformer paginationTransformer, + IBaseUrlService baseUrlService) : + base(resourceCollectionDocumentBuilder, + enumerationTransformer, + filteringTransformer, + sortingTransformer, + paginationTransformer, + baseUrlService) + { + } + + /// + /// Returns the metadata that should be sent with this document. + /// + protected override async Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery, + IPaginationTransformResult paginationResult, CancellationToken cancellationToken) + { + var metadata = new Metadata(); + if (paginationResult.PaginationWasApplied) + { + var count = await filteredQuery.CountAsync(cancellationToken); + metadata.MetaObject.Add("total-pages", (int)Math.Ceiling((decimal) count / paginationResult.PageSize)); + metadata.MetaObject.Add("total-count", count); + } + if (metadata.MetaObject.HasValues) + return metadata; + return null; + } + } +} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 0f1dd5c8..2045b33d 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -72,6 +72,7 @@ + diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json index 9eb2c810..fad9ad33 100644 --- a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json +++ b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json @@ -1,12 +1,12 @@ { - "data": [ - "Primary data 1", - "Primary data 2" - ], - "included": [ - "Related data object 1", - "Related data object 2", - "Related data object 3" - ], - "meta": "Placeholder metadata object" + "meta": "Placeholder metadata object", + "data": [ + "Primary data 1", + "Primary data 2" + ], + "included": [ + "Related data object 1", + "Related data object 2", + "Related data object 3" + ] } \ No newline at end of file diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json index ef98daf7..6d770373 100644 --- a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json +++ b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json @@ -1,7 +1,7 @@ { - "data": [ - "Primary data 1", - "Primary data 2" - ], - "meta": "Placeholder metadata object" + "meta": "Placeholder metadata object", + "data": [ + "Primary data 1", + "Primary data 2" + ] } \ No newline at end of file diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs index d47e93c6..d0488b1c 100644 --- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs +++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs @@ -60,7 +60,7 @@ public async Task BuildDocument(IQueryable qu protected virtual Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery, IPaginationTransformResult paginationResult, CancellationToken cancellationToken) { - return Task.FromResult((IMetadata)null); + return Task.FromResult((IMetadata) null); } } } diff --git a/JSONAPI/Documents/Metadata.cs b/JSONAPI/Documents/Metadata.cs new file mode 100644 index 00000000..004de8c7 --- /dev/null +++ b/JSONAPI/Documents/Metadata.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Documents +{ + /// + /// Default implementation of + /// + public class Metadata : IMetadata + { + public Metadata() + { + MetaObject = new JObject(); + } + public JObject MetaObject { get; } + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 4336261f..9bd3d2e2 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -91,6 +91,7 @@ + diff --git a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs index 79cd75d8..b7a0c999 100644 --- a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs +++ b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs @@ -75,6 +75,12 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) { writer.WriteStartObject(); + if (document.Metadata != null) + { + writer.WritePropertyName(MetaKeyName); + MetadataFormatter.Serialize(document.Metadata, writer); + } + writer.WritePropertyName(PrimaryDataKeyName); writer.WriteStartArray(); @@ -95,11 +101,7 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer) writer.WriteEndArray(); } - if (document.Metadata != null) - { - writer.WritePropertyName(MetaKeyName); - MetadataFormatter.Serialize(document.Metadata, writer); - } + writer.WriteEndObject(); diff --git a/README.md b/README.md index 129060b7..1fb411ca 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,13 @@ configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.exa ``` +# Metadata + + + +## Pagination + +### total-pages / total-count +When using Entity Framework you can register type `EntityFrameworkQueryableResourceCollectionDocumentBuilder` to enable the `total-pages` and `total-count` meta properties. +When pagination is used the `total-pages` and `total-count` meta properties are provided to indicate the number of pages and records to the client. + From dfef2787acf856d3be7fc534240d9ec63ce4f378 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 15 Sep 2016 22:23:01 +0200 Subject: [PATCH 181/186] bugfix: includes (eager-loading) must be applied to related not to primary resource! (#122) --- .../FetchingResourcesTests.cs | 17 ++ ...ted_to_many_include_external_response.json | 164 ++++++++++++++++++ ...sts.EntityFrameworkTestWebApp.Tests.csproj | 1 + ...ManyRelatedResourceDocumentMaterializer.cs | 36 ++-- 4 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs index 18e83042..39eb2938 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs @@ -116,6 +116,23 @@ public async Task Get_related_to_many_included() } } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_related_to_many_included_external() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users/401/posts?include=tags"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_include_external_response.json", HttpStatusCode.OK); + } + } + [TestMethod] [DeploymentItem(@"Data\Comment.csv", @"Data")] [DeploymentItem(@"Data\Post.csv", @"Data")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json new file mode 100644 index 00000000..e8008645 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json @@ -0,0 +1,164 @@ +{ + "data": [ + { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + }, + "data": [ + { + "type": "tags", + "id": "301" + }, + { + "type": "tags", + "id": "302" + } + ] + } + } + }, + { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + }, + "data": [ + { + "type": "tags", + "id": "302" + }, + { + "type": "tags", + "id": "303" + } + ] + } + } + }, + { + "type": "posts", + "id": "203", + "attributes": { + "content": "Post 3 content", + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + }, + "data": [ + { + "type": "tags", + "id": "303" + } + ] + } + } + } + ], + "included": [ + { + "type": "tags", + "id": "301", + "attributes": { + "name": "Tag A" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/301/relationships/posts", + "related": "https://www.example.com/tags/301/posts" + } + } + } + }, + { + "type": "tags", + "id": "302", + "attributes": { + "name": "Tag B" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/302/relationships/posts", + "related": "https://www.example.com/tags/302/posts" + } + } + } + }, + { + "type": "tags", + "id": "303", + "attributes": { + "name": "Tag C" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/303/relationships/posts", + "related": "https://www.example.com/tags/303/posts" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index e9cb8a2c..35d82430 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -280,6 +280,7 @@ + diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs index 50004e16..142075a0 100644 --- a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs @@ -45,7 +45,8 @@ protected override async Task> GetRelatedQuery(string prima var param = Expression.Parameter(typeof (TPrimaryResource)); var accessorExpr = Expression.Property(param, _relationship.Property); var lambda = Expression.Lambda>>(accessorExpr, param); - var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration, GetNavigationPropertiesIncludes(Includes)); + + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); // We have to see if the resource even exists, so we can throw a 404 if it doesn't var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); @@ -53,8 +54,12 @@ protected override async Task> GetRelatedQuery(string prima throw JsonApiException.CreateForNotFound(string.Format( "No resource of type `{0}` exists with id `{1}`.", _primaryTypeRegistration.ResourceTypeName, primaryResourceId)); + var includes = GetNavigationPropertiesIncludes(Includes); + var query = primaryEntityQuery.SelectMany(lambda); - return primaryEntityQuery.SelectMany(lambda); + if (includes != null && includes.Any()) + query = includes.Aggregate(query, (current, include) => current.Include(include)); + return query; } @@ -62,18 +67,17 @@ protected override async Task> GetRelatedQuery(string prima /// This method allows to include into query. /// This can reduce the number of queries (eager loading) /// - /// /// /// - protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) + protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) { - List>> list = new List>>(); + List>> list = new List>>(); foreach (var include in includes) { var incl = include.Pascalize(); - var param = Expression.Parameter(typeof(TResource)); + var param = Expression.Parameter(typeof(TRelated)); var lambda = - Expression.Lambda>( + Expression.Lambda>( Expression.PropertyOrField(param, incl), param); list.Add(lambda); } @@ -81,26 +85,14 @@ protected virtual Expression>[] GetNavigationPropertiesI } private IQueryable FilterById(string id, - IResourceTypeRegistration resourceTypeRegistration, - params Expression>[] includes) where TResource : class + IResourceTypeRegistration resourceTypeRegistration) where TResource : class { var param = Expression.Parameter(typeof (TResource)); var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); var predicate = Expression.Lambda>(filterByIdExpression, param); - return QueryIncludeNavigationProperties(predicate, includes); - } - - private IQueryable QueryIncludeNavigationProperties(Expression> predicate, - params Expression>[] includes) where TResource : class - { IQueryable query = _dbContext.Set(); - if (includes != null && includes.Any()) - query = includes.Aggregate(query, (current, include) => current.Include(include)); - - if (predicate != null) - query = query.Where(predicate); - - return query.AsQueryable(); + return query.Where(predicate); } + } } From 7448fa87477510fb1dd8094c083cf1490afc5904 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Thu, 15 Sep 2016 19:12:44 -0400 Subject: [PATCH 182/186] Treat JToken properties as primitive attributes for serialization (#123) --- ..._of_various_types_serialize_correctly.json | 193 ++++++++++-------- .../Controllers/SamplesController.cs | 26 ++- .../Models/Sample.cs | 7 + .../Extensions/TypeExtensionsTests.cs | 4 + JSONAPI/Extensions/TypeExtensions.cs | 3 +- 5 files changed, 144 insertions(+), 89 deletions(-) diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json index ba0978cf..ca94936a 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json @@ -1,89 +1,110 @@ { - "data": [ - { - "type": "samples", - "id": "1", - "attributes": { - "boolean-field": false, - "byte-field": 0, - "complex-attribute-field": null, - "date-time-field": "0001-01-01T00:00:00", - "date-time-offset-field": "0001-01-01T00:00:00.0000000+00:00", - "decimal-field": "0", - "double-field": 0.0, - "enum-field": 0, - "guid-field": "00000000-0000-0000-0000-000000000000", - "int16-field": 0, - "int32-field": 0, - "int64-field": 0, - "nullable-boolean-field": false, - "nullable-byte-field": null, - "nullable-date-time-field": null, - "nullable-date-time-offset-field": null, - "nullable-decimal-field": null, - "nullable-double-field": null, - "nullable-enum-field": null, - "nullable-guid-field": null, - "nullable-int16-field": null, - "nullable-int32-field": null, - "nullable-int64-field": null, - "nullable-sbyte-field": null, - "nullable-single-field": null, - "nullable-uint16-field": null, - "nullable-uint32-field": null, - "nullable-uint64-field": null, - "sbyte-field": 0, - "single-field": 0.0, - "string-field": null, - "uint16-field": 0, - "uint32-field": 0, - "uint64-field": 0 - } + "data": [ + { + "type": "samples", + "id": "1", + "attributes": { + "boolean-field": false, + "byte-field": 0, + "complex-attribute-field": null, + "date-time-field": "0001-01-01T00:00:00", + "date-time-offset-field": "0001-01-01T00:00:00.0000000+00:00", + "decimal-field": "0", + "double-field": 0.0, + "enum-field": 0, + "guid-field": "00000000-0000-0000-0000-000000000000", + "int16-field": 0, + "int32-field": 0, + "int64-field": 0, + "j-token-array-field": null, + "j-token-object-field": null, + "j-token-string-field": null, + "nullable-boolean-field": false, + "nullable-byte-field": null, + "nullable-date-time-field": null, + "nullable-date-time-offset-field": null, + "nullable-decimal-field": null, + "nullable-double-field": null, + "nullable-enum-field": null, + "nullable-guid-field": null, + "nullable-int16-field": null, + "nullable-int32-field": null, + "nullable-int64-field": null, + "nullable-sbyte-field": null, + "nullable-single-field": null, + "nullable-uint16-field": null, + "nullable-uint32-field": null, + "nullable-uint64-field": null, + "sbyte-field": 0, + "single-field": 0.0, + "string-field": null, + "uint16-field": 0, + "uint32-field": 0, + "uint64-field": 0 + } + }, + { + "type": "samples", + "id": "2", + "attributes": { + "boolean-field": true, + "byte-field": 253, + "complex-attribute-field": { + "foo": { + "baz": [ 11 ] + }, + "bar": 5 }, - { - "type": "samples", - "id": "2", - "attributes": { - "boolean-field": true, - "byte-field": 253, - "complex-attribute-field": { - "foo": { - "baz": [ 11 ] - }, - "bar": 5 - }, - "date-time-field": "1776-07-04T00:00:00", - "date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", - "decimal-field": "1056789.123", - "double-field": 1056789.123, - "enum-field": 1, - "guid-field": "6566f9b4-5245-40de-890d-98b40a4ad656", - "int16-field": 32000, - "int32-field": 2000000000, - "int64-field": 9223372036854775807, - "nullable-boolean-field": true, - "nullable-byte-field": 253, - "nullable-date-time-field": "1776-07-04T00:00:00", - "nullable-date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", - "nullable-decimal-field": "1056789.123", - "nullable-double-field": 1056789.123, - "nullable-enum-field": 2, - "nullable-guid-field": "3d1fb81e-43ee-4d04-af91-c8a326341293", - "nullable-int16-field": 32000, - "nullable-int32-field": 2000000000, - "nullable-int64-field": 9223372036854775807, - "nullable-sbyte-field": 123, - "nullable-single-field": 1056789.13, - "nullable-uint16-field": 64000, - "nullable-uint32-field": 3000000000, - "nullable-uint64-field": 9223372036854775808, - "sbyte-field": 123, - "single-field": 1056789.13, - "string-field": "Some string 156", - "uint16-field": 64000, - "uint32-field": 3000000000, - "uint64-field": 9223372036854775808 - } - } - ] + "date-time-field": "1776-07-04T00:00:00", + "date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "decimal-field": "1056789.123", + "double-field": 1056789.123, + "enum-field": 1, + "guid-field": "6566f9b4-5245-40de-890d-98b40a4ad656", + "int16-field": 32000, + "int32-field": 2000000000, + "int64-field": 9223372036854775807, + "j-token-array-field": [ + { + "my-field1": "George Washington", + "overridden-field2": null, + "MyField3": 216 + }, + { + "my-field1": "Thomas Jefferson", + "overridden-field2": false, + "MyField3": 631 + } + ], + "j-token-object-field": { + "my-field1": "Abraham Lincoln", + "overridden-field2": true, + "MyField3": 439 + }, + "j-token-string-field": "Andrew Jackson", + "nullable-boolean-field": true, + "nullable-byte-field": 253, + "nullable-date-time-field": "1776-07-04T00:00:00", + "nullable-date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "nullable-decimal-field": "1056789.123", + "nullable-double-field": 1056789.123, + "nullable-enum-field": 2, + "nullable-guid-field": "3d1fb81e-43ee-4d04-af91-c8a326341293", + "nullable-int16-field": 32000, + "nullable-int32-field": 2000000000, + "nullable-int64-field": 9223372036854775807, + "nullable-sbyte-field": 123, + "nullable-single-field": 1056789.13, + "nullable-uint16-field": 64000, + "nullable-uint32-field": 3000000000, + "nullable-uint64-field": 9223372036854775808, + "sbyte-field": 123, + "single-field": 1056789.13, + "string-field": "Some string 156", + "uint16-field": 64000, + "uint32-field": 3000000000, + "uint64-field": 9223372036854775808 + } + } + ] } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs index 66588415..4d6d7546 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs @@ -1,6 +1,8 @@ using System; using System.Web.Http; using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers { @@ -44,7 +46,10 @@ public IHttpActionResult GetSamples() StringField = null, EnumField = default(SampleEnum), NullableEnumField = null, - ComplexAttributeField = null + ComplexAttributeField = null, + JTokenStringField = null, + JTokenObjectField = null, + JTokenArrayField = null }; var s2 = new Sample { @@ -82,10 +87,27 @@ public IHttpActionResult GetSamples() StringField = "Some string 156", EnumField = SampleEnum.Value1, NullableEnumField = SampleEnum.Value2, - ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}", + JTokenStringField = "Andrew Jackson", + JTokenObjectField = JToken.FromObject(new SomeSerializableClass { MyField1 = "Abraham Lincoln", MyField2 = true, MyField3 = 439 }), + JTokenArrayField = new JArray( + JToken.FromObject(new SomeSerializableClass { MyField1 = "George Washington", MyField2 = null, MyField3 = 216 }), + JToken.FromObject(new SomeSerializableClass { MyField1 = "Thomas Jefferson", MyField2 = false, MyField3 = 631 })) }; return Ok(new[] { s1, s2 }); } + + [Serializable] + public class SomeSerializableClass + { + [JsonProperty("my-field1")] + public string MyField1 { get; set; } + + [JsonProperty("overridden-field2")] + public bool? MyField2 { get; set; } + + public int MyField3 { get; set; } + } } } \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs index c56ea671..e8096599 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs @@ -1,5 +1,6 @@ using System; using JSONAPI.Attributes; +using Newtonsoft.Json.Linq; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { @@ -48,5 +49,11 @@ public class Sample [SerializeAsComplex] public string ComplexAttributeField { get; set; } + + public JToken JTokenStringField { get; set; } + + public JToken JTokenObjectField { get; set; } + + public JToken JTokenArrayField { get; set; } } } diff --git a/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs b/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs index 19b51b69..9d593abc 100644 --- a/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs +++ b/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs @@ -1,6 +1,7 @@ using System; using JSONAPI.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Extensions { @@ -46,6 +47,9 @@ public void CanWriteAsJsonApiAttributeTest() Assert.IsTrue(typeof(String).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for String!"); Assert.IsTrue(typeof(TestEnum).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for enum!"); Assert.IsTrue(typeof(TestEnum?).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for nullable enum!"); + Assert.IsTrue(typeof(JToken).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for JToken!"); + Assert.IsTrue(typeof(JObject).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for JObject!"); + Assert.IsTrue(typeof(JArray).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for Jarray!"); Assert.IsFalse(typeof(Object).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for Object!"); } diff --git a/JSONAPI/Extensions/TypeExtensions.cs b/JSONAPI/Extensions/TypeExtensions.cs index 8df43e56..c7d849ca 100644 --- a/JSONAPI/Extensions/TypeExtensions.cs +++ b/JSONAPI/Extensions/TypeExtensions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace JSONAPI.Extensions { @@ -17,6 +17,7 @@ public static bool CanWriteAsJsonApiAttribute(this Type objectType) || typeof (DateTime).IsAssignableFrom(objectType) || typeof (DateTimeOffset).IsAssignableFrom(objectType) || typeof (String).IsAssignableFrom(objectType) + || typeof (JToken).IsAssignableFrom(objectType) || objectType.IsEnum; } From f4f2862ca0df8d8bd6c9e5be87507bb22ddb09b9 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 30 Nov 2016 12:58:55 -0500 Subject: [PATCH 183/186] make module constructor public --- JSONAPI.Autofac/JsonApiAutofacModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index 9c57ca4a..dd248aa8 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -16,7 +16,7 @@ public class JsonApiAutofacModule : Module { private readonly IJsonApiConfiguration _jsonApiConfiguration; - internal JsonApiAutofacModule(IJsonApiConfiguration jsonApiConfiguration) + public JsonApiAutofacModule(IJsonApiConfiguration jsonApiConfiguration) { _jsonApiConfiguration = jsonApiConfiguration; } From 2e0226a2fc1792b9267a28176a66889b058b5108 Mon Sep 17 00:00:00 2001 From: jakubozga Date: Mon, 12 Dec 2016 16:24:45 +0100 Subject: [PATCH 184/186] Added static method MethodNotAllowed in JsonApiException (#127) --- JSONAPI/Documents/Builders/JsonApiException.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/JSONAPI/Documents/Builders/JsonApiException.cs b/JSONAPI/Documents/Builders/JsonApiException.cs index 00472ea0..4b72176c 100644 --- a/JSONAPI/Documents/Builders/JsonApiException.cs +++ b/JSONAPI/Documents/Builders/JsonApiException.cs @@ -99,5 +99,20 @@ public static JsonApiException CreateForForbidden(string detail = null) }; return new JsonApiException(error); } + + /// + /// Creates a JsonApiException to send a 405 Method Not Allowed error. + /// + public static JsonApiException CreateForMethodNotAllowed(string detail = null) + { + var error = new Error + { + Id = Guid.NewGuid().ToString(), + Status = HttpStatusCode.MethodNotAllowed, + Title = "Method not allowed", + Detail = detail + }; + return new JsonApiException(error); + } } } From 751296d5082eaad01e5588c35ada0a0669bf5ece Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 4 Apr 2017 16:56:03 -0400 Subject: [PATCH 185/186] Update README.md --- README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1fb411ca..ee1dc2e9 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,11 @@ JSONAPI.NET =========== [![jsonapi MyGet Build Status](https://www.myget.org/BuildSource/Badge/jsonapi?identifier=caf48269-c15b-4850-a29e-b41a23d9854d)](https://www.myget.org/) -News ----- +_Deprecation notice_ +------------------ -The NuGet packages are out! -* [JSONAPI](https://www.nuget.org/packages/JSONAPI/) -* [JSONAPI.EntityFramework](https://www.nuget.org/packages/JSONAPI.EntityFramework/) +JSONAPI.NET is no longer actively maintained. It is not recommended for use in new projects. It does NOT work with .NET Core. Please consider [one of the other .NET server implementations](http://jsonapi.org/implementations/#server-libraries-net) instead. -JSON API Compliance ----------------------- - -The master branch is roughly compatible with the RC3 version of JSON API. The major missing feature is inclusion of related resources. Many changes made to the spec since RC3 are not yet available in this library. Full 1.0 compliance is planned, so stay tuned! What is JSONAPI.NET? ==================== From aee11a70dbaa00620944b80506c3d34dc283216f Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Fri, 23 Feb 2018 16:48:40 -0500 Subject: [PATCH 186/186] give JsonApiException more descriptive message --- JSONAPI/Documents/Builders/JsonApiException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI/Documents/Builders/JsonApiException.cs b/JSONAPI/Documents/Builders/JsonApiException.cs index 4b72176c..ee6d04f9 100644 --- a/JSONAPI/Documents/Builders/JsonApiException.cs +++ b/JSONAPI/Documents/Builders/JsonApiException.cs @@ -19,7 +19,7 @@ public class JsonApiException : Exception /// Creates a new JsonApiException /// /// - public JsonApiException(IError error) + public JsonApiException(IError error) : base(error?.Detail ?? "An error occurred in JSONAPI") { Error = error; }