From bf9e4c85b54b2f2e8810fb103de7ba399d74e48e Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Tue, 9 Jun 2015 18:45:25 -0400 Subject: [PATCH 001/122] 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 002/122] 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 003/122] 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 004/122] 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 005/122] 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 006/122] 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 007/122] 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 008/122] 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 009/122] 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 010/122] 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 011/122] 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 012/122] 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 013/122] 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 014/122] 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 015/122] 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 016/122] 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 017/122] 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 036/122] 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 037/122] 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 038/122] 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 039/122] 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 040/122] 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 041/122] 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 042/122] 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 043/122] 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 044/122] 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 045/122] 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 046/122] 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 047/122] 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 048/122] 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 049/122] 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 050/122] 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 051/122] 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 052/122] 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 053/122] 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 054/122] 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 055/122] 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 056/122] 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 057/122] 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 058/122] 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 059/122] 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 060/122] 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 061/122] 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 062/122] 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 063/122] 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 064/122] 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 065/122] 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 066/122] 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 067/122] 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 068/122] 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 069/122] 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 070/122] 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 071/122] 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 072/122] 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 073/122] 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 074/122] 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 075/122] 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 076/122] 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 077/122] 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 078/122] 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 079/122] 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 080/122] 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 081/122] 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 082/122] 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 083/122] 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 084/122] 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 085/122] 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 086/122] 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 087/122] 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 088/122] 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 089/122] 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 090/122] 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 091/122] 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 092/122] 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 093/122] 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 094/122] 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 095/122] 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 096/122] 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 097/122] 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 098/122] 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 099/122] 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 100/122] 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 101/122] 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 102/122] 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 103/122] 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 104/122] 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 105/122] 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 106/122] 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 107/122] 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 108/122] 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 109/122] 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 110/122] 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 111/122] 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 112/122] 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 113/122] 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 114/122] 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 115/122] 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 116/122] 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 117/122] 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 118/122] 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 119/122] 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 120/122] 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 121/122] 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 122/122] 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; }