From aaabb1ae730831ad97edb303b154e72495af2096 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:10:59 -0500 Subject: [PATCH 01/43] feat(meta): add a meta builder class --- .../Builders/IMetaBuilder.cs | 11 +++ src/JsonApiDotNetCore/Builders/MetaBuilder.cs | 31 ++++++++ .../Unit/Builders/MetaBuilderTests.cs | 73 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/JsonApiDotNetCore/Builders/IMetaBuilder.cs create mode 100644 src/JsonApiDotNetCore/Builders/MetaBuilder.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs diff --git a/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs b/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs new file mode 100644 index 0000000000..bf35b9d210 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Builders +{ + public interface IMetaBuilder + { + void Add(string key, object value); + void Add(Dictionary values); + Dictionary Build(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Builders/MetaBuilder.cs new file mode 100644 index 0000000000..14b80321f6 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/MetaBuilder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; + +namespace JsonApiDotNetCore.Builders +{ + public class MetaBuilder : IMetaBuilder + { + private Dictionary _meta = new Dictionary(); + + public void Add(string key, object value) + { + _meta[key] = value; + } + + /// + /// Joins the new dictionary with the current one. In the event of a key collision, + /// the new value will override the old. + /// + public void Add(Dictionary values) + { + _meta = values.Keys.Union(_meta.Keys) + .ToDictionary(key => key, + key => values.ContainsKey(key) ? values[key] : _meta[key]); + } + + public Dictionary Build() + { + return _meta; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs b/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs new file mode 100644 index 0000000000..5cd0b765de --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs @@ -0,0 +1,73 @@ +using Xunit; +using JsonApiDotNetCore.Builders; +using System.Collections.Generic; + +namespace JsonApiDotNetCoreExampleTests.Unit.Builders +{ + public class MetaBuilderTests + { + [Fact] + public void Can_Add_Key_Value() + { + // arrange + var builder = new MetaBuilder(); + var key = "test"; + var value = "testValue"; + + // act + builder.Add(key, value); + var result = builder.Build(); + + // assert + Assert.NotEmpty(result); + Assert.Equal(value, result[key]); + } + + [Fact] + public void Can_Add_Multiple_Values() + { + // arrange + var builder = new MetaBuilder(); + var input = new Dictionary { + { "key1", "value1" }, + { "key2", "value2" } + }; + + // act + builder.Add(input); + var result = builder.Build(); + + // assert + Assert.NotEmpty(result); + foreach (var entry in input) + Assert.Equal(input[entry.Key], result[entry.Key]); + } + + [Fact] + public void When_Adding_Duplicate_Values_Keep_Newest() + { + // arrange + var builder = new MetaBuilder(); + + var key = "key"; + var oldValue = "oldValue"; + var newValue = "newValue"; + + builder.Add(key, oldValue); + + var input = new Dictionary { + { key, newValue }, + { "key2", "value2" } + }; + + // act + builder.Add(input); + var result = builder.Build(); + + // assert + Assert.NotEmpty(result); + Assert.Equal(input.Count, result.Count); + Assert.Equal(input[key], result[key]); + } + } +} From 19a38226e8de4f01f0ce23e0d1113483941b3dea Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:12:11 -0500 Subject: [PATCH 02/43] feat(service-provider-ext): inject IMetaBuilder --- src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs index 38face7307..a4c741d1ab 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs @@ -1,4 +1,5 @@ using System; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Formatters; @@ -54,6 +55,8 @@ public static void AddJsonApiInternals(this IServiceCollection service services.AddSingleton(); services.AddScoped(); + + services.AddScoped(); } public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) From 5545a91e24692175536bb1ebb09d74166504de0e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:32:49 -0500 Subject: [PATCH 03/43] test(service-collection-extension): test service injection --- ...ons.cs => IServiceCollectionExtensions.cs} | 2 +- .../IServiceCollectionExtensionsTests.cs | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) rename src/JsonApiDotNetCore/Extensions/{ServiceProviderExtensions.cs => IServiceCollectionExtensions.cs} (98%) create mode 100644 test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs diff --git a/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs similarity index 98% rename from src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs rename to src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index a4c741d1ab..7154e43df3 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -12,7 +12,7 @@ namespace JsonApiDotNetCore.Extensions { - public static class ServiceProviderExtensions + public static class IServiceCollectionExtensions { public static void AddJsonApi(this IServiceCollection services) where TContext : DbContext diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..0eb0f18636 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,45 @@ +using Xunit; +using JsonApiDotNetCore.Builders; +using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Http; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Data; +using Microsoft.Extensions.Caching.Memory; +using JsonApiDotNetCoreExample.Models; + +namespace JsonApiDotNetCoreExampleTests.Unit.Extensions +{ + public class IServiceCollectionExtensionsTests + { + [Fact] + public void AddJsonApiInternals_Adds_All_Required_Services() + { + // arrange + var services = new ServiceCollection(); + var jsonApiOptions = new JsonApiOptions(); + + services.AddDbContext(options => + { + options.UseMemoryCache(new MemoryCache(new MemoryCacheOptions())); + }, ServiceLifetime.Transient); + + // act + services.AddJsonApiInternals(jsonApiOptions); + var provider = services.BuildServiceProvider(); + + // assert + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService(typeof(IEntityRepository))); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + } + } +} From a82aade07903a8bd691f17c6ded49a3729cf5e3c Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:33:35 -0500 Subject: [PATCH 04/43] feat(json-api-context): expose meta builder as a property of the context --- src/JsonApiDotNetCore/Services/IJsonApiContext.cs | 3 ++- src/JsonApiDotNetCore/Services/JsonApiContext.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 1757109b71..34b6693601 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; @@ -17,6 +18,6 @@ public interface IJsonApiContext List IncludedRelationships { get; set; } bool IsRelationshipPath { get; } PageManager PageManager { get; set; } - + IMetaBuilder MetaBuilder { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 5ecd72872d..ee6560412a 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -14,11 +14,13 @@ public class JsonApiContext : IJsonApiContext public JsonApiContext( IContextGraph contextGraph, IHttpContextAccessor httpContextAccessor, - JsonApiOptions options) + JsonApiOptions options, + IMetaBuilder metaBuilder) { ContextGraph = contextGraph; _httpContextAccessor = httpContextAccessor; Options = options; + MetaBuilder = metaBuilder; } public JsonApiOptions Options { get; set; } @@ -30,6 +32,7 @@ public JsonApiContext( public bool IsRelationshipPath { get; private set; } public List IncludedRelationships { get; set; } public PageManager PageManager { get; set; } + public IMetaBuilder MetaBuilder { get; set; } public IJsonApiContext ApplyContext() { From c3b6af5d2d0a6421d42b58ef736280569fdc14fe Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:33:57 -0500 Subject: [PATCH 05/43] feat(document-builder): use the meta builder --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 2df9eaca4f..fdd8667ff2 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -62,16 +62,16 @@ public Documents Build(IEnumerable entities) private Dictionary _getMeta(IIdentifiable entity) { if (entity == null) return null; - - var meta = new Dictionary(); - var metaEntity = entity as IHasMeta; - if(metaEntity != null) - meta = metaEntity.GetMeta(_jsonApiContext); + var builder = _jsonApiContext.MetaBuilder; + + if(entity is IHasMeta metaEntity) + builder.Add(metaEntity.GetMeta(_jsonApiContext)); if(_jsonApiContext.Options.IncludeTotalRecordCount) - meta["total-records"] = _jsonApiContext.PageManager.TotalRecords; + builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); + var meta = builder.Build(); if(meta.Count > 0) return meta; return null; } From 34d56ef604e787d72c5c533591305a151ee79103 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 15:50:05 -0500 Subject: [PATCH 06/43] feat(services): add IRequestMeta for extensibility --- src/JsonApiDotNetCore/Services/IRequestMeta.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/JsonApiDotNetCore/Services/IRequestMeta.cs diff --git a/src/JsonApiDotNetCore/Services/IRequestMeta.cs b/src/JsonApiDotNetCore/Services/IRequestMeta.cs new file mode 100644 index 0000000000..7dd5fdcada --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IRequestMeta.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Services +{ + public interface IRequestMeta + { + Dictionary GetMeta(); + } +} \ No newline at end of file From aa0ddfc4ae3451e4c1af88a6813bc0201f12b785 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 20:52:58 -0500 Subject: [PATCH 07/43] test(extensibility): failing test that users can define meta via services --- .../Extensibility/RequestMetaTests.cs | 72 +++++++++++++++++++ .../Acceptance/Spec/DocumentTests/Meta.cs | 2 - .../Services/MetaService.cs | 15 ++++ .../Startups/MetaStartup.cs | 3 + 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs new file mode 100644 index 0000000000..a1b597f7d5 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; +using JsonApiDotNetCoreExample.Models; +using DotNetCoreDocs; +using JsonApiDotNetCoreExample; +using DotNetCoreDocs.Writers; +using Newtonsoft.Json; +using JsonApiDotNetCore.Models; +using System.Collections; +using System.Diagnostics; +using System.Threading; +using JsonApiDotNetCoreExampleTests.Startups; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + [Collection("WebHostCollection")] + public class RequestMetaTests + { + private DocsFixture _fixture; + + public RequestMetaTests(DocsFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Injecting_IRequestMeta_Adds_Meta_Data() + { + // arrange + var person = new Person(); + var expectedMeta = person.GetMeta(null); + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/people"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(documents.Meta); + Assert.NotNull(expectedMeta); + Assert.NotEmpty(expectedMeta); + + foreach(var hash in expectedMeta) + { + if(hash.Value is IList) + { + var listValue = (IList)hash.Value; + for(var i=0; i < listValue.Count; i++) + Assert.Equal(listValue[i].ToString(), ((IList)documents.Meta[hash.Key])[i].ToString()); + } + else + { + Assert.Equal(hash.Value, documents.Meta[hash.Key]); + } + } + Assert.Equal("request-meta-value", documents.Meta["request-meta"]); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index e024786252..4fb7693d4c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -10,11 +10,9 @@ using Xunit; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Data; -using System.Linq; using JsonApiDotNetCoreExampleTests.Startups; using JsonApiDotNetCoreExample.Models; using System.Collections; -using System; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs b/test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs new file mode 100644 index 0000000000..91de8fda5e --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCoreExampleTests.Services +{ + public class MetaService : IRequestMeta + { + public Dictionary GetMeta() + { + return new Dictionary { + { "request-meta", "request-meta-value" } + }; + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs index 0c84733632..b23fded9ba 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs @@ -7,6 +7,8 @@ using DotNetCoreDocs.Configuration; using System; using JsonApiDotNetCoreExample; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExampleTests.Services; namespace JsonApiDotNetCoreExampleTests.Startups { @@ -38,6 +40,7 @@ public override IServiceProvider ConfigureServices(IServiceCollection services) }); services.AddDocumentationConfiguration(Config); + services.AddScoped(); return services.BuildServiceProvider(); } From 340ee446a8d32ff183ceef1f71742b1c6548608f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 20:54:15 -0500 Subject: [PATCH 08/43] feat(document-builder): apply request meta --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 8 +++++++- .../Formatters/JsonApiOutputFormatter.cs | 11 ++++++++--- .../Serialization/JsonApiSerializer.cs | 15 ++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index fdd8667ff2..72ee23b245 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -12,11 +12,13 @@ public class DocumentBuilder { private IJsonApiContext _jsonApiContext; private IContextGraph _contextGraph; + private readonly IRequestMeta _requestMeta; - public DocumentBuilder(IJsonApiContext jsonApiContext) + public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta) { _jsonApiContext = jsonApiContext; _contextGraph = jsonApiContext.ContextGraph; + _requestMeta = requestMeta; } public Document Build(IIdentifiable entity) @@ -71,6 +73,10 @@ private Dictionary _getMeta(IIdentifiable entity) if(_jsonApiContext.Options.IncludeTotalRecordCount) builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); + var requestMeta = _requestMeta?.GetMeta(); + if(requestMeta != null) + builder.Add(requestMeta); + var meta = builder.Build(); if(meta.Count > 0) return meta; return null; diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index 95e607fca5..0c86571003 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -32,6 +32,8 @@ public async Task WriteAsync(OutputFormatterWriteContext context) var logger = GetService(context)? .CreateLogger(); + var requestMeta = GetService(context); + logger?.LogInformation("Formatting response as JSONAPI"); var response = context.HttpContext.Response; @@ -43,7 +45,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) string responseContent; try { - responseContent = GetResponseBody(context.Object, jsonApiContext, logger); + responseContent = GetResponseBody(context.Object, jsonApiContext, requestMeta, logger); } catch (Exception e) { @@ -64,7 +66,10 @@ private T GetService(OutputFormatterWriteContext context) return context.HttpContext.RequestServices.GetService(); } - private string GetResponseBody(object responseObject, IJsonApiContext jsonApiContext, ILogger logger) + private string GetResponseBody(object responseObject, + IJsonApiContext jsonApiContext, + IRequestMeta requestMeta, + ILogger logger) { if (responseObject == null) return GetNullDataResponse(); @@ -72,7 +77,7 @@ private string GetResponseBody(object responseObject, IJsonApiContext jsonApiCon if (responseObject.GetType() == typeof(Error) || jsonApiContext.RequestEntity == null) return GetErrorJson(responseObject, logger); - return JsonApiSerializer.Serialize(responseObject, jsonApiContext); + return JsonApiSerializer.Serialize(responseObject, jsonApiContext, requestMeta); } private string GetNullDataResponse() diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index 1fedd019a1..c5a4b83ddd 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -8,24 +8,25 @@ namespace JsonApiDotNetCore.Serialization { public static class JsonApiSerializer { - public static string Serialize(object entity, IJsonApiContext jsonApiContext) + public static string Serialize(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) { if (entity is IEnumerable) - return _serializeDocuments(entity, jsonApiContext); - return _serializeDocument(entity, jsonApiContext); + return _serializeDocuments(entity, jsonApiContext, requestMeta); + + return _serializeDocument(entity, jsonApiContext, requestMeta); } - private static string _serializeDocuments(object entity, IJsonApiContext jsonApiContext) + private static string _serializeDocuments(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) { - var documentBuilder = new DocumentBuilder(jsonApiContext); + var documentBuilder = new DocumentBuilder(jsonApiContext, requestMeta); var entities = entity as IEnumerable; var documents = documentBuilder.Build(entities); return _serialize(documents); } - private static string _serializeDocument(object entity, IJsonApiContext jsonApiContext) + private static string _serializeDocument(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) { - var documentBuilder = new DocumentBuilder(jsonApiContext); + var documentBuilder = new DocumentBuilder(jsonApiContext, requestMeta); var identifiableEntity = entity as IIdentifiable; var document = documentBuilder.Build(identifiableEntity); return _serialize(document); From 8462e75ae2906304a770e657548339f315bf7fd0 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Thu, 16 Mar 2017 21:00:17 -0500 Subject: [PATCH 09/43] docs(readme): document Request Meta --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index f7b068f988..b9fbb9faf5 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,10 @@ Resources can be sorted by an attribute: ### Meta +Meta objects can be assigned in two ways: + - Resource meta + - Request Meta + Resource meta can be defined by implementing `IHasMeta` on the model class: ```csharp @@ -343,6 +347,9 @@ public class Person : Identifiable, IHasMeta } ``` +Request Meta can be added by injecting a service that implements `IRequestMeta`. +In the event of a key collision, the Request Meta will take precendence. + ### Client Generated Ids By default, the server will respond with a `403 Forbidden` HTTP Status Code if a `POST` request is From 34431fa4f54557247cce51e5d337c4b3206f6213 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Fri, 17 Mar 2017 07:41:23 -0500 Subject: [PATCH 10/43] refactor(serialization): introduce new interfaces and composition root at JsonApiOutputFormatter removes logic from JsonApiOutputFormatter and into JsonApiWriter. this also reduces the large method parameter requirements throughout the affected classes --- .vscode/launch.json | 32 +------ .../Builders/DocumentBuilder.cs | 13 ++- .../Builders/IDocumentBuilder.cs | 11 +++ .../IServiceCollectionExtensions.cs | 4 + .../Formatters/IJsonApiWriter.cs | 10 ++ .../Formatters/JsonApiOutputFormatter.cs | 84 +---------------- .../Formatters/JsonApiWriter.cs | 93 +++++++++++++++++++ .../Serialization/IJsonApiSerializer.cs | 7 ++ .../Serialization/JsonApiSerializer.cs | 28 +++--- .../IServiceCollectionExtensionsTests.cs | 5 + 10 files changed, 158 insertions(+), 129 deletions(-) create mode 100644 src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs create mode 100644 src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs create mode 100644 src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs create mode 100644 src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 4dfa71e085..b26b008078 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,41 +1,11 @@ { "version": "0.2.0", "configurations": [ - { - "name": ".NET Core Launch (web)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/src/JsonApiDotNetCoreExample/bin/Debug/netcoreapp1.0/JsonApiDotNetCoreExample.dll", - "args": [], - "cwd": "${workspaceRoot}/src/JsonApiDotNetCoreExample", - "stopAtEntry": false, - "launchBrowser": { - "enabled": false, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/Views" - } - }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", - "processId": "${command.pickProcess}" + "processId": "${command:pickProcess}" } ] } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 72ee23b245..63829021d7 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -8,12 +8,18 @@ namespace JsonApiDotNetCore.Builders { - public class DocumentBuilder + public class DocumentBuilder : IDocumentBuilder { private IJsonApiContext _jsonApiContext; private IContextGraph _contextGraph; private readonly IRequestMeta _requestMeta; + public DocumentBuilder(IJsonApiContext jsonApiContext) + { + _jsonApiContext = jsonApiContext; + _contextGraph = jsonApiContext.ContextGraph; + } + public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta) { _jsonApiContext = jsonApiContext; @@ -73,9 +79,8 @@ private Dictionary _getMeta(IIdentifiable entity) if(_jsonApiContext.Options.IncludeTotalRecordCount) builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); - var requestMeta = _requestMeta?.GetMeta(); - if(requestMeta != null) - builder.Add(requestMeta); + if(_requestMeta != null) + builder.Add(_requestMeta.GetMeta()); var meta = builder.Build(); if(meta.Count > 0) return meta; diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs new file mode 100644 index 0000000000..8fe5c65ae9 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + public interface IDocumentBuilder + { + Document Build(IIdentifiable entity); + Documents Build(IEnumerable entities); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 7154e43df3..9cbf432081 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -57,6 +58,9 @@ public static void AddJsonApiInternals(this IServiceCollection service services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs new file mode 100644 index 0000000000..ce8b7da6a4 --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace JsonApiDotNetCore.Formatters +{ + public interface IJsonApiWriter + { + Task WriteAsync(OutputFormatterWriteContext context); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index 0c86571003..2431055d1d 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -1,14 +1,7 @@ using System; -using System.Text; using System.Threading.Tasks; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Formatters { @@ -26,81 +19,8 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context) public async Task WriteAsync(OutputFormatterWriteContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - var logger = GetService(context)? - .CreateLogger(); - - var requestMeta = GetService(context); - - logger?.LogInformation("Formatting response as JSONAPI"); - - var response = context.HttpContext.Response; - using (var writer = context.WriterFactory(response.Body, Encoding.UTF8)) - { - var jsonApiContext = GetService(context); - - response.ContentType = "application/vnd.api+json"; - string responseContent; - try - { - responseContent = GetResponseBody(context.Object, jsonApiContext, requestMeta, logger); - } - catch (Exception e) - { - logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); - var errors = new ErrorCollection(); - errors.Add(new Error("400", e.Message)); - responseContent = errors.GetJson(); - response.StatusCode = 400; - } - - await writer.WriteAsync(responseContent); - await writer.FlushAsync(); - } - } - - private T GetService(OutputFormatterWriteContext context) - { - return context.HttpContext.RequestServices.GetService(); - } - - private string GetResponseBody(object responseObject, - IJsonApiContext jsonApiContext, - IRequestMeta requestMeta, - ILogger logger) - { - if (responseObject == null) - return GetNullDataResponse(); - - if (responseObject.GetType() == typeof(Error) || jsonApiContext.RequestEntity == null) - return GetErrorJson(responseObject, logger); - - return JsonApiSerializer.Serialize(responseObject, jsonApiContext, requestMeta); - } - - private string GetNullDataResponse() - { - return JsonConvert.SerializeObject(new Document - { - Data = null - }); - } - - private string GetErrorJson(object responseObject, ILogger logger) - { - if (responseObject.GetType() == typeof(Error)) - { - var errors = new ErrorCollection(); - errors.Add((Error)responseObject); - return errors.GetJson(); - } - else - { - logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); - return JsonConvert.SerializeObject(responseObject); - } + var writer = context.HttpContext.RequestServices.GetService(); + await writer.WriteAsync(context); } } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs new file mode 100644 index 0000000000..8607026387 --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -0,0 +1,93 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Formatters +{ + public class JsonApiWriter : IJsonApiWriter + { + private readonly ILogger _logger; + private readonly IJsonApiContext _jsonApiContext; + private readonly IJsonApiSerializer _serializer; + + public JsonApiWriter(IJsonApiContext jsonApiContext, + IJsonApiSerializer serializer, + ILoggerFactory loggerFactory) + { + _jsonApiContext = jsonApiContext; + _serializer = serializer; + _logger = loggerFactory.CreateLogger(); + } + + public async Task WriteAsync(OutputFormatterWriteContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + _logger?.LogInformation("Formatting response as JSONAPI"); + + var response = context.HttpContext.Response; + using (var writer = context.WriterFactory(response.Body, Encoding.UTF8)) + { + response.ContentType = "application/vnd.api+json"; + string responseContent; + try + { + responseContent = GetResponseBody(context.Object); + } + catch (Exception e) + { + _logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); + var errors = new ErrorCollection(); + errors.Add(new Error("400", e.Message)); + responseContent = errors.GetJson(); + response.StatusCode = 400; + } + + await writer.WriteAsync(responseContent); + await writer.FlushAsync(); + } + } + + private string GetResponseBody(object responseObject) + { + if (responseObject == null) + return GetNullDataResponse(); + + if (responseObject.GetType() == typeof(Error) || _jsonApiContext.RequestEntity == null) + return GetErrorJson(responseObject, _logger); + + return _serializer.Serialize(responseObject); + } + + private string GetNullDataResponse() + { + return JsonConvert.SerializeObject(new Document + { + Data = null + }); + } + + private string GetErrorJson(object responseObject, ILogger logger) + { + if (responseObject.GetType() == typeof(Error)) + { + var errors = new ErrorCollection(); + errors.Add((Error)responseObject); + return errors.GetJson(); + } + else + { + logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); + return JsonConvert.SerializeObject(responseObject); + } + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs new file mode 100644 index 0000000000..21eae09980 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.Serialization +{ + public interface IJsonApiSerializer + { + string Serialize(object entity); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index c5a4b83ddd..58bc9b19d0 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -1,38 +1,42 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { - public static class JsonApiSerializer + public class JsonApiSerializer : IJsonApiSerializer { - public static string Serialize(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) + private readonly IDocumentBuilder _documentBuilder; + + public JsonApiSerializer(IDocumentBuilder documentBuilder) + { + _documentBuilder = documentBuilder; + } + + public string Serialize(object entity) { if (entity is IEnumerable) - return _serializeDocuments(entity, jsonApiContext, requestMeta); + return _serializeDocuments(entity); - return _serializeDocument(entity, jsonApiContext, requestMeta); + return _serializeDocument(entity); } - private static string _serializeDocuments(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) + private string _serializeDocuments(object entity) { - var documentBuilder = new DocumentBuilder(jsonApiContext, requestMeta); var entities = entity as IEnumerable; - var documents = documentBuilder.Build(entities); + var documents = _documentBuilder.Build(entities); return _serialize(documents); } - private static string _serializeDocument(object entity, IJsonApiContext jsonApiContext, IRequestMeta requestMeta) + private string _serializeDocument(object entity) { - var documentBuilder = new DocumentBuilder(jsonApiContext, requestMeta); var identifiableEntity = entity as IIdentifiable; - var document = documentBuilder.Build(identifiableEntity); + var document = _documentBuilder.Build(identifiableEntity); return _serialize(document); } - private static string _serialize(object obj) + private string _serialize(object obj) { return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs index 0eb0f18636..35761adf09 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs @@ -11,6 +11,8 @@ using JsonApiDotNetCoreExample.Data; using Microsoft.Extensions.Caching.Memory; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Formatters; namespace JsonApiDotNetCoreExampleTests.Unit.Extensions { @@ -40,6 +42,9 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); } } } From 77225005bda82a9754034154ba777734f461c6a0 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Fri, 17 Mar 2017 08:23:59 -0500 Subject: [PATCH 11/43] refactor(de-serialization): introduce new interfaces and composition root at JsonApiInputFormatter removes logic from the input formatter and into JsonApiReader. Significantly reduces coupling of concrete types. --- .../Data/DefaultEntityRepository.cs | 4 +- .../IServiceCollectionExtensions.cs | 4 ++ .../Formatters/IJsonApiReader.cs | 10 +++ .../Formatters/JsonApiInputFormatter.cs | 57 +---------------- .../Formatters/JsonApiReader.cs | 64 +++++++++++++++++++ .../Generics/GenericProcessorFactory.cs | 23 ++++--- .../Generics/IGenericProcessorFactory.cs | 14 ++++ .../Serialization/IJsonApiDeSerializer.cs | 11 ++++ .../Serialization/JsonApiDeSerializer.cs | 57 +++++++++-------- .../Services/IJsonApiContext.cs | 1 + .../Services/JsonApiContext.cs | 5 +- .../Extensibility/RepositoryOverrideTests.cs | 2 +- .../Acceptance/Spec/CreatingDataTests.cs | 8 +-- .../Acceptance/Spec/FetchingDataTests.cs | 2 +- .../Spec/UpdatingRelationshipsTests.cs | 2 + .../Acceptance/TodoItemsControllerTests.cs | 20 +++--- .../IServiceCollectionExtensionsTests.cs | 4 ++ 17 files changed, 181 insertions(+), 107 deletions(-) create mode 100644 src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs create mode 100644 src/JsonApiDotNetCore/Formatters/JsonApiReader.cs create mode 100644 src/JsonApiDotNetCore/Internal/Generics/IGenericProcessorFactory.cs create mode 100644 src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 5a44bb076c..4c98f8cec8 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -32,6 +32,7 @@ public class DefaultEntityRepository private readonly DbSet _dbSet; private readonly ILogger _logger; private readonly IJsonApiContext _jsonApiContext; + private readonly IGenericProcessorFactory _genericProcessorFactory; public DefaultEntityRepository( DbContext context, @@ -42,6 +43,7 @@ public DefaultEntityRepository( _dbSet = context.GetDbSet(); _jsonApiContext = jsonApiContext; _logger = loggerFactory.CreateLogger>(); + _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; } public virtual IQueryable Get() @@ -110,7 +112,7 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) { - var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context); + var genericProcessor = _genericProcessorFactory.GetProcessor(relationship.Type); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); } diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 9cbf432081..1e646c085d 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -61,6 +61,10 @@ public static void AddJsonApiInternals(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(typeof(GenericProcessor<>)); } public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs new file mode 100644 index 0000000000..5b64cc42bf --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace JsonApiDotNetCore.Formatters +{ + public interface IJsonApiReader + { + Task ReadAsync(InputFormatterContext context); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs index c4f1692eaa..12e57deadf 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs @@ -1,13 +1,7 @@ using System; -using System.IO; using System.Threading.Tasks; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Formatters { @@ -23,55 +17,10 @@ public bool CanRead(InputFormatterContext context) return contentTypeString == "application/vnd.api+json"; } - public Task ReadAsync(InputFormatterContext context) + public async Task ReadAsync(InputFormatterContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - var request = context.HttpContext.Request; - - if (request.ContentLength == 0) - { - return InputFormatterResult.SuccessAsync(null); - } - - var loggerFactory = GetService(context); - var logger = loggerFactory?.CreateLogger(); - - var dbContext = GetService(context); - - try - { - var body = GetRequestBody(context.HttpContext.Request.Body); - var jsonApiContext = GetService(context); - var model = jsonApiContext.IsRelationshipPath ? - JsonApiDeSerializer.DeserializeRelationship(body, jsonApiContext) : - JsonApiDeSerializer.Deserialize(body, jsonApiContext, dbContext); - - if(model == null) - logger?.LogError("An error occurred while de-serializing the payload"); - - return InputFormatterResult.SuccessAsync(model); - } - catch (JsonSerializationException ex) - { - logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload"); - context.HttpContext.Response.StatusCode = 422; - return InputFormatterResult.FailureAsync(); - } - } - - private string GetRequestBody(Stream body) - { - using (var reader = new StreamReader(body)) - { - return reader.ReadToEnd(); - } - } - - private TService GetService(InputFormatterContext context) - { - return context.HttpContext.RequestServices.GetService(); + var reader = context.HttpContext.RequestServices.GetService(); + return await reader.ReadAsync(context); } } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs new file mode 100644 index 0000000000..65a2382a73 --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Formatters +{ + public class JsonApiReader : IJsonApiReader + { + private readonly IJsonApiDeSerializer _deSerializer; + private readonly IJsonApiContext _jsonApiContext; + private readonly ILogger _logger; + + + public JsonApiReader(IJsonApiDeSerializer deSerializer, IJsonApiContext jsonApiContext, ILoggerFactory loggerFactory) + { + _deSerializer = deSerializer; + _jsonApiContext = jsonApiContext; + _logger = loggerFactory.CreateLogger(); + } + + public Task ReadAsync(InputFormatterContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var request = context.HttpContext.Request; + if (request.ContentLength == 0) + return InputFormatterResult.SuccessAsync(null); + + try + { + var body = GetRequestBody(context.HttpContext.Request.Body); + var model = _jsonApiContext.IsRelationshipPath ? + _deSerializer.DeserializeRelationship(body) : + _deSerializer.Deserialize(body); + + if(model == null) + _logger?.LogError("An error occurred while de-serializing the payload"); + + return InputFormatterResult.SuccessAsync(model); + } + catch (JsonSerializationException ex) + { + _logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload"); + context.HttpContext.Response.StatusCode = 422; + return InputFormatterResult.FailureAsync(); + } + } + + private string GetRequestBody(Stream body) + { + using (var reader = new StreamReader(body)) + { + return reader.ReadToEnd(); + } + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs index 24a963599a..7a647bf60f 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs @@ -3,17 +3,22 @@ namespace JsonApiDotNetCore.Internal { - /// - /// Used to generate a generic operations processor when the types - /// are not know until runtime. The typical use case would be for - /// accessing relationship data. - /// - public static class GenericProcessorFactory + public class GenericProcessorFactory : IGenericProcessorFactory { - public static IGenericProcessor GetProcessor(Type type, DbContext dbContext) + private readonly DbContext _dbContext; + private readonly IServiceProvider _serviceProvider; + + public GenericProcessorFactory(DbContext dbContext, + IServiceProvider serviceProvider) + { + _dbContext = dbContext; + _serviceProvider = serviceProvider; + } + + public IGenericProcessor GetProcessor(Type type) { - var repositoryType = typeof(GenericProcessor<>).MakeGenericType(type); - return (IGenericProcessor)Activator.CreateInstance(repositoryType, dbContext); + var processorType = typeof(GenericProcessor<>).MakeGenericType(type); + return (IGenericProcessor)_serviceProvider.GetService(processorType); } } } diff --git a/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessorFactory.cs b/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessorFactory.cs new file mode 100644 index 0000000000..ce959658c4 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Generics/IGenericProcessorFactory.cs @@ -0,0 +1,14 @@ +using System; + +namespace JsonApiDotNetCore.Internal +{ + /// + /// Used to generate a generic operations processor when the types + /// are not know until runtime. The typical use case would be for + /// accessing relationship data. + /// + public interface IGenericProcessorFactory + { + IGenericProcessor GetProcessor(Type type); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs new file mode 100644 index 0000000000..02f84a747a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Serialization +{ + public interface IJsonApiDeSerializer + { + object Deserialize(string requestBody); + object DeserializeRelationship(string requestBody); + List DeserializeList(string requestBody); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 59772d9a76..63187099ea 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -8,23 +8,33 @@ using JsonApiDotNetCore.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System.Collections; -using JsonApiDotNetCore.Data; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Serialization { - public static class JsonApiDeSerializer + public class JsonApiDeSerializer : IJsonApiDeSerializer { - public static object Deserialize(string requestBody, IJsonApiContext context, - DbContext dbContext) + private readonly DbContext _dbContext; + private readonly IJsonApiContext _jsonApiContext; + private readonly IGenericProcessorFactory _genericProcessorFactor; + + public JsonApiDeSerializer(DbContext dbContext, + IJsonApiContext jsonApiContext, + IGenericProcessorFactory genericProcessorFactory) + { + _dbContext = dbContext; + _jsonApiContext = jsonApiContext; + _genericProcessorFactor = genericProcessorFactory; + } + + public object Deserialize(string requestBody) { var document = JsonConvert.DeserializeObject(requestBody); - var entity = DataToObject(document.Data, context, dbContext); + var entity = DataToObject(document.Data); return entity; } - public static object DeserializeRelationship(string requestBody, IJsonApiContext context) + public object DeserializeRelationship(string requestBody) { var data = JToken.Parse(requestBody)["data"]; @@ -35,34 +45,31 @@ public static object DeserializeRelationship(string requestBody, IJsonApiContext } - public static List DeserializeList(string requestBody, IJsonApiContext context, - DbContext dbContext) + public List DeserializeList(string requestBody) { var documents = JsonConvert.DeserializeObject(requestBody); var deserializedList = new List(); foreach (var data in documents.Data) { - var entity = DataToObject(data, context, dbContext); + var entity = DataToObject(data); deserializedList.Add((TEntity)entity); } return deserializedList; } - private static object DataToObject(DocumentData data, - IJsonApiContext context, - DbContext dbContext) + private object DataToObject(DocumentData data) { var entityTypeName = data.Type.ToProperCase(); - var contextEntity = context.ContextGraph.GetContextEntity(entityTypeName); - context.RequestEntity = contextEntity; + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entityTypeName); + _jsonApiContext.RequestEntity = contextEntity; var entity = Activator.CreateInstance(contextEntity.EntityType); entity = _setEntityAttributes(entity, contextEntity, data.Attributes); - entity = _setRelationships(entity, contextEntity, data.Relationships, dbContext); + entity = _setRelationships(entity, contextEntity, data.Relationships); var identifiableEntity = (IIdentifiable)entity; @@ -72,7 +79,7 @@ private static object DataToObject(DocumentData data, return identifiableEntity; } - private static object _setEntityAttributes( + private object _setEntityAttributes( object entity, ContextEntity contextEntity, Dictionary attributeValues) { if (attributeValues == null || attributeValues.Count == 0) @@ -98,11 +105,10 @@ private static object _setEntityAttributes( return entity; } - private static object _setRelationships( + private object _setRelationships( object entity, ContextEntity contextEntity, - Dictionary relationships, - DbContext context) + Dictionary relationships) { if (relationships == null || relationships.Count == 0) return entity; @@ -114,13 +120,13 @@ private static object _setRelationships( if (attr.IsHasOne) entity = _setHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships); else - entity = _setHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships, context); + entity = _setHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); } return entity; } - private static object _setHasOneRelationship(object entity, + private object _setHasOneRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, @@ -147,12 +153,11 @@ private static object _setHasOneRelationship(object entity, return entity; } - private static object _setHasManyRelationship(object entity, + private object _setHasManyRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, - Dictionary relationships, - DbContext context) + Dictionary relationships) { var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); @@ -167,7 +172,7 @@ private static object _setHasManyRelationship(object entity, if (data == null) return entity; - var genericProcessor = GenericProcessorFactory.GetProcessor(attr.Type, context); + var genericProcessor = _genericProcessorFactor.GetProcessor(attr.Type); var ids = relationshipData.ManyData.Select(r => r["id"]); genericProcessor.SetRelationships(entity, attr, ids); } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 34b6693601..f7d05dc4b3 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -19,5 +19,6 @@ public interface IJsonApiContext bool IsRelationshipPath { get; } PageManager PageManager { get; set; } IMetaBuilder MetaBuilder { get; set; } + IGenericProcessorFactory GenericProcessorFactory { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index ee6560412a..f9bd3f0b0f 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -15,12 +15,14 @@ public JsonApiContext( IContextGraph contextGraph, IHttpContextAccessor httpContextAccessor, JsonApiOptions options, - IMetaBuilder metaBuilder) + IMetaBuilder metaBuilder, + IGenericProcessorFactory genericProcessorFactory) { ContextGraph = contextGraph; _httpContextAccessor = httpContextAccessor; Options = options; MetaBuilder = metaBuilder; + GenericProcessorFactory = genericProcessorFactory; } public JsonApiOptions Options { get; set; } @@ -33,6 +35,7 @@ public JsonApiContext( public List IncludedRelationships { get; set; } public PageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } + public IGenericProcessorFactory GenericProcessorFactory { get; set; } public IJsonApiContext ApplyContext() { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs index 7c7b145830..847d6eefff 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs @@ -57,7 +57,7 @@ public async Task Total_Record_Count_Included() // act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(responseBody, jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(responseBody); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 25e2e3d6fe..52b399191f 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -156,7 +156,7 @@ public async Task Can_Create_Entity_With_Client_Defined_Id_If_Configured() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -208,7 +208,7 @@ public async Task Can_Create_Guid_Identifiable_Entity_With_Client_Defined_Id_If_ // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context); + var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -269,7 +269,7 @@ public async Task Can_Create_And_Set_HasMany_Relationships() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context); + var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); var newId = deserializedBody.Id; var contextCollection = context.TodoItemCollections .Include(c => c.Owner) @@ -312,7 +312,7 @@ public async Task ShouldReceiveLocationHeader_InResponse() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 7c1339f86c..075fc77129 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -50,7 +50,7 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index dfbd862232..ffb336e2c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Bogus; using DotNetCoreDocs; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 0e9449ad86..84e398d4b9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -56,7 +56,7 @@ public async Task Can_Get_TodoItems() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -85,7 +85,7 @@ public async Task Can_Paginate_TodoItems() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -114,7 +114,7 @@ public async Task Can_Filter_TodoItems() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -144,7 +144,7 @@ public async Task Can_Filter_TodoItems_Using_Like_Operator() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -182,7 +182,7 @@ public async Task Can_Sort_TodoItems_By_Ordinal_Ascending() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -224,7 +224,7 @@ public async Task Can_Sort_TodoItems_By_Ordinal_Descending() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -258,7 +258,7 @@ public async Task Can_Get_TodoItem_ById() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -288,7 +288,7 @@ public async Task Can_Get_TodoItem_WithOwner() // Act var response = await _fixture.MakeRequest(description, httpMethod, route); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -343,7 +343,7 @@ public async Task Can_Post_TodoItem() // Act var response = await _fixture.MakeRequest(description, request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -390,7 +390,7 @@ public async Task Can_Patch_TodoItem() // Act var response = await _fixture.MakeRequest(description, request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, _fixture.GetService()); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs index 35761adf09..71a952994a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs @@ -45,6 +45,10 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService(typeof(GenericProcessor))); } } } From 5c1a14808f08d358531dfb595a64617f5ac4cbfc Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Fri, 17 Mar 2017 08:25:18 -0500 Subject: [PATCH 12/43] chore(csproj): bump package version This is not a significant breaking change, but does affect the serialization APIs which may be used by tests --- README.md | 4 ++-- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b9fbb9faf5..29b8bff318 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,14 @@ Install-Package JsonApiDotnetCore - project.json ```json -"JsonApiDotNetCore": "1.1.0" +"JsonApiDotNetCore": "1.2.0" ``` - *.csproj ```xml - + ``` diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 9f0d438389..a222271081 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,7 +1,7 @@  - 1.1.1 + 1.2.0 netcoreapp1.0 JsonApiDotNetCore JsonApiDotNetCore From 588b98036c79d266f32f6dda0dc876123dbd7a4f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 10:50:02 -0500 Subject: [PATCH 13/43] test(patch): failing test for patching entity has one relationship --- .../Acceptance/Spec/UpdatingDataTests.cs | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index c87683fa2f..5bfed72684 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -7,8 +6,6 @@ using Bogus; using DotNetCoreDocs; using DotNetCoreDocs.Writers; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -16,6 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -25,14 +23,18 @@ public class UpdatingDataTests private DocsFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; + private Faker _personFaker; public UpdatingDataTests(DocsFixture fixture) { _fixture = fixture; _context = fixture.GetService(); - _todoItemFaker = new Faker() + _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()); + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); } [Fact] @@ -73,5 +75,61 @@ public async Task Respond_404_If_EntityDoesNotExist() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + [Fact] + public async Task Can_Patch_Entity_And_HasOne_Relationships() + { + // arrange + var todoItem = _todoItemFaker.Generate(); + var person = _personFaker.Generate(); + _context.TodoItems.Add(todoItem); + _context.People.Add(person); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = new + { + type = "todo-items", + attributes = new + { + description = todoItem.Description, + ordinal = todoItem.Ordinal + }, + relationships = new + { + owner = new + { + data = new + { + type = "people", + id = person.Id.ToString() + } + } + } + } + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + var updatedTodoItem = _context.TodoItems + .SingleOrDefault(t => t.Id == todoItem.Id); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(person.Id, updatedTodoItem.OwnerId); + } } } From 402d6ccbe5137338d63a7872e8265052dffe5931 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:36:10 -0500 Subject: [PATCH 14/43] feat(relationship-attr): make setValue abstract allows setting of has-one relationship by Id --- src/JsonApiDotNetCore/Models/HasManyAttribute.cs | 11 ++++++++++- src/JsonApiDotNetCore/Models/HasOneAttribute.cs | 15 +++++++++++++++ .../Models/RelationshipAttribute.cs | 12 ++---------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 13e4a9efad..445b82c22a 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System.Reflection; namespace JsonApiDotNetCore.Models { @@ -9,5 +9,14 @@ public HasManyAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyInfo = entity + .GetType() + .GetProperty(InternalRelationshipName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index e5670eae29..29661de485 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute @@ -7,5 +9,18 @@ public HasOneAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyName = (newValue.GetType() == Type) + ? InternalRelationshipName + : $"{InternalRelationshipName}Id"; + + var propertyInfo = entity + .GetType() + .GetProperty(propertyName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 45b3565592..5e02eaf1ef 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -1,9 +1,8 @@ using System; -using System.Reflection; namespace JsonApiDotNetCore.Models { - public class RelationshipAttribute : Attribute + public abstract class RelationshipAttribute : Attribute { protected RelationshipAttribute(string publicName) { @@ -16,13 +15,6 @@ protected RelationshipAttribute(string publicName) public bool IsHasMany { get { return this.GetType() == typeof(HasManyAttribute); } } public bool IsHasOne { get { return this.GetType() == typeof(HasOneAttribute); } } - public void SetValue(object entity, object newValue) - { - var propertyInfo = entity - .GetType() - .GetProperty(InternalRelationshipName); - - propertyInfo.SetValue(entity, newValue); - } + public abstract void SetValue(object entity, object newValue); } } From 0f2ba282edeb626fe345f18d25028ba675fbf141 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:37:21 -0500 Subject: [PATCH 15/43] feat(json-api-context): store a list of relationships to update when PATCHing entities, we need to know which relationships to also PATCH --- src/JsonApiDotNetCore/Services/IJsonApiContext.cs | 2 ++ src/JsonApiDotNetCore/Services/JsonApiContext.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index f7d05dc4b3..2860c3eb74 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Services { @@ -20,5 +21,6 @@ public interface IJsonApiContext PageManager PageManager { get; set; } IMetaBuilder MetaBuilder { get; set; } IGenericProcessorFactory GenericProcessorFactory { get; set; } + Dictionary RelationshipsToUpdate { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index f9bd3f0b0f..9cc244677d 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -23,6 +24,7 @@ public JsonApiContext( Options = options; MetaBuilder = metaBuilder; GenericProcessorFactory = genericProcessorFactory; + RelationshipsToUpdate = new Dictionary(); } public JsonApiOptions Options { get; set; } @@ -36,6 +38,7 @@ public JsonApiContext( public PageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } public IGenericProcessorFactory GenericProcessorFactory { get; set; } + public Dictionary RelationshipsToUpdate { get; set; } public IJsonApiContext ApplyContext() { From 7f49ef914722013662b9079c18599a5aa27d54ea Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:38:22 -0500 Subject: [PATCH 16/43] feat(de-serializer): store which relationships should be updated --- src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 63187099ea..367904f8dd 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -141,12 +141,18 @@ private object _setHasOneRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) { + var relationshipAttr = _jsonApiContext.RequestEntity.Relationships + .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + var data = (Dictionary)relationshipData.ExposedData; if (data == null) return entity; var newValue = data["id"]; var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + entityProperty.SetValue(entity, convertedValue); } From 3143e4ad61efa90e997ef1701518ddbdc0fb9089 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:38:54 -0500 Subject: [PATCH 17/43] feat(entity-repository): update relationship values --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 4c98f8cec8..6e18aebc96 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -105,9 +105,12 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) attr.SetValue(oldEntity, attr.GetValue(entity)); }); + foreach(var relationship in _jsonApiContext.RelationshipsToUpdate) + relationship.Key.SetValue(oldEntity, relationship.Value); + await _context.SaveChangesAsync(); - return oldEntity; + return oldEntity; } public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) From f6d5c34e649ffe7297d66cd6762af9b65d518586 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:39:58 -0500 Subject: [PATCH 18/43] test(patch): use non-cached context otherwise the todoItem.OwnerId will be null from the cached version --- .../Acceptance/Spec/UpdatingDataTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index 5bfed72684..a5ad8f6a7a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -11,6 +11,7 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -124,7 +125,8 @@ public async Task Can_Patch_Entity_And_HasOne_Relationships() // Act var response = await client.SendAsync(request); - var updatedTodoItem = _context.TodoItems + var updatedTodoItem = _context.TodoItems.AsNoTracking() + .Include(t => t.Owner) .SingleOrDefault(t => t.Id == todoItem.Id); // Assert From 072ced3662dba5f0cb61ac306a3a1b6e15ed68b9 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:02:45 -0500 Subject: [PATCH 19/43] test(fetching-data): add failing test checks whether or not included entities contain relationship data --- .../Acceptance/Spec/FetchingDataTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 075fc77129..7ff0b05e85 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -2,8 +2,10 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Bogus; using DotNetCoreDocs; using DotNetCoreDocs.Writers; +using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; @@ -13,6 +15,7 @@ using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -21,11 +24,19 @@ public class FetchingDataTests { private DocsFixture _fixture; private IJsonApiContext _jsonApiContext; + private Faker _todoItemFaker; + private Faker _personFaker; public FetchingDataTests(DocsFixture fixture) { _fixture = fixture; _jsonApiContext = fixture.GetService(); + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()); + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); } [Fact] @@ -60,5 +71,38 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() context.Dispose(); } + + [Fact] + public async Task Included_Records_Contain_Relationship_Links() + { + // arrange + var context = _fixture.GetService(); + var todoItem = _todoItemFaker.Generate(); + var person = _personFaker.Generate(); + todoItem.Owner = person; + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner"; + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = JsonConvert.DeserializeObject(body); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(person.StringId, deserializedBody.Included[0].Id); + Assert.NotNull(deserializedBody.Included[0].Relationships); + Assert.Equal($"http://localhost/api/v1/people/{person.Id}/todo-items", deserializedBody.Included[0].Relationships["todo-items"].Links.Related); + Assert.Equal($"http://localhost/api/v1/people/{person.Id}/relationships/todo-items", deserializedBody.Included[0].Relationships["todo-items"].Links.Self); + context.Dispose(); + } } } From ec256649f5fe8d5e45d827bf686b9e69f2e55755 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:03:50 -0500 Subject: [PATCH 20/43] feat(document-builder): include relationship objects in compound doc --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 63829021d7..7bc59a3b60 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -183,11 +183,7 @@ private DocumentData _getIncludedEntity(IIdentifiable entity) var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); - var data = new DocumentData - { - Type = contextEntity.EntityName, - Id = entity.StringId - }; + var data = _getData(contextEntity, entity); data.Attributes = new Dictionary(); From 635e20f1f796f51f3bf44f85cb3523412814c038 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:04:19 -0500 Subject: [PATCH 21/43] fix(document-builder): do not include null relationships in compound doc --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 7bc59a3b60..12710a4927 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -169,14 +169,21 @@ private List _getIncludedEntities(ContextEntity contextEntity, IId if (navigationEntity is IEnumerable) foreach (var includedEntity in (IEnumerable)navigationEntity) - included.Add(_getIncludedEntity((IIdentifiable)includedEntity)); + _addIncludedEntity(included, (IIdentifiable)includedEntity); else - included.Add(_getIncludedEntity((IIdentifiable)navigationEntity)); + _addIncludedEntity(included, (IIdentifiable)navigationEntity); }); return included; } + private void _addIncludedEntity(List entities, IIdentifiable entity) + { + var includedEntity = _getIncludedEntity(entity); + if(includedEntity != null) + entities.Add(includedEntity); + } + private DocumentData _getIncludedEntity(IIdentifiable entity) { if(entity == null) return null; From 10184acc2c1413999b5cdecb07bfef9c779f0f56 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:32:07 -0500 Subject: [PATCH 22/43] refactor(jsonapi-exception): treat all errors as error collection add overloads for throwing errors directly --- .../Internal/JsonApiException.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index e5ecb56fd1..30b35770fa 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -4,28 +4,34 @@ namespace JsonApiDotNetCore.Internal { public class JsonApiException : Exception { - private string _statusCode; - private string _detail; - private string _message; + private ErrorCollection _errors = new ErrorCollection(); + + public JsonApiException(ErrorCollection errorCollection) + { + _errors = errorCollection; + } + + public JsonApiException(Error error) + : base(error.Title) + { + _errors.Add(error); + } public JsonApiException(string statusCode, string message) : base(message) { - _statusCode = statusCode; - _message = message; + _errors.Add(new Error(statusCode, message, null)); } public JsonApiException(string statusCode, string message, string detail) : base(message) { - _statusCode = statusCode; - _message = message; - _detail = detail; + _errors.Add(new Error(statusCode, message, detail)); } - public Error GetError() + public ErrorCollection GetError() { - return new Error(_statusCode, _message, _detail); + return _errors; } } } From 2feec1d30a5fc940b473e8d6d76d3a0b476f0cbb Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:33:05 -0500 Subject: [PATCH 23/43] feat(jsonapi-exception): add logic for getting error collectoon status --- src/JsonApiDotNetCore/Internal/Error.cs | 2 ++ .../Internal/JsonApiException.cs | 15 +++++++++++++++ .../Middleware/JsonApiExceptionFilter.cs | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index 01c4a26de0..a7aca41962 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -28,5 +28,7 @@ public Error(string status, string title, string detail) [JsonProperty("status")] public string Status { get; set; } + + public int StatusCode { get { return int.Parse(Status); } } } } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index 30b35770fa..907b1db7fd 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace JsonApiDotNetCore.Internal { @@ -33,5 +34,19 @@ public ErrorCollection GetError() { return _errors; } + + public int GetStatusCode() + { + if(_errors.Errors.Count == 1) + return _errors.Errors[0].StatusCode; + + if(_errors.Errors.FirstOrDefault(e => e.StatusCode >= 500) != null) + return 500; + + if(_errors.Errors.FirstOrDefault(e => e.StatusCode >= 400) != null) + return 400; + + return 500; + } } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs index 479a947e5e..ee038e7902 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs @@ -23,7 +23,7 @@ public void OnException(ExceptionContext context) var error = jsonApiException.GetError(); var result = new ObjectResult(error); - result.StatusCode = Convert.ToInt16(error.Status); + result.StatusCode = jsonApiException.GetStatusCode(); context.Result = result; } } From c459700a92911c0d304756d18afe04425ca6d44e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:02:00 -0500 Subject: [PATCH 24/43] fix(error): ignore StatusCode property --- src/JsonApiDotNetCore/Internal/Error.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index a7aca41962..b9261324a5 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -29,6 +29,7 @@ public Error(string status, string title, string detail) [JsonProperty("status")] public string Status { get; set; } + [JsonIgnore] public int StatusCode { get { return int.Parse(Status); } } } } From 75e72b7824fc8e1f3d013c2e977cbf341f01cd22 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:03:47 -0500 Subject: [PATCH 25/43] refactor(writer): move logic into serializer --- .../Formatters/JsonApiWriter.cs | 39 +------------ .../Serialization/JsonApiSerializer.cs | 55 +++++++++++++++++-- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index 8607026387..730a88f13e 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -2,26 +2,20 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Formatters { public class JsonApiWriter : IJsonApiWriter { private readonly ILogger _logger; - private readonly IJsonApiContext _jsonApiContext; private readonly IJsonApiSerializer _serializer; - public JsonApiWriter(IJsonApiContext jsonApiContext, - IJsonApiSerializer serializer, + public JsonApiWriter(IJsonApiSerializer serializer, ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext; _serializer = serializer; _logger = loggerFactory.CreateLogger(); } @@ -58,36 +52,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) private string GetResponseBody(object responseObject) { - if (responseObject == null) - return GetNullDataResponse(); - - if (responseObject.GetType() == typeof(Error) || _jsonApiContext.RequestEntity == null) - return GetErrorJson(responseObject, _logger); - return _serializer.Serialize(responseObject); - } - - private string GetNullDataResponse() - { - return JsonConvert.SerializeObject(new Document - { - Data = null - }); - } - - private string GetErrorJson(object responseObject, ILogger logger) - { - if (responseObject.GetType() == typeof(Error)) - { - var errors = new ErrorCollection(); - errors.Add((Error)responseObject); - return errors.GetJson(); - } - else - { - logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); - return JsonConvert.SerializeObject(responseObject); - } - } + } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index 58bc9b19d0..e4fb140d5b 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization @@ -8,28 +11,70 @@ namespace JsonApiDotNetCore.Serialization public class JsonApiSerializer : IJsonApiSerializer { private readonly IDocumentBuilder _documentBuilder; + private readonly ILogger _logger; + private readonly IJsonApiContext _jsonApiContext; - public JsonApiSerializer(IDocumentBuilder documentBuilder) + public JsonApiSerializer( + IJsonApiContext jsonApiContext, + IDocumentBuilder documentBuilder) { + _jsonApiContext = jsonApiContext; + _documentBuilder = documentBuilder; + } + + public JsonApiSerializer( + IJsonApiContext jsonApiContext, + IDocumentBuilder documentBuilder, + ILoggerFactory loggerFactory) + { + _jsonApiContext = jsonApiContext; _documentBuilder = documentBuilder; + _logger = loggerFactory?.CreateLogger(); } public string Serialize(object entity) { + if (entity == null) + return GetNullDataResponse(); + + if (entity.GetType() == typeof(ErrorCollection) || _jsonApiContext.RequestEntity == null) + return GetErrorJson(entity, _logger); + if (entity is IEnumerable) - return _serializeDocuments(entity); + return SerializeDocuments(entity); + + return SerializeDocument(entity); + } + + private string GetNullDataResponse() + { + return JsonConvert.SerializeObject(new Document + { + Data = null + }); + } - return _serializeDocument(entity); + private string GetErrorJson(object responseObject, ILogger logger) + { + if (responseObject is ErrorCollection errorCollection) + { + return errorCollection.GetJson(); + } + else + { + logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); + return JsonConvert.SerializeObject(responseObject); + } } - private string _serializeDocuments(object entity) + private string SerializeDocuments(object entity) { var entities = entity as IEnumerable; var documents = _documentBuilder.Build(entities); return _serialize(documents); } - private string _serializeDocument(object entity) + private string SerializeDocument(object entity) { var identifiableEntity = entity as IIdentifiable; var document = _documentBuilder.Build(identifiableEntity); From 3365d59c6d0cabe68e517fe5ab4dac74d144c48f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:04:04 -0500 Subject: [PATCH 26/43] fix(error-collection): use camel-case serialization --- src/JsonApiDotNetCore/Internal/ErrorCollection.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/ErrorCollection.cs b/src/JsonApiDotNetCore/Internal/ErrorCollection.cs index 6e5c375da1..bf0375843d 100644 --- a/src/JsonApiDotNetCore/Internal/ErrorCollection.cs +++ b/src/JsonApiDotNetCore/Internal/ErrorCollection.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Internal { @@ -20,7 +21,8 @@ public void Add(Error error) public string GetJson() { return JsonConvert.SerializeObject(this, new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() }); } } From 70b273a6ab90901d307013b45b73e0128005fb3c Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:04:30 -0500 Subject: [PATCH 27/43] test(extensibility): verify users can serialize custom errors --- .../Extensibility/CustomErrorTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs new file mode 100644 index 0000000000..3ba7d18156 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -0,0 +1,53 @@ +using DotNetCoreDocs; +using JsonApiDotNetCoreExample; +using DotNetCoreDocs.Writers; +using Newtonsoft.Json; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Serialization; +using Xunit; +using System.Diagnostics; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + public class CustomErrorTests + { + [Fact] + public void Can_Return_Custom_Error_Types() + { + // while(!Debugger.IsAttached) { bool stop = false; } + + // arrange + var error = new CustomError("507", "title", "detail", "custom"); + var errorCollection = new ErrorCollection(); + errorCollection.Add(error); + + var expectedJson = JsonConvert.SerializeObject(new { + errors = new dynamic[] { + new { + myCustomProperty = "custom", + title = "title", + detail = "detail", + status = "507" + } + } + }); + + // act + var result = new JsonApiSerializer(null, null, null) + .Serialize(errorCollection); + + // assert + Assert.Equal(expectedJson, result); + + } + + class CustomError : Error { + public CustomError(string status, string title, string detail, string myProp) + : base(status, title, detail) + { + MyCustomProperty = myProp; + } + public string MyCustomProperty { get; set; } + } + } +} From 7fd3e48aa68c2e915d4b78555d3751de8e4bd53d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:13:51 -0500 Subject: [PATCH 28/43] docs(readme): document custom error usage --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 29b8bff318..fc79478e28 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or - [Sorting](#sorting) - [Meta](#meta) - [Client Generated Ids](#client-generated-ids) + - [Custom Errors](#custom-errors) - [Tests](#tests) ## Comprehensive Demo @@ -364,6 +365,38 @@ services.AddJsonApi(opt => }); ``` +### Custom Errors + +By default, errors will only contain the properties defined by the internal [Error](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/src/JsonApiDotNetCore/Internal/Error.cs) class. However, you can create your own by inheriting from `Error` and either throwing it in a `JsonApiException` or returning the error from your controller. + +```chsarp +// custom error definition +public class CustomError : Error { + public CustomError(string status, string title, string detail, string myProp) + : base(status, title, detail) + { + MyCustomProperty = myProp; + } + public string MyCustomProperty { get; set; } +} + +// throwing a custom error +public void MyMethod() { + var error = new CustomError("507", "title", "detail", "custom"); + throw new JsonApiException(error); +} + +// returning from controller +[HttpPost] +public override async Task PostAsync([FromBody] MyEntity entity) +{ + if(_db.IsFull) + return new ObjectResult(new CustomError("507", "Database is full.", "Theres no more room.", "Sorry.")); + + // ... +} +``` + ## Tests I am using DotNetCoreDocs to generate sample requests and documentation. From 2b8038565d831510a0be1300c7e364cee9155526 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:22:36 -0500 Subject: [PATCH 29/43] docs(readme): fix syntax highlighting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc79478e28..5176708077 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ services.AddJsonApi(opt => By default, errors will only contain the properties defined by the internal [Error](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/src/JsonApiDotNetCore/Internal/Error.cs) class. However, you can create your own by inheriting from `Error` and either throwing it in a `JsonApiException` or returning the error from your controller. -```chsarp +```csharp // custom error definition public class CustomError : Error { public CustomError(string status, string title, string detail, string myProp) From 99f623d130e778399c98d544e63c7044302bd308 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 17:18:56 -0500 Subject: [PATCH 30/43] feat(IQueryable): add extension for selecting columns by list of names --- .../Extensions/IQueryableExtensions.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 2e2551603c..6cecb52762 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -1,4 +1,3 @@ - using System; using System.Linq; using System.Linq.Expressions; @@ -126,5 +125,25 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } + public static IQueryable Select(this IQueryable source, string[] columns) + { + var sourceType = source.ElementType; + + var resultType = typeof(TSource); + + // {model} + var parameter = Expression.Parameter(sourceType, "model"); + + var bindings = columns.Select(column => Expression.Bind( + resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); + + var body = Expression.MemberInit(Expression.New(resultType), bindings); + + var selector = Expression.Lambda(body, parameter); + + return source.Provider.CreateQuery( + Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, + source.Expression, Expression.Quote(selector))); + } } } From 1b8b6cbb0fbdc211bbbac4b18dd13f111d22a344 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 20:52:05 -0500 Subject: [PATCH 31/43] fix(IQueryableExt): clean up extension --- .../Extensions/IQueryableExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 6cecb52762..2c12bc78aa 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -125,7 +125,7 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, string[] columns) + public static IQueryable Select(this IQueryable source, string[] columns) { var sourceType = source.ElementType; @@ -137,13 +137,13 @@ public static IQueryable Select(this IQueryable source, string var bindings = columns.Select(column => Expression.Bind( resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); + // { new Model () { Property = model.Property } } var body = Expression.MemberInit(Expression.New(resultType), bindings); - var selector = Expression.Lambda(body, parameter); + // { model => new TodoItem() { Property = model.Property } } + var selector = Expression.Lambda>(body, parameter); - return source.Provider.CreateQuery( - Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, - source.Expression, Expression.Quote(selector))); + return source.Select(selector); } } } From aa6faf40efb458808f2acb5117ddf6f642e974b6 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 20:53:14 -0500 Subject: [PATCH 32/43] test(sparse-fieldsets): validate the use of the Select extension --- .../Acceptance/Spec/SparseFieldSetTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs new file mode 100644 index 0000000000..14e3c874a1 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using DotNetCoreDocs; +using DotNetCoreDocs.Writers; +using JsonApiDotNetCoreExample; +using Xunit; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCoreExample.Models; +using System.Linq; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class SparseFieldSetTests + { + private DocsFixture _fixture; + private readonly AppDbContext _dbContext; + + public SparseFieldSetTests(DocsFixture fixture) + { + _fixture = fixture; + _dbContext = fixture.GetService(); + } + + [Fact] + public async Task Can_Select_Sparse_Fieldsets() + { + // arrange + var fields = new string[] { "Id", "Description" }; + var todoItem = new TodoItem { + Description = "description", + Ordinal = 1 + }; + _dbContext.TodoItems.Add(todoItem); + await _dbContext.SaveChangesAsync(); + + // act + var result = await _dbContext + .TodoItems + .Where(t=>t.Id == todoItem.Id) + .Select(fields) + .FirstAsync(); + + // assert + Assert.Equal(0, result.Ordinal); + Assert.Equal(todoItem.Description, result.Description); + } + } +} From d50fb51d8847f6c01630c0a5e26e4a7574db21ba Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:17:19 -0500 Subject: [PATCH 33/43] clean(error-test): remove unused ns --- .../Acceptance/Extensibility/CustomErrorTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs index 3ba7d18156..ce2b541f5b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -1,11 +1,7 @@ -using DotNetCoreDocs; -using JsonApiDotNetCoreExample; -using DotNetCoreDocs.Writers; using Newtonsoft.Json; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Serialization; using Xunit; -using System.Diagnostics; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility { @@ -14,8 +10,6 @@ public class CustomErrorTests [Fact] public void Can_Return_Custom_Error_Types() { - // while(!Debugger.IsAttached) { bool stop = false; } - // arrange var error = new CustomError("507", "title", "detail", "custom"); var errorCollection = new ErrorCollection(); From a2ab5bd27c0559ea51624340dc61a449c6a3abd8 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:49:54 -0500 Subject: [PATCH 34/43] feat(query-set): parse fields parameter --- .../Internal/Query/QuerySet.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 864251e4a4..58f1c189f1 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -19,6 +19,7 @@ public QuerySet( _jsonApiContext = jsonApiContext; PageQuery = new PageQuery(); Filters = new List(); + Fields = new List(); BuildQuerySet(query); } @@ -26,6 +27,7 @@ public QuerySet( public PageQuery PageQuery { get; set; } public List SortParameters { get; set; } public List IncludedRelationships { get; set; } + public List Fields { get; set; } private void BuildQuerySet(IQueryCollection query) { @@ -55,6 +57,12 @@ private void BuildQuerySet(IQueryCollection query) continue; } + if (pair.Key.StartsWith("fields")) + { + Fields = ParseFieldsQuery(pair.Key, pair.Value); + continue; + } + throw new JsonApiException("400", $"{pair} is not a valid query."); } } @@ -160,6 +168,29 @@ private List ParseIncludedRelationships(string value) .ToList(); } + private List ParseFieldsQuery(string key, string value) + { + // expected: fields[TYPE]=prop1,prop2 + var typeName = key.Split('[', ']')[1]; + + var includedFields = new List { "Id" }; + + if(typeName != _jsonApiContext.RequestEntity.EntityName.Dasherize()) + return includedFields; + + var fields = value.Split(','); + foreach(var field in fields) + { + var internalAttrName = _jsonApiContext.RequestEntity + .Attributes + .SingleOrDefault(attr => attr.PublicAttributeName == field) + .InternalAttributeName; + includedFields.Add(internalAttrName); + } + + return includedFields; + } + private AttrAttribute GetAttribute(string propertyName) { return _jsonApiContext.RequestEntity.Attributes From 027b3b0daa02e79a30975aa203844edc8096a9de Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:50:17 -0500 Subject: [PATCH 35/43] fix(IQueryableExt): do not return dynamic type --- .../Extensions/IQueryableExtensions.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 2c12bc78aa..abd1686a22 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -125,8 +126,11 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, string[] columns) + public static IQueryable Select(this IQueryable source, IEnumerable columns) { + if(columns == null || columns.Count() == 0) + return source; + var sourceType = source.ElementType; var resultType = typeof(TSource); @@ -141,9 +145,11 @@ public static IQueryable Select(this IQueryable sourc var body = Expression.MemberInit(Expression.New(resultType), bindings); // { model => new TodoItem() { Property = model.Property } } - var selector = Expression.Lambda>(body, parameter); - - return source.Select(selector); + var selector = Expression.Lambda(body, parameter); + + return source.Provider.CreateQuery( + Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, + source.Expression, Expression.Quote(selector))); } } } From 66c0bd22971e6f2c31affb0e2f6689841d72ee5e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:50:37 -0500 Subject: [PATCH 36/43] feat(repository): apply select query --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 6e18aebc96..62102bec7b 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -48,7 +48,7 @@ public DefaultEntityRepository( public virtual IQueryable Get() { - return _dbSet; + return _dbSet.Select(_jsonApiContext.QuerySet?.Fields); } public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) @@ -76,12 +76,12 @@ public virtual IQueryable Sort(IQueryable entities, List GetAsync(TId id) { - return await _dbSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); + return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id)); } public virtual async Task GetAndIncludeAsync(TId id, string relationshipName) { - return await _dbSet + return await Get() .Include(relationshipName) .SingleOrDefaultAsync(e => e.Id.Equals(id)); } From b3c3f16fe0c06f1e1058406c9a1cd4927e152a44 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:51:07 -0500 Subject: [PATCH 37/43] feat(document-builder): check whether or not an attribute should be inc --- .../Builders/DocumentBuilder.cs | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 12710a4927..92c785c5d0 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -33,12 +33,12 @@ public Document Build(IIdentifiable entity) var document = new Document { - Data = _getData(contextEntity, entity), - Meta = _getMeta(entity), + Data = GetData(contextEntity, entity), + Meta = GetMeta(entity), Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; - document.Included = _appendIncludedObject(document.Included, contextEntity, entity); + document.Included = AppendIncludedObject(document.Included, contextEntity, entity); return document; } @@ -54,20 +54,20 @@ public Documents Build(IEnumerable entities) var documents = new Documents { Data = new List(), - Meta = _getMeta(entities.FirstOrDefault()), + Meta = GetMeta(entities.FirstOrDefault()), Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; foreach (var entity in entities) { - documents.Data.Add(_getData(contextEntity, entity)); - documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity); + documents.Data.Add(GetData(contextEntity, entity)); + documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity); } return documents; } - private Dictionary _getMeta(IIdentifiable entity) + private Dictionary GetMeta(IIdentifiable entity) { if (entity == null) return null; @@ -87,9 +87,9 @@ private Dictionary _getMeta(IIdentifiable entity) return null; } - private List _appendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) + private List AppendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) { - var includedEntities = _getIncludedEntities(contextEntity, entity); + var includedEntities = GetIncludedEntities(contextEntity, entity); if (includedEntities.Count > 0) { if (includedObject == null) @@ -100,7 +100,7 @@ private List _appendIncludedObject(List includedObje return includedObject; } - private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) + private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) { var data = new DocumentData { @@ -115,16 +115,24 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) contextEntity.Attributes.ForEach(attr => { - data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); + if(ShouldIncludeAttribute(attr)) + data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); }); if (contextEntity.Relationships.Count > 0) - _addRelationships(data, contextEntity, entity); + AddRelationships(data, contextEntity, entity); return data; } - private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) + private bool ShouldIncludeAttribute(AttrAttribute attr) + { + return (_jsonApiContext.QuerySet == null + || _jsonApiContext.QuerySet.Fields.Count == 0 + || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); + } + + private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) { data.Relationships = new Dictionary(); var linkBuilder = new LinkBuilder(_jsonApiContext); @@ -140,7 +148,7 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I } }; - if (_relationshipIsIncluded(r.InternalRelationshipName)) + if (RelationshipIsIncluded(r.InternalRelationshipName)) { var navigationEntity = _jsonApiContext.ContextGraph .GetRelationship(entity, r.InternalRelationshipName); @@ -148,49 +156,49 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I if(navigationEntity == null) relationshipData.SingleData = null; else if (navigationEntity is IEnumerable) - relationshipData.ManyData = _getRelationships((IEnumerable)navigationEntity, r.InternalRelationshipName); + relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity, r.InternalRelationshipName); else - relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName); + relationshipData.SingleData = GetRelationship(navigationEntity, r.InternalRelationshipName); } data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData); }); } - private List _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity) + private List GetIncludedEntities(ContextEntity contextEntity, IIdentifiable entity) { var included = new List(); contextEntity.Relationships.ForEach(r => { - if (!_relationshipIsIncluded(r.InternalRelationshipName)) return; + if (!RelationshipIsIncluded(r.InternalRelationshipName)) return; var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName); if (navigationEntity is IEnumerable) foreach (var includedEntity in (IEnumerable)navigationEntity) - _addIncludedEntity(included, (IIdentifiable)includedEntity); + AddIncludedEntity(included, (IIdentifiable)includedEntity); else - _addIncludedEntity(included, (IIdentifiable)navigationEntity); + AddIncludedEntity(included, (IIdentifiable)navigationEntity); }); return included; } - private void _addIncludedEntity(List entities, IIdentifiable entity) + private void AddIncludedEntity(List entities, IIdentifiable entity) { - var includedEntity = _getIncludedEntity(entity); + var includedEntity = GetIncludedEntity(entity); if(includedEntity != null) entities.Add(includedEntity); } - private DocumentData _getIncludedEntity(IIdentifiable entity) + private DocumentData GetIncludedEntity(IIdentifiable entity) { if(entity == null) return null; var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); - var data = _getData(contextEntity, entity); + var data = GetData(contextEntity, entity); data.Attributes = new Dictionary(); @@ -202,13 +210,13 @@ private DocumentData _getIncludedEntity(IIdentifiable entity) return data; } - private bool _relationshipIsIncluded(string relationshipName) + private bool RelationshipIsIncluded(string relationshipName) { return _jsonApiContext.IncludedRelationships != null && _jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase()); } - private List> _getRelationships(IEnumerable entities, string relationshipName) + private List> GetRelationships(IEnumerable entities, string relationshipName) { var objType = entities.GetType().GenericTypeArguments[0]; @@ -224,7 +232,7 @@ private List> _getRelationships(IEnumerable e } return relationships; } - private Dictionary _getRelationship(object entity, string relationshipName) + private Dictionary GetRelationship(object entity, string relationshipName) { var objType = entity.GetType(); From 743edd13e2d907d77117f5923cabda7250f32382 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:51:43 -0500 Subject: [PATCH 38/43] test(sparse-fields): test that fields can be restricted using query --- .../Acceptance/Spec/SparseFieldSetTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 14e3c874a1..abddc04613 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -8,6 +8,12 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCoreExample.Models; using System.Linq; +using Microsoft.AspNetCore.Hosting; +using System.Net.Http; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using JsonApiDotNetCore.Models; +using System.Diagnostics; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -46,5 +52,36 @@ public async Task Can_Select_Sparse_Fieldsets() Assert.Equal(0, result.Ordinal); Assert.Equal(todoItem.Description, result.Description); } + + [Fact] + public async Task Fields_Query_Selects_Sparse_Field_Sets() + { + // arrange + var todoItem = new TodoItem { + Description = "description", + Ordinal = 1 + }; + _dbContext.TodoItems.Add(todoItem); + await _dbContext.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description"; + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializeBody = JsonConvert.DeserializeObject(body); + + // assert + Assert.Equal(todoItem.StringId, deserializeBody.Data.Id); + Assert.Equal(1, deserializeBody.Data.Attributes.Count); + Assert.Equal(todoItem.Description, deserializeBody.Data.Attributes["description"]); + } } } From 4cb667228ae5c5baf7d9ae20c5bd1576ebd09c7b Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 07:45:48 -0500 Subject: [PATCH 39/43] docs(readme): document sparse fieldset support --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 5176708077..9ff27f5b73 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or - [Meta](#meta) - [Client Generated Ids](#client-generated-ids) - [Custom Errors](#custom-errors) + - [Sparse Fieldsets](#sparse-fieldsets) - [Tests](#tests) ## Comprehensive Demo @@ -397,6 +398,23 @@ public override async Task PostAsync([FromBody] MyEntity entity) } ``` +### Sparse Fieldsets + +We currently support top-level field selection. +What this means is you can restrict which fields are returned by a query using the `fields` query parameter, but this does not yet apply to included relationships. + +- Currently valid: +```http +GET /articles?fields[articles]=title,body HTTP/1.1 +Accept: application/vnd.api+json +``` + +- Not yet supported: +```http +GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1 +Accept: application/vnd.api+json +``` + ## Tests I am using DotNetCoreDocs to generate sample requests and documentation. From 3d58227686f41e727cf3492afbe3ce368e8f2eb2 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:09:49 -0500 Subject: [PATCH 40/43] chore(tests): move all helper classes into a helper dir --- .../{ => Helpers}/Repositories/AuthorizedTodoItemsRepository.cs | 0 .../{ => Helpers}/Services/IAuthorizationService.cs | 0 .../{ => Helpers}/Services/MetaService.cs | 0 .../{ => Helpers}/Startups/AuthorizedStartup.cs | 0 .../{ => Helpers}/Startups/ClientGeneratedIdsStartup.cs | 0 .../{ => Helpers}/Startups/MetaStartup.cs | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Repositories/AuthorizedTodoItemsRepository.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Services/IAuthorizationService.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Services/MetaService.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/AuthorizedStartup.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/ClientGeneratedIdsStartup.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/MetaStartup.cs (100%) diff --git a/test/JsonApiDotNetCoreExampleTests/Repositories/AuthorizedTodoItemsRepository.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Repositories/AuthorizedTodoItemsRepository.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Services/IAuthorizationService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Services/IAuthorizationService.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/AuthorizedStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/AuthorizedStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/ClientGeneratedIdsStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/ClientGeneratedIdsStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs From 69afe90571f1ab7c98564c091fb35987de0854af Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:09 -0500 Subject: [PATCH 41/43] feat(tests): add helper extension to get ef sql output --- .../Extensions/IQueryableExtensions.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs new file mode 100644 index 0000000000..a40dfb4a5a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Remotion.Linq.Parsing.Structure; +using Database = Microsoft.EntityFrameworkCore.Storage.Database; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + + public static class IQueryableExtensions + { + private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); + + private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler"); + + private static readonly PropertyInfo NodeTypeProviderField = QueryCompilerTypeInfo.DeclaredProperties.Single(x => x.Name == "NodeTypeProvider"); + + private static readonly MethodInfo CreateQueryParserMethod = QueryCompilerTypeInfo.DeclaredMethods.First(x => x.Name == "CreateQueryParser"); + + private static readonly FieldInfo DataBaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database"); + + private static readonly FieldInfo QueryCompilationContextFactoryField = typeof(Database).GetTypeInfo().DeclaredFields.Single(x => x.Name == "_queryCompilationContextFactory"); + + public static string ToSql(this IQueryable query) where TEntity : class + { + if (!(query is EntityQueryable) && !(query is InternalDbSet)) + throw new ArgumentException("Invalid query"); + + var queryCompiler = (IQueryCompiler)QueryCompilerField.GetValue(query.Provider); + var nodeTypeProvider = (INodeTypeProvider)NodeTypeProviderField.GetValue(queryCompiler); + var parser = (IQueryParser)CreateQueryParserMethod.Invoke(queryCompiler, new object[] { nodeTypeProvider }); + var queryModel = parser.GetParsedQuery(query.Expression); + var database = DataBaseField.GetValue(queryCompiler); + var queryCompilationContextFactory = (IQueryCompilationContextFactory)QueryCompilationContextFactoryField.GetValue(database); + var queryCompilationContext = queryCompilationContextFactory.Create(false); + var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); + modelVisitor.CreateQueryExecutor(queryModel); + var sql = modelVisitor.Queries.First().ToString(); + + return sql; + } + } +} \ No newline at end of file From ce969ed7371a731728276608569facee41d4b928 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:26 -0500 Subject: [PATCH 42/43] feat(tests): add helper extension to normalize strings --- .../Helpers/Extensions/StringExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..19c7491d2a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + + public static class StringExtensions + { + public static string Normalize(this string input) + { + return Regex.Replace(input, @"\s+", string.Empty) + .ToUpper() + .Replace('"', '\''); + } + } +} \ No newline at end of file From 0179f9e41b600d9c074a85554f9d67021776f3ed Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:44 -0500 Subject: [PATCH 43/43] test(sparse-fields): validate the result SQL --- .../Acceptance/Spec/SparseFieldSetTests.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index abddc04613..2b0be2dc59 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using JsonApiDotNetCore.Models; -using System.Diagnostics; +using JsonApiDotNetCoreExampleTests.Helpers.Extensions; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -40,17 +40,23 @@ public async Task Can_Select_Sparse_Fieldsets() }; _dbContext.TodoItems.Add(todoItem); await _dbContext.SaveChangesAsync(); + var expectedSql = $@"SELECT 't'.'Id', 't'.'Description' + FROM 'TodoItems' AS 't' + WHERE 't'.'Id' = {todoItem.Id}".Normalize(); // act - var result = await _dbContext + var query = _dbContext .TodoItems .Where(t=>t.Id == todoItem.Id) - .Select(fields) - .FirstAsync(); + .Select(fields); + + var resultSql = query.ToSql().Normalize(); + var result = await query.FirstAsync(); // assert Assert.Equal(0, result.Ordinal); Assert.Equal(todoItem.Description, result.Description); + Assert.Equal(expectedSql, resultSql); } [Fact]