Skip to content

Resource-specific meta #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ public JsonApiSerializerBenchmarks()
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph);

var metaBuilderMock = new Mock<IMetaBuilder<BenchmarkResource>>();
var linkBuilderMock = new Mock<ILinkBuilder>();
var includeBuilderMock = new Mock<IIncludedResourceObjectBuilder>();
var metaBuilder = new Mock<IMetaBuilder>().Object;
var linkBuilder = new Mock<ILinkBuilder>().Object;
var includeBuilder = new Mock<IIncludedResourceObjectBuilder>().Object;
var accessor = new Mock<IResourceDefinitionAccessor>().Object;

var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, accessor, new ResourceObjectBuilderSettings());

_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilderMock.Object, linkBuilderMock.Object,
includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options);
_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder,
includeBuilder, fieldsToSerialize, resourceObjectBuilder, options);
}

private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)
Expand Down
56 changes: 31 additions & 25 deletions docs/usage/meta.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
# 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 sealed class CopyrightResponseMeta : IResponseMeta
{
public ResponseMetaService(/*...other dependencies here */) {
// ...
}

public Dictionary<string, object> GetMeta()
public IReadOnlyDictionary<string, object> GetMeta()
{
return new Dictionary<string, object>
{
{"copyright", "Copyright 2018 Example Corp."},
{"authors", new string[] {"John Doe"}}
["copyright"] = "Copyright (C) 2002 Umbrella Corporation.",
["authors"] = new[] {"Alice", "Red Queen"}
};
}
}
Expand All @@ -28,14 +24,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": []
}
```

Expand All @@ -50,23 +45,34 @@ public class PersonDefinition : JsonApiResourceDefinition<Person>
{
}

public override IReadOnlyDictionary<string, object> GetMeta()
public override IReadOnlyDictionary<string, object> GetMeta(Person person)
{
return new Dictionary<string, object>
if (person.IsEmployee)
{
["notice"] = "Check our intranet at http://www.example.com for personal details."
};
return new Dictionary<string, object>
{
["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."
}
}
]
}
```

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<TodoItem>
{
public TodoItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
{
}

public override IDictionary<string, object> GetMeta(TodoItem resource)
{
if (resource.Description != null && resource.Description.StartsWith("Important:"))
{
return new Dictionary<string, object>
{
["hasHighPriority"] = true
};
}

return base.GetMeta(resource);
}
}
}
2 changes: 1 addition & 1 deletion src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public override void ConfigureServices(IServiceCollection services)
ConfigureClock(services);

services.AddScoped<SkipCacheQueryStringParameterReader>();
services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<SkipCacheQueryStringParameterReader>());
services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<SkipCacheQueryStringParameterReader>());

services.AddDbContext<AppDbContext>(options =>
{
Expand Down
37 changes: 19 additions & 18 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
_mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder));

_intermediateProvider = services.BuildServiceProvider();
var loggerFactory = _intermediateProvider.GetService<ILoggerFactory>();
var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();

_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
Expand Down Expand Up @@ -161,7 +161,7 @@ private void AddMiddlewareLayer()
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
_services.TryAddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetService<IJsonApiRoutingConvention>());
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_services.AddScoped<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
Expand Down Expand Up @@ -235,21 +235,21 @@ private void AddQueryStringLayer()
_services.AddScoped<INullsQueryStringParameterReader, NullsQueryStringParameterReader>();
_services.AddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();

_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IIncludeQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IFilterQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<ISortQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<ISparseFieldSetQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IPaginationQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IDefaultsQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<INullsQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IResourceDefinitionQueryableParameterReader>());

_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IIncludeQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IFilterQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<ISortQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<ISparseFieldSetQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IPaginationQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IResourceDefinitionQueryableParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IIncludeQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IFilterQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<ISortQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<ISparseFieldSetQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IPaginationQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IDefaultsQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<INullsQueryStringParameterReader>());
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IResourceDefinitionQueryableParameterReader>());

_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IIncludeQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IFilterQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<ISortQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<ISparseFieldSetQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IPaginationQueryStringParameterReader>());
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IResourceDefinitionQueryableParameterReader>());

_services.AddScoped<IQueryStringReader, QueryStringReader>();
_services.AddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
Expand All @@ -271,7 +271,8 @@ private void AddSerializationLayer()
_services.AddScoped<IResourceObjectBuilderSettingsProvider, ResourceObjectBuilderSettingsProvider>();
_services.AddScoped<IJsonApiSerializerFactory, ResponseSerializerFactory>();
_services.AddScoped<ILinkBuilder, LinkBuilder>();
_services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>));
_services.AddScoped<IResponseMeta, EmptyResponseMeta>();
_services.AddScoped<IMetaBuilder, MetaBuilder>();
_services.AddScoped(typeof(ResponseSerializer<>));
_services.AddScoped(sp => sp.GetRequiredService<IJsonApiSerializerFactory>().GetSerializer());
_services.AddScoped<IResourceObjectBuilder, ResponseResourceObjectBuilder>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -70,8 +72,9 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection
services.AddScoped<IResponseDeserializer, ResponseDeserializer>();
services.AddScoped<IRequestSerializer>(sp =>
{
var graph = sp.GetService<IResourceGraph>();
return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings()));
var graph = sp.GetRequiredService<IResourceGraph>();
var accessor = new ResourceDefinitionAccessor(graph, new ServiceContainer());
return new RequestSerializer(graph, new ResourceObjectBuilder(graph, accessor, new ResourceObjectBuilderSettings()));
});
return services;
}
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));

var reader = context.HttpContext.RequestServices.GetService<IJsonApiReader>();
var reader = context.HttpContext.RequestServices.GetRequiredService<IJsonApiReader>();
return await reader.ReadAsync(context);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));

var writer = context.HttpContext.RequestServices.GetService<IJsonApiWriter>();
var writer = context.HttpContext.RequestServices.GetRequiredService<IJsonApiWriter>();
await writer.WriteAsync(context);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Resources/IResourceDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ public interface IResourceDefinition<TResource, TId>
QueryStringParameterHandlers<TResource> OnRegisterQueryableHandlersForQueryStringParameters();

/// <summary>
/// Enables to add json:api meta information, specific to this resource type.
/// Enables to add json:api meta information, specific to this resource.
/// </summary>
IReadOnlyDictionary<string, object> GetMeta();
IDictionary<string, object> GetMeta(TResource resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public interface IResourceDefinitionAccessor
object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName);

/// <summary>
/// Invokes <see cref="IResourceDefinition{TResource,TId}.GetMeta"/> for the specified resource type.
/// Invokes <see cref="IResourceDefinition{TResource,TId}.GetMeta"/> for the specified resource.
/// </summary>
IReadOnlyDictionary<string, object> GetMeta(Type resourceType);
IDictionary<string, object> GetMeta(Type resourceType, IIdentifiable resourceInstance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public virtual QueryStringParameterHandlers<TResource> OnRegisterQueryableHandle
}

/// <inheritdoc />
public virtual IReadOnlyDictionary<string, object> GetMeta()
public virtual IDictionary<string, object> GetMeta(TResource resource)
{
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri
}

/// <inheritdoc />
public IReadOnlyDictionary<string, object> GetMeta(Type resourceType)
public IDictionary<string, object> 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)
Expand Down
10 changes: 6 additions & 4 deletions src/JsonApiDotNetCore/Serialization/BaseSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,34 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder)
/// Adds the attributes and relationships that are enlisted in <paramref name="attributes"/> and <paramref name="relationships"/>.
/// </summary>
/// <param name="resource">Resource to build a <see cref="ResourceObject"/> for.</param>
/// <param name="includeResourceMeta">When <c>true</c>, retrieves resource-specific meta from <see cref="IResourceDefinition{TResource}"/>.</param>
/// <param name="attributes">Attributes to include in the building process.</param>
/// <param name="relationships">Relationships to include in the building process.</param>
/// <returns>The resource object that was built.</returns>
protected Document Build(IIdentifiable resource, IReadOnlyCollection<AttrAttribute> attributes, IReadOnlyCollection<RelationshipAttribute> relationships)
protected Document Build(IIdentifiable resource, bool includeResourceMeta, IReadOnlyCollection<AttrAttribute> attributes, IReadOnlyCollection<RelationshipAttribute> relationships)
{
if (resource == null)
return new Document();

return new Document { Data = ResourceObjectBuilder.Build(resource, attributes, relationships) };
return new Document { Data = ResourceObjectBuilder.Build(resource, includeResourceMeta, attributes, relationships) };
}

/// <summary>
/// Builds a <see cref="Document"/> for <paramref name="resources"/>.
/// Adds the attributes and relationships that are enlisted in <paramref name="attributes"/> and <paramref name="relationships"/>.
/// </summary>
/// <param name="resources">Resource to build a <see cref="ResourceObject"/> for.</param>
/// <param name="includeResourceMeta">When <c>true</c>, retrieves resource-specific meta from <see cref="IResourceDefinition{TResource}"/>.</param>
/// <param name="attributes">Attributes to include in the building process.</param>
/// <param name="relationships">Relationships to include in the building process.</param>
/// <returns>The resource object that was built.</returns>
protected Document Build(IReadOnlyCollection<IIdentifiable> resources, IReadOnlyCollection<AttrAttribute> attributes, IReadOnlyCollection<RelationshipAttribute> relationships)
protected Document Build(IReadOnlyCollection<IIdentifiable> resources, bool includeResourceMeta, IReadOnlyCollection<AttrAttribute> attributes, IReadOnlyCollection<RelationshipAttribute> relationships)
{
if (resources == null) throw new ArgumentNullException(nameof(resources));

var data = new List<ResourceObject>();
foreach (IIdentifiable resource in resources)
data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships));
data.Add(ResourceObjectBuilder.Build(resource, includeResourceMeta, attributes, relationships));

return new Document { Data = data };
}
Expand Down
Loading