diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index bd11dbbbd7..da0fa71ca1 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -28,14 +28,14 @@ public JsonApiSerializerBenchmarks() IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph); - var metaBuilderMock = new Mock>(); - var linkBuilderMock = new Mock(); - var includeBuilderMock = new Mock(); + var metaBuilder = new Mock().Object; + var linkBuilder = new Mock().Object; + var includeBuilder = new Mock().Object; var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); - _jsonApiSerializer = new ResponseSerializer(metaBuilderMock.Object, linkBuilderMock.Object, - includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options); + _jsonApiSerializer = new ResponseSerializer(metaBuilder, linkBuilder, + includeBuilder, fieldsToSerialize, resourceObjectBuilder, options); } private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 2cefca3858..1bd4678d69 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -1,25 +1,24 @@ # Metadata -Top-level metadata can be added to your API responses in two ways: globally and per resource type. In the event of a key collision, the resource meta will take precendence. +We support two ways to add json:api meta to your responses: global and per resource. ## Global Meta -Global metadata can be added by registering a service that implements `IResponseMeta`. +Global metadata can be added to the root of the response document by registering a service that implements `IResponseMeta`. This is useful if you need access to other registered services to build the meta object. ```c# -public class ResponseMetaService : IResponseMeta -{ - public ResponseMetaService(/*...other dependencies here */) { - // ... - } +// In Startup.ConfigureServices +services.AddSingleton(); - public Dictionary GetMeta() +public sealed class CopyrightResponseMeta : IResponseMeta +{ + public IReadOnlyDictionary GetMeta() { return new Dictionary { - {"copyright", "Copyright 2018 Example Corp."}, - {"authors", new string[] {"John Doe"}} + ["copyright"] = "Copyright (C) 2002 Umbrella Corporation.", + ["authors"] = new[] {"Alice", "Red Queen"} }; } } @@ -28,14 +27,13 @@ public class ResponseMetaService : IResponseMeta ```json { "meta": { - "copyright": "Copyright 2018 Example Corp.", + "copyright": "Copyright (C) 2002 Umbrella Corporation.", "authors": [ - "John Doe" + "Alice", + "Red Queen" ] }, - "data": { - // ... - } + "data": [] } ``` @@ -50,23 +48,34 @@ public class PersonDefinition : JsonApiResourceDefinition { } - public override IReadOnlyDictionary GetMeta() + public override IReadOnlyDictionary GetMeta(Person person) { - return new Dictionary + if (person.IsEmployee) { - ["notice"] = "Check our intranet at http://www.example.com for personal details." - }; + return new Dictionary + { + ["notice"] = "Check our intranet at http://www.example.com/employees/" + person.StringId + " for personal details." + }; + } + + return null; } } ``` ```json { - "meta": { - "notice": "Check our intranet at http://www.example.com for personal details." - }, - "data": { - // ... - } + "data": [ + { + "type": "people", + "id": "1", + "attributes": { + ... + }, + "meta": { + "notice": "Check our intranet at http://www.example.com/employees/1 for personal details." + } + } + ] } ``` diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs deleted file mode 100644 index aaa45b75a7..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCoreExample.Models; - -namespace JsonApiDotNetCoreExample.Definitions -{ - public class PersonDefinition : JsonApiResourceDefinition - { - public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) - { - } - - public override IReadOnlyDictionary GetMeta() - { - return new Dictionary - { - ["license"] = "MIT", - ["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/", - ["versions"] = new[] - { - "v4.0.0", - "v3.1.0", - "v2.5.2", - "v1.3.1" - } - }; - } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs new file mode 100644 index 0000000000..5d0dc01cf5 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCoreExample.Models; + +namespace JsonApiDotNetCoreExample.Definitions +{ + public sealed class TodoItemDefinition : JsonApiResourceDefinition + { + public TodoItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + { + } + + public override IDictionary GetMeta(TodoItem resource) + { + if (resource.Description != null && resource.Description.StartsWith("Important:")) + { + return new Dictionary + { + ["hasHighPriority"] = true + }; + } + + return base.GetMeta(resource); + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index b41e501b54..85116981ba 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -29,7 +29,7 @@ public override void ConfigureServices(IServiceCollection services) ConfigureClock(services); services.AddScoped(); - services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetRequiredService()); services.AddDbContext(options => { diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 4ffc42205a..7aeffd5518 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -45,7 +45,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv _mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder)); _intermediateProvider = services.BuildServiceProvider(); - var loggerFactory = _intermediateProvider.GetService(); + var loggerFactory = _intermediateProvider.GetRequiredService(); _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory); @@ -161,7 +161,7 @@ private void AddMiddlewareLayer() _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.AddSingleton(sp => sp.GetService()); + _services.AddSingleton(sp => sp.GetRequiredService()); _services.AddSingleton(); _services.AddScoped(); _services.AddScoped(); @@ -235,21 +235,21 @@ private void AddQueryStringLayer() _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); + _services.AddScoped(sp => sp.GetRequiredService()); _services.AddScoped(); _services.AddSingleton(); @@ -271,7 +271,8 @@ private void AddSerializationLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); + _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(typeof(ResponseSerializer<>)); _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 0fb325d8f2..ffa5fd340e 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Linq; using System.Reflection; using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCore.Services; @@ -70,7 +72,7 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection services.AddScoped(); services.AddScoped(sp => { - var graph = sp.GetService(); + var graph = sp.GetRequiredService(); return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings())); }); return services; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index 044431b8a5..ebab9cc134 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -22,7 +22,7 @@ public async Task ReadAsync(InputFormatterContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); - var reader = context.HttpContext.RequestServices.GetService(); + var reader = context.HttpContext.RequestServices.GetRequiredService(); return await reader.ReadAsync(context); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 644e829538..9f77fc58e7 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -22,7 +22,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); - var writer = context.HttpContext.RequestServices.GetService(); + var writer = context.HttpContext.RequestServices.GetRequiredService(); await writer.WriteAsync(context); } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index ab246ceb1f..b5708436f9 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -115,8 +115,8 @@ public interface IResourceDefinition QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters(); /// - /// Enables to add json:api meta information, specific to this resource type. + /// Enables to add json:api meta information, specific to this resource. /// - IReadOnlyDictionary GetMeta(); + IDictionary GetMeta(TResource resource); } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index 7dac3e616c..2f7a5837ee 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -42,8 +42,8 @@ public interface IResourceDefinitionAccessor object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource. /// - IReadOnlyDictionary GetMeta(Type resourceType); + IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance); } } diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 37c9d6e761..282ec058ef 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -103,7 +103,7 @@ public virtual QueryStringParameterHandlers OnRegisterQueryableHandle } /// - public virtual IReadOnlyDictionary GetMeta() + public virtual IDictionary GetMeta(TResource resource) { return null; } diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 7318e82236..ed9b2ea1dd 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -76,12 +76,12 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri } /// - public IReadOnlyDictionary GetMeta(Type resourceType) + public IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); dynamic resourceDefinition = GetResourceDefinition(resourceType); - return resourceDefinition.GetMeta(); + return resourceDefinition.GetMeta((dynamic) resourceInstance); } protected object GetResourceDefinition(Type resourceType) diff --git a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs index c939d9d139..c56607db75 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs @@ -1,27 +1,21 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Serialization.Building { /// - /// Builds the top-level meta object. This builder is generic to allow for - /// different top-level meta objects depending on the associated resource of the request. + /// Builds the top-level meta object. /// - /// Associated resource for which to build the meta element. - public interface IMetaBuilder where TResource : class, IIdentifiable + public interface IMetaBuilder { /// - /// Adds a key-value pair to the top-level meta object. + /// Merges the specified dictionary with existing key/value pairs. In the event of a key collision, + /// the value from the specified dictionary will overwrite the existing one. /// - void Add(string key, object value); - /// - /// Joins the new dictionary with the current one. In the event of a key collision, - /// the new value will overwrite the old one. - /// - void Add(IReadOnlyDictionary values); + void Add(IReadOnlyDictionary values); + /// /// Builds the top-level meta data object. /// - IDictionary GetMeta(); + IDictionary Build(); } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 8ff303c40c..0b81bf3553 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -14,16 +14,19 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes private readonly HashSet _included; private readonly IFieldsToSerialize _fieldsToSerialize; private readonly ILinkBuilder _linkBuilder; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider, + IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { _included = new HashSet(ResourceIdentifierObjectComparer.Instance); _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); } /// @@ -47,6 +50,17 @@ public IList Build() return null; } + /// + public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, + IReadOnlyCollection relationships = null) + { + var resourceObject = base.Build(resource, attributes, relationships); + + resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); + + return resourceObject; + } + /// public void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource) { diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index b9837ac7ba..dc455fe50c 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -3,48 +3,37 @@ using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.Serialization.Building { /// - public class MetaBuilder : IMetaBuilder where TResource : class, IIdentifiable + public class MetaBuilder : IMetaBuilder { private readonly IPaginationContext _paginationContext; private readonly IJsonApiOptions _options; - private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IResponseMeta _responseMeta; private Dictionary _meta = new Dictionary(); - public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor, IResponseMeta responseMeta = null) + public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResponseMeta responseMeta) { _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); _options = options ?? throw new ArgumentNullException(nameof(options)); - _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); - _responseMeta = responseMeta; + _responseMeta = responseMeta ?? throw new ArgumentNullException(nameof(responseMeta)); } /// - public void Add(string key, object value) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - - _meta[key] = value ?? throw new ArgumentNullException(nameof(value)); - } - - /// - public void Add(IReadOnlyDictionary values) + public void Add(IReadOnlyDictionary values) { if (values == null) throw new ArgumentNullException(nameof(values)); _meta = values.Keys.Union(_meta.Keys) - .ToDictionary(key => key, + .ToDictionary(key => key, key => values.ContainsKey(key) ? values[key] : _meta[key]); } /// - public IDictionary GetMeta() + public IDictionary Build() { if (_paginationContext.TotalResourceCount != null) { @@ -54,15 +43,10 @@ public IDictionary GetMeta() _meta.Add(key, _paginationContext.TotalResourceCount); } - if (_responseMeta != null) - { - Add(_responseMeta.GetMeta()); - } - - var resourceMeta = _resourceDefinitionAccessor.GetMeta(typeof(TResource)); - if (resourceMeta != null) + var extraMeta = _responseMeta.GetMeta(); + if (extraMeta != null) { - Add(resourceMeta); + Add(extraMeta); } return _meta.Any() ? _meta : null; diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index bb4453d64e..071fc73e30 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -23,24 +23,24 @@ public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, R } /// - public ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) + public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { if (resource == null) throw new ArgumentNullException(nameof(resource)); var resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); // populating the top-level "type" and "id" members. - var ro = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId == string.Empty ? null : resource.StringId }; + var resourceObject = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId == string.Empty ? null : resource.StringId }; // populating the top-level "attribute" member of a resource object. never include "id" as an attribute if (attributes != null && (attributes = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray()).Any()) - ProcessAttributes(resource, attributes, ro); + ProcessAttributes(resource, attributes, resourceObject); // populating the top-level "relationship" member of a resource object. if (relationships != null) - ProcessRelationships(resource, relationships, ro); + ProcessRelationships(resource, relationships, resourceObject); - return ro; + return resourceObject; } /// diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index e7a1344b53..6daae945ec 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -15,6 +15,7 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder { private readonly IIncludedResourceObjectBuilder _includedBuilder; private readonly IEnumerable _constraintProviders; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly ILinkBuilder _linkBuilder; private RelationshipAttribute _requestRelationship; @@ -22,12 +23,14 @@ public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, + IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); _includedBuilder = includedBuilder ?? throw new ArgumentNullException(nameof(includedBuilder)); _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); } public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) @@ -38,6 +41,17 @@ public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute req return GetRelationshipData(requestRelationship, resource); } + /// + public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, + IReadOnlyCollection relationships = null) + { + var resourceObject = base.Build(resource, attributes, relationships); + + resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); + + return resourceObject; + } + /// /// Builds the values of the relationships object on a resource object. /// The server serializer only populates the "data" member when the relationship is included, diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs index 2e0cf5f461..d216db5818 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs @@ -31,7 +31,7 @@ public string Serialize(IIdentifiable resource) { if (resource == null) { - var empty = Build((IIdentifiable) null, Array.Empty(), Array.Empty()); + var empty = Build((IIdentifiable) null, Array.Empty(), Array.Empty()); return SerializeObject(empty, _jsonSerializerSettings); } diff --git a/src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs new file mode 100644 index 0000000000..592a752926 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/EmptyResponseMeta.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Serialization +{ + /// + public sealed class EmptyResponseMeta : IResponseMeta + { + /// + public IReadOnlyDictionary GetMeta() + { + return null; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs index 2c423ed74e..85db12b6e9 100644 --- a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs @@ -5,11 +5,14 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Service to add global top-level json:api meta to a response . - /// Use to specify top-level metadata per resource type. + /// Provides a method to obtain global json:api meta, which is added at top-level to a response . + /// Use to specify nested metadata per individual resource. /// public interface IResponseMeta { + /// + /// Gets the global top-level json:api meta information to add to the response. + /// IReadOnlyDictionary GetMeta(); } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index 787bfe292e..c3ae877787 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -13,5 +13,8 @@ public sealed class ResourceObject : ResourceIdentifierObject [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] public ResourceLinks Links { get; set; } + + [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 5639ed5a77..2e5b0a4afc 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -29,12 +29,12 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer, private readonly IFieldsToSerialize _fieldsToSerialize; private readonly IJsonApiOptions _options; - private readonly IMetaBuilder _metaBuilder; + private readonly IMetaBuilder _metaBuilder; private readonly Type _primaryResourceType; private readonly ILinkBuilder _linkBuilder; private readonly IIncludedResourceObjectBuilder _includedBuilder; - public ResponseSerializer(IMetaBuilder metaBuilder, + public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, @@ -137,7 +137,7 @@ internal string SerializeMany(IReadOnlyCollection resources) private void AddTopLevelObjects(Document document) { document.Links = _linkBuilder.GetTopLevelLinks(); - document.Meta = _metaBuilder.GetMeta(); + document.Meta = _metaBuilder.Build(); document.Included = _includedBuilder.Build(); } } diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs index 356c606833..0232eec083 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs @@ -1,6 +1,7 @@ using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCore.Serialization { @@ -28,7 +29,7 @@ public IJsonApiSerializer GetSerializer() var targetType = GetDocumentType(); var serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); - var serializer = (IResponseSerializer)_provider.GetService(serializerType); + var serializer = (IResponseSerializer)_provider.GetRequiredService(serializerType); if (_request.Kind == EndpointKind.Relationship && _request.Relationship != null) serializer.RequestRelationship = _request.Relationship; diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 5f4c324cd7..5a7a67c000 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -89,7 +89,7 @@ public void DiscoverInjectables_Adds_Resource_Services_From_Current_Assembly_To_ // Assert var services = _services.BuildServiceProvider(); - var service = services.GetService>(); + var service = services.GetRequiredService>(); Assert.IsType(service); } @@ -105,7 +105,7 @@ public void DiscoverInjectables_Adds_Resource_Repositories_From_Current_Assembly // Assert var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); + Assert.IsType(services.GetRequiredService>()); } [Fact] @@ -120,7 +120,7 @@ public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_T // Assert var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); + Assert.IsType(services.GetRequiredService>()); } [Fact] @@ -137,7 +137,7 @@ public void AddCurrentAssembly_Adds_Resource_Hooks_Definitions_From_Current_Asse // Assert var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); + Assert.IsType(services.GetRequiredService>()); } public sealed class TestModel : Identifiable { } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index cdd5d448e4..86c021e86d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -81,7 +81,7 @@ public async Task CustomRouteControllers_Uses_Dasherized_Collection_Route() public async Task CustomRouteControllers_Uses_Dasherized_Item_Route() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); todoItem.Owner = person; @@ -108,7 +108,7 @@ public async Task CustomRouteControllers_Uses_Dasherized_Item_Route() public async Task CustomRouteControllers_Creates_Proper_Relationship_Links() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); todoItem.Owner = person; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs index bc4400b7c1..144bc9218c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Xunit; @@ -23,7 +24,7 @@ public sealed class IgnoreDefaultValuesTests : IAsyncLifetime public IgnoreDefaultValuesTests(TestFixture fixture) { - _dbContext = fixture.GetService(); + _dbContext = fixture.GetRequiredService(); _todoItem = new TodoItem { CreatedDate = default, @@ -95,7 +96,7 @@ public async Task CheckBehaviorCombination(DefaultValueHandling? defaultValue, b var services = server.Host.Services; var client = server.CreateClient(); - var options = (JsonApiOptions)services.GetService(typeof(IJsonApiOptions)); + var options = (JsonApiOptions)services.GetRequiredService(typeof(IJsonApiOptions)); if (defaultValue != null) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs index f6a03bf098..e4ac1e61cc 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Xunit; @@ -23,7 +24,7 @@ public sealed class IgnoreNullValuesTests : IAsyncLifetime public IgnoreNullValuesTests(TestFixture fixture) { - _dbContext = fixture.GetService(); + _dbContext = fixture.GetRequiredService(); _todoItem = new TodoItem { Description = null, @@ -98,7 +99,7 @@ public async Task CheckBehaviorCombination(NullValueHandling? defaultValue, bool var services = server.Host.Services; var client = server.CreateClient(); - var options = (JsonApiOptions)services.GetService(typeof(IJsonApiOptions)); + var options = (JsonApiOptions)services.GetRequiredService(typeof(IJsonApiOptions)); if (defaultValue != null) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs deleted file mode 100644 index 326240b6cc..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCoreExample.Data; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility -{ - public sealed class RequestMetaTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - public RequestMetaTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - testContext.ConfigureServicesBeforeStartup(services => - { - services.AddScoped(); - }); - } - - [Fact] - public async Task Injecting_IResponseMeta_Adds_Meta_Data() - { - // Arrange - var route = "/api/v1/people"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta.ContainsKey("request-meta").Should().BeTrue(); - responseDocument.Meta["request-meta"].Should().Be("request-meta-value"); - } - } - - public sealed class TestResponseMeta : IResponseMeta - { - public IReadOnlyDictionary GetMeta() - { - return new Dictionary - { - {"request-meta", "request-meta-value"} - }; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs index 1af223de4a..4d6dc63aaa 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs @@ -27,7 +27,7 @@ public class InjectableResourceTests public InjectableResourceTests(TestFixture fixture) { _fixture = fixture; - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 76d18461b7..eda2dcdd4d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -31,7 +31,7 @@ public sealed class ManyToManyTests public ManyToManyTests(TestFixture fixture) { _fixture = fixture; - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); _authorFaker = new Faker() .RuleFor(a => a.LastName, f => f.Random.Words(2)); @@ -49,7 +49,7 @@ public ManyToManyTests(TestFixture fixture) public async Task Can_Fetch_Many_To_Many_Through_Id() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); var articleTag = new ArticleTag @@ -88,7 +88,7 @@ public async Task Can_Fetch_Many_To_Many_Through_Id() public async Task Can_Fetch_Many_To_Many_Through_GetById_Relationship_Link() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); var articleTag = new ArticleTag @@ -126,7 +126,7 @@ public async Task Can_Fetch_Many_To_Many_Through_GetById_Relationship_Link() public async Task Can_Fetch_Many_To_Many_Through_Relationship_Link() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); var articleTag = new ArticleTag @@ -164,7 +164,7 @@ public async Task Can_Fetch_Many_To_Many_Through_Relationship_Link() public async Task Can_Fetch_Many_To_Many_Without_Include() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); var articleTag = new ArticleTag @@ -197,7 +197,7 @@ public async Task Can_Fetch_Many_To_Many_Without_Include() public async Task Can_Create_Many_To_Many() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var tag = _tagFaker.Generate(); var author = _authorFaker.Generate(); context.Tags.Add(tag); @@ -267,7 +267,7 @@ public async Task Can_Create_Many_To_Many() public async Task Can_Update_Many_To_Many() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var tag = _tagFaker.Generate(); var article = _articleFaker.Generate(); context.Tags.Add(tag); @@ -327,7 +327,7 @@ public async Task Can_Update_Many_To_Many() public async Task Can_Update_Many_To_Many_With_Complete_Replacement() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var firstTag = _tagFaker.Generate(); var article = _articleFaker.Generate(); var articleTag = new ArticleTag @@ -391,7 +391,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement() public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var firstTag = _tagFaker.Generate(); var article = _articleFaker.Generate(); var articleTag = new ArticleTag @@ -459,7 +459,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap public async Task Can_Update_Many_To_Many_Through_Relationship_Link() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var tag = _tagFaker.Generate(); var article = _articleFaker.Generate(); context.Tags.Add(tag); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs index 7b5f67905f..214f8adbda 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs @@ -47,16 +47,6 @@ public async Task When_getting_person_it_must_match_JSON_text() var json = JsonConvert.DeserializeObject(bodyText).ToString(); var expected = @"{ - ""meta"": { - ""license"": ""MIT"", - ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", - ""versions"": [ - ""v4.0.0"", - ""v3.1.0"", - ""v2.5.2"", - ""v1.3.1"" - ] - }, ""links"": { ""self"": ""http://localhost/api/v1/people/123"" }, diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs index 2a434f8508..3b74dd7502 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs @@ -20,7 +20,7 @@ public sealed class DeletingDataTests public DeletingDataTests(TestFixture fixture) { - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs index e347d8097f..25bc0d8a80 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs @@ -27,7 +27,7 @@ public async Task GET_RelativeLinks_True_With_Namespace_Returns_RelativeLinks() var route = "/api/v1/people/" + person.StringId; var request = new HttpRequestMessage(HttpMethod.Get, route); - var options = (JsonApiOptions) _factory.GetService(); + var options = (JsonApiOptions) _factory.GetRequiredService(); options.UseRelativeLinks = true; // Act @@ -52,7 +52,7 @@ public async Task GET_RelativeLinks_False_With_Namespace_Returns_AbsoluteLinks() var route = "/api/v1/people/" + person.StringId; var request = new HttpRequestMessage(HttpMethod.Get, route); - var options = (JsonApiOptions) _factory.GetService(); + var options = (JsonApiOptions) _factory.GetRequiredService(); options.UseRelativeLinks = false; // Act diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs deleted file mode 100644 index 351dd35d47..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System.Collections; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests -{ - [Collection("WebHostCollection")] - public sealed class Meta - { - private readonly TestFixture _fixture; - private readonly AppDbContext _context; - public Meta(TestFixture fixture) - { - _fixture = fixture; - _context = fixture.GetService(); - } - - [Fact] - public async Task Total_Resource_Count_Included() - { - // Arrange - await _context.ClearTableAsync(); - _context.TodoItems.Add(new TodoItem()); - await _context.SaveChangesAsync(); - var expectedCount = 1; - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - - var httpMethod = new HttpMethod("GET"); - var route = "/api/v1/todoItems"; - - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(documents.Meta); - Assert.Equal(expectedCount, (long)documents.Meta["totalResources"]); - } - - [Fact] - public async Task Total_Resource_Count_Included_When_None() - { - // Arrange - await _context.ClearTableAsync(); - await _context.SaveChangesAsync(); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - - var httpMethod = new HttpMethod("GET"); - var route = "/api/v1/todoItems"; - - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(documents.Meta); - Assert.Equal(0, (long)documents.Meta["totalResources"]); - } - - [Fact] - public async Task Total_Resource_Count_Not_Included_In_POST_Response() - { - // Arrange - await _context.ClearTableAsync(); - await _context.SaveChangesAsync(); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todoItems"; - - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todoItems", - attributes = new - { - description = "New Description" - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.True(documents.Meta == null || !documents.Meta.ContainsKey("totalResources")); - } - - [Fact] - public async Task Total_Resource_Count_Not_Included_In_PATCH_Response() - { - // Arrange - await _context.ClearTableAsync(); - TodoItem todoItem = new TodoItem(); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - - var httpMethod = new HttpMethod("PATCH"); - var route = $"/api/v1/todoItems/{todoItem.Id}"; - - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todoItems", - id = todoItem.Id, - attributes = new - { - description = "New Description" - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - - // Act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(documents.Meta == null || !documents.Meta.ContainsKey("totalResources")); - } - - [Fact] - public async Task ResourceThatImplements_IHasMeta_Contains_MetaData() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder() - .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); - var expectedMeta = _fixture.GetService>().GetMeta(); - - // 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 listValue) - { - 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]); - } - } - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs index cf183eb641..dacac855a8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs @@ -24,7 +24,7 @@ public sealed class Relationships public Relationships(TestFixture fixture) { - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index bdaf4ee109..701235ba41 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -51,7 +51,7 @@ public AppDbContext PrepareTest() where TStartup : class public AppDbContext GetDbContext() { - return _fixture.GetService(); + return _fixture.GetRequiredService(); } public async Task<(string, HttpResponseMessage)> SendRequest(string method, string route, string content = null) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 8a1ba5b09b..d80be3aba0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -41,7 +41,7 @@ public FetchingDataTests(TestFixture fixture) public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); await context.ClearTableAsync(); await context.SaveChangesAsync(); @@ -72,7 +72,7 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() public async Task Included_Resources_Contain_Relationship_Links() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); todoItem.Owner = person; @@ -105,7 +105,7 @@ public async Task Included_Resources_Contain_Relationship_Links() public async Task GetResources_NoDefaultPageSize_ReturnsResources() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); await context.ClearTableAsync(); await context.SaveChangesAsync(); @@ -138,7 +138,7 @@ public async Task GetResources_NoDefaultPageSize_ReturnsResources() public async Task GetSingleResource_ResourceDoesNotExist_ReturnsNotFound() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); await context.ClearTableAsync(); await context.SaveChangesAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 152bc31d82..3c65dec700 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -40,7 +40,7 @@ public async Task When_getting_existing_ToOne_relationship_it_should_succeed() var todoItem = _todoItemFaker.Generate(); todoItem.Owner = new Person(); - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -93,7 +93,7 @@ public async Task When_getting_existing_ToMany_relationship_it_should_succeed() } }; - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -140,7 +140,7 @@ public async Task When_getting_related_missing_to_one_resource_it_should_succeed var todoItem = _todoItemFaker.Generate(); todoItem.Owner = null; - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -161,16 +161,6 @@ public async Task When_getting_related_missing_to_one_resource_it_should_succeed var json = JsonConvert.DeserializeObject(body).ToString(); var expected = @"{ - ""meta"": { - ""license"": ""MIT"", - ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", - ""versions"": [ - ""v4.0.0"", - ""v3.1.0"", - ""v2.5.2"", - ""v1.3.1"" - ] - }, ""links"": { ""self"": ""http://localhost/api/v1/todoItems/" + todoItem.StringId + @"/owner"" }, @@ -187,7 +177,7 @@ public async Task When_getting_relationship_for_missing_to_one_resource_it_shoul var todoItem = _todoItemFaker.Generate(); todoItem.Owner = null; - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -217,7 +207,7 @@ public async Task When_getting_related_missing_to_many_resource_it_should_succee var todoItem = _todoItemFaker.Generate(); todoItem.ChildrenTodos = new List(); - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -247,7 +237,7 @@ public async Task When_getting_relationship_for_missing_to_many_resource_it_shou var todoItem = _todoItemFaker.Generate(); todoItem.ChildrenTodos = new List(); - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -324,7 +314,7 @@ public async Task When_getting_unknown_related_resource_it_should_fail() // Arrange var todoItem = _todoItemFaker.Generate(); - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -354,7 +344,7 @@ public async Task When_getting_unknown_relationship_for_resource_it_should_fail( // Arrange var todoItem = _todoItemFaker.Generate(); - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs index 18fdf77398..7e4a14b094 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs @@ -28,7 +28,7 @@ public FunctionalTestCollection(TFactory factory) { _factory = factory; _client = _factory.CreateClient(); - _dbContext = _factory.GetService(); + _dbContext = _factory.GetRequiredService(); _deserializer = GetDeserializer(); ClearDbContext(); } @@ -83,7 +83,7 @@ protected IResponseDeserializer GetDeserializer() protected AppDbContext GetDbContext() => GetService(); - protected T GetService() => _factory.GetService(); + protected T GetService() => _factory.GetRequiredService(); protected void AssertEqualStatusCode(HttpStatusCode expected, HttpResponseMessage response) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index b575c4a890..ad62bb6b6e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -34,7 +34,7 @@ public sealed class UpdatingDataTests : EndToEndTest public UpdatingDataTests(TestFixture fixture) : base(fixture) { - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index 52cda9df1b..5764b77542 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -31,7 +31,7 @@ public sealed class UpdatingRelationshipsTests public UpdatingRelationshipsTests(TestFixture fixture) { _fixture = fixture; - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); _personFaker = new Faker() .RuleFor(t => t.FirstName, f => f.Name.FirstName()) .RuleFor(t => t.LastName, f => f.Name.LastName()); @@ -92,7 +92,7 @@ public async Task Can_Update_Cyclic_ToMany_Relationship_By_Patching_Resource() // Act await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItem = _context.TodoItems.AsNoTracking() .Where(ti => ti.Id == todoItem.Id) @@ -144,7 +144,7 @@ public async Task Can_Update_Cyclic_ToOne_Relationship_By_Patching_Resource() // Act await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItem = _context.TodoItems.AsNoTracking() .Where(ti => ti.Id == todoItem.Id) @@ -207,7 +207,7 @@ public async Task Can_Update_Both_Cyclic_ToOne_And_ToMany_Relationship_By_Patchi // Act await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItem = _context.TodoItems.AsNoTracking() .Where(ti => ti.Id == todoItem.Id) @@ -271,7 +271,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource() // Act var response = await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItems = _context.TodoItemCollections.AsNoTracking() .Where(tic => tic.Id == todoCollection.Id) .Include(tdc => tdc.TodoItems).SingleOrDefault().TodoItems; @@ -349,7 +349,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_When_Targe // Act var response = await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItems = _context.TodoItemCollections.AsNoTracking() .Where(tic => tic.Id == todoCollection.Id) .Include(tdc => tdc.TodoItems).SingleOrDefault().TodoItems; @@ -415,7 +415,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_With_Overl var response = await client.SendAsync(request); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var updatedTodoItems = _context.TodoItemCollections.AsNoTracking() .Where(tic => tic.Id == todoCollection.Id) .Include(tdc => tdc.TodoItems).SingleOrDefault().TodoItems; @@ -470,7 +470,7 @@ public async Task Can_Update_ToMany_Relationship_ThroughLink() var body = response.Content.ReadAsStringAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - _context = _fixture.GetService(); + _context = _fixture.GetRequiredService(); var personsTodoItems = _context.People.Include(p => p.TodoItems).Single(p => p.Id == person.Id).TodoItems; Assert.NotEmpty(personsTodoItems); @@ -664,7 +664,7 @@ public async Task Can_Delete_Relationship_By_Patching_Relationship() public async Task Updating_ToOne_Relationship_With_Implicit_Remove() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var passport = new Passport(context); var person1 = _personFaker.Generate(); person1.Passport = passport; @@ -712,7 +712,7 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove() public async Task Updating_ToMany_Relationship_With_Implicit_Remove() { // Arrange - var context = _fixture.GetService(); + var context = _fixture.GetRequiredService(); var person1 = _personFaker.Generate(); person1.TodoItems = _todoItemFaker.Generate(3).ToHashSet(); var person2 = _personFaker.Generate(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index b9e35a4ca2..5868e34fc0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -31,7 +31,7 @@ public TestFixture() ServiceProvider = _server.Host.Services; Client = _server.CreateClient(); - Context = GetService().GetContext() as AppDbContext; + Context = GetRequiredService().GetContext() as AppDbContext; } public HttpClient Client { get; set; } @@ -39,8 +39,8 @@ public TestFixture() public static IRequestSerializer GetSerializer(IServiceProvider serviceProvider, Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable { - var serializer = (IRequestSerializer)serviceProvider.GetService(typeof(IRequestSerializer)); - var graph = (IResourceGraph)serviceProvider.GetService(typeof(IResourceGraph)); + var serializer = (IRequestSerializer)serviceProvider.GetRequiredService(typeof(IRequestSerializer)); + var graph = (IResourceGraph)serviceProvider.GetRequiredService(typeof(IResourceGraph)); serializer.AttributesToSerialize = attributes != null ? graph.GetAttributes(attributes) : null; serializer.RelationshipsToSerialize = relationships != null ? graph.GetRelationships(relationships) : null; return serializer; @@ -48,8 +48,8 @@ public static IRequestSerializer GetSerializer(IServiceProvider servi public IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable { - var serializer = GetService(); - var graph = GetService(); + var serializer = GetRequiredService(); + var graph = GetRequiredService(); serializer.AttributesToSerialize = attributes != null ? graph.GetAttributes(attributes) : null; serializer.RelationshipsToSerialize = relationships != null ? graph.GetRelationships(relationships) : null; return serializer; @@ -57,7 +57,7 @@ public IRequestSerializer GetSerializer(Expression(); + var options = GetRequiredService(); var resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance) .Add() @@ -74,12 +74,12 @@ public IResponseDeserializer GetDeserializer() return new ResponseDeserializer(resourceGraph, new ResourceFactory(ServiceProvider)); } - public T GetService() => (T)ServiceProvider.GetService(typeof(T)); + public T GetRequiredService() => (T)ServiceProvider.GetRequiredService(typeof(T)); public void ReloadDbContext() { ISystemClock systemClock = ServiceProvider.GetRequiredService(); - DbContextOptions options = GetService>(); + DbContextOptions options = GetRequiredService>(); Context = new AppDbContext(options, systemClock); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs index b4b03d5a65..9a75fcbb82 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs @@ -31,7 +31,7 @@ public sealed class TodoItemControllerTests public TodoItemControllerTests(TestFixture fixture) { _fixture = fixture; - _context = fixture.GetService(); + _context = fixture.GetRequiredService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -49,7 +49,7 @@ public async Task Can_Get_TodoItems_Paginate_Check() // Arrange await _context.ClearTableAsync(); await _context.SaveChangesAsync(); - var expectedResourcesPerPage = _fixture.GetService().DefaultPageSize.Value; + var expectedResourcesPerPage = _fixture.GetRequiredService().DefaultPageSize.Value; var person = new Person(); var todoItems = _todoItemFaker.Generate(expectedResourcesPerPage + 1); diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 9496c0394b..570e479b16 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -1,4 +1,3 @@ -using System.Reflection; using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -24,8 +23,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.DefaultPageSize = new PageSize(5); options.IncludeTotalResourceCount = true; options.AllowClientGeneratedIds = true; + options.IncludeExceptionStackTraceInErrors = true; }, - discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + discovery => discovery.AddAssembly(typeof(JsonApiDotNetCoreExample.Program).Assembly)); }); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs b/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs index d9d1fb8fb5..04e237acb8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs @@ -20,7 +20,7 @@ public CustomApplicationFactoryBase() _scope = Services.CreateScope(); } - public T GetService() => (T)_scope.ServiceProvider.GetService(typeof(T)); + public T GetRequiredService() => (T)_scope.ServiceProvider.GetRequiredService(typeof(T)); protected override void ConfigureWebHost(IWebHostBuilder builder) { @@ -32,7 +32,7 @@ public interface IApplicationFactory { IServiceProvider ServiceProvider { get; } - T GetService(); + T GetRequiredService(); HttpClient CreateClient(); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs index b38c58be59..72f879054b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs @@ -1,4 +1,3 @@ -using System.Reflection; using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -25,8 +24,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.IncludeTotalResourceCount = true; options.EnableResourceHooks = true; options.LoadDatabaseValues = true; + options.IncludeExceptionStackTraceInErrors = true; }, - discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + discovery => discovery.AddAssembly(typeof(JsonApiDotNetCoreExample.Program).Assembly)); }); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/TestServerExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/TestServerExtensions.cs index a7363e7b32..a15ad59bb4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/TestServerExtensions.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/TestServerExtensions.cs @@ -1,12 +1,13 @@ using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions { public static class TestServerExtensions { - public static T GetService(this TestServer server) + public static T GetRequiredService(this TestServer server) { - return (T)server.Host.Services.GetService(typeof(T)); + return (T)server.Host.Services.GetRequiredService(typeof(T)); } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceTests.cs new file mode 100644 index 0000000000..60690f1758 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceTests.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta +{ + public sealed class ResourceTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public ResourceTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task ResourceDefinition_That_Implements_GetMeta_Contains_Resource_Meta() + { + // Arrange + var todoItems = new[] + { + new TodoItem {Id = 1, Description = "Important: Pay the bills"}, + new TodoItem {Id = 2, Description = "Plan my birthday party"}, + new TodoItem {Id = 3, Description = "Important: Call mom"} + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.TodoItems.AddRange(todoItems); + + await dbContext.SaveChangesAsync(); + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.ManyData[1].Meta.Should().BeNull(); + responseDocument.ManyData[2].Meta.Should().ContainKey("hasHighPriority"); + } + + [Fact] + public async Task ResourceDefinition_That_Implements_GetMeta_Contains_Include_Meta() + { + // Arrange + var person = new Person + { + TodoItems = new HashSet + { + new TodoItem {Id = 1, Description = "Important: Pay the bills"}, + } + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.People.Add(person); + await dbContext.SaveChangesAsync(); + }); + + var route = $"/api/v1/people/{person.StringId}?include=todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Meta.Should().ContainKey("hasHighPriority"); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs new file mode 100644 index 0000000000..353ae641de --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta +{ + public sealed class ResponseMetaTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public ResponseMetaTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + testContext.ConfigureServicesAfterStartup(services => + { + services.AddSingleton(); + }); + + var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = false; + } + + [Fact] + public async Task Registered_IResponseMeta_Adds_TopLevel_Meta() + { + // Arrange + await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); }); + + var route = "/api/v1/people"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + var expected = @"{ + ""meta"": { + ""license"": ""MIT"", + ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", + ""versions"": [ + ""v4.0.0"", + ""v3.1.0"", + ""v2.5.2"", + ""v1.3.1"" + ] + }, + ""links"": { + ""self"": ""http://localhost/api/v1/people"", + ""first"": ""http://localhost/api/v1/people"" + }, + ""data"": [] +}"; + + responseDocument.ToString().NormalizeLineEndings().Should().Be(expected.NormalizeLineEndings()); + } + } + + public sealed class TestResponseMeta : IResponseMeta + { + public IReadOnlyDictionary GetMeta() + { + return new Dictionary + { + ["license"] = "MIT", + ["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/", + ["versions"] = new[] + { + "v4.0.0", + "v3.1.0", + "v2.5.2", + "v1.3.1" + } + }; + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs new file mode 100644 index 0000000000..ce5c9f0577 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -0,0 +1,133 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta +{ + public sealed class TopLevelCountTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public TopLevelCountTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Collection() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.TodoItems.Add(todoItem); + + await dbContext.SaveChangesAsync(); + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(1); + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Empty_Collection() + { + // Arrange + await _testContext.RunOnDatabaseAsync(async dbContext => { await dbContext.ClearTableAsync(); }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(0); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_POST_Response() + { + // Arrange + var requestBody = new + { + data = new + { + type = "todoItems", + attributes = new + { + description = "Something" + } + } + }; + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.Meta.Should().BeNull(); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_PATCH_Response() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.Id, + attributes = new + { + description = "Something else" + } + } + }; + + var route = "/api/v1/todoItems/" + todoItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().BeNull(); + } + } +} diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index ccddd24f1f..dc3931d429 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -38,7 +38,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() var provider = services.BuildServiceProvider(); // Assert - var request = provider.GetService() as JsonApiRequest; + var request = provider.GetRequiredService() as JsonApiRequest; Assert.NotNull(request); var resourceGraph = provider.GetService(); Assert.NotNull(resourceGraph); @@ -48,7 +48,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService(typeof(IResourceRepository))); 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()); @@ -72,7 +72,7 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() // to get the request scoped DbContext instance services.AddScoped(); var provider = services.BuildServiceProvider(); - var graph = provider.GetService(); + var graph = provider.GetRequiredService(); var resourceContext = graph.GetResourceContext(); // Assert @@ -90,16 +90,16 @@ public void AddResourceService_Registers_All_Shorthand_Service_Interfaces() // Assert var provider = services.BuildServiceProvider(); - Assert.IsType(provider.GetService(typeof(IResourceService))); - Assert.IsType(provider.GetService(typeof(IResourceCommandService))); - Assert.IsType(provider.GetService(typeof(IResourceQueryService))); - Assert.IsType(provider.GetService(typeof(IGetAllService))); - Assert.IsType(provider.GetService(typeof(IGetByIdService))); - Assert.IsType(provider.GetService(typeof(IGetSecondaryService))); - Assert.IsType(provider.GetService(typeof(IGetRelationshipService))); - Assert.IsType(provider.GetService(typeof(ICreateService))); - Assert.IsType(provider.GetService(typeof(IUpdateService))); - Assert.IsType(provider.GetService(typeof(IDeleteService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceCommandService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceQueryService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetAllService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetByIdService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetSecondaryService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetRelationshipService))); + Assert.IsType(provider.GetRequiredService(typeof(ICreateService))); + Assert.IsType(provider.GetRequiredService(typeof(IUpdateService))); + Assert.IsType(provider.GetRequiredService(typeof(IDeleteService))); } [Fact] @@ -113,16 +113,16 @@ public void AddResourceService_Registers_All_LongForm_Service_Interfaces() // Assert var provider = services.BuildServiceProvider(); - Assert.IsType(provider.GetService(typeof(IResourceService))); - Assert.IsType(provider.GetService(typeof(IResourceCommandService))); - Assert.IsType(provider.GetService(typeof(IResourceQueryService))); - Assert.IsType(provider.GetService(typeof(IGetAllService))); - Assert.IsType(provider.GetService(typeof(IGetByIdService))); - Assert.IsType(provider.GetService(typeof(IGetSecondaryService))); - Assert.IsType(provider.GetService(typeof(IGetRelationshipService))); - Assert.IsType(provider.GetService(typeof(ICreateService))); - Assert.IsType(provider.GetService(typeof(IUpdateService))); - Assert.IsType(provider.GetService(typeof(IDeleteService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceCommandService))); + Assert.IsType(provider.GetRequiredService(typeof(IResourceQueryService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetAllService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetByIdService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetSecondaryService))); + Assert.IsType(provider.GetRequiredService(typeof(IGetRelationshipService))); + Assert.IsType(provider.GetRequiredService(typeof(ICreateService))); + Assert.IsType(provider.GetRequiredService(typeof(IUpdateService))); + Assert.IsType(provider.GetRequiredService(typeof(IDeleteService))); } [Fact] @@ -150,7 +150,7 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( // Assert var provider = services.BuildServiceProvider(); - var resourceGraph = provider.GetService(); + var resourceGraph = provider.GetRequiredService(); var resource = resourceGraph.GetResourceContext(typeof(IntResource)); Assert.Equal("intResources", resource.PublicName); } diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs index e5a2d696ce..0089f70262 100644 --- a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs +++ b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.Design; using System.Text.RegularExpressions; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Building; diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs index 58d380cee9..1e884c28f1 100644 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Linq; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; using UnitTests.TestModels; diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 840250f799..233e5568ec 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -41,12 +41,12 @@ public SerializerTestsSetup() protected ResponseSerializer GetResponseSerializer(List> inclusionChains = null, Dictionary metaDict = null, TopLevelLinks topLinks = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) where T : class, IIdentifiable { - var meta = GetMetaBuilder(metaDict); + var meta = GetMetaBuilder(metaDict); var link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks); var includeConstraints = GetIncludeConstraints(inclusionChains); var includedBuilder = GetIncludedBuilder(); var fieldsToSerialize = GetSerializableFields(); - ResponseResourceObjectBuilder resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, _resourceGraph, GetSerializerSettingsProvider()); + ResponseResourceObjectBuilder resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, _resourceGraph, GetResourceDefinitionAccessor(), GetSerializerSettingsProvider()); return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new JsonApiOptions()); } @@ -55,12 +55,12 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List
  • GetMetaBuilder(Dictionary meta = null) where T : class, IIdentifiable + protected IResourceDefinitionAccessor GetResourceDefinitionAccessor() { - var mock = new Mock>(); - mock.Setup(m => m.GetMeta()).Returns(meta); + var mock = new Mock(); + return mock.Object; + } + + protected IMetaBuilder GetMetaBuilder(Dictionary meta = null) + { + var mock = new Mock(); + mock.Setup(m => m.Build()).Returns(meta); return mock.Object; } diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index 798f3b4f79..e6f43b0919 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; +using Moq; using UnitTests.TestModels; using Xunit; @@ -167,8 +169,9 @@ private IncludedResourceObjectBuilder GetBuilder() { var fields = GetSerializableFields(); var links = GetLinkBuilder(); - return new IncludedResourceObjectBuilder(fields, links, _resourceGraph, GetSerializerSettingsProvider()); - } + var accessor = new Mock().Object; + return new IncludedResourceObjectBuilder(fields, links, _resourceGraph, accessor, GetSerializerSettingsProvider()); + } } }