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 all 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
10 changes: 5 additions & 5 deletions benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ 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 resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, 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
59 changes: 34 additions & 25 deletions docs/usage/meta.md
Original file line number Diff line number Diff line change
@@ -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<IResponseMeta, CopyrightResponseMeta>();

public Dictionary<string, object> GetMeta()
public sealed class CopyrightResponseMeta : IResponseMeta
{
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 +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": []
}
```

Expand All @@ -50,23 +48,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,7 +72,7 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection
services.AddScoped<IResponseDeserializer, ResponseDeserializer>();
services.AddScoped<IRequestSerializer>(sp =>
{
var graph = sp.GetService<IResourceGraph>();
var graph = sp.GetRequiredService<IResourceGraph>();
return new RequestSerializer(graph, new ResourceObjectBuilder(graph, 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
20 changes: 7 additions & 13 deletions src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
using System.Collections.Generic;
using JsonApiDotNetCore.Resources;

namespace JsonApiDotNetCore.Serialization.Building
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TResource">Associated resource for which to build the meta element.</typeparam>
public interface IMetaBuilder<TResource> where TResource : class, IIdentifiable
public interface IMetaBuilder
{
/// <summary>
/// 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.
/// </summary>
void Add(string key, object value);
/// <summary>
/// Joins the new dictionary with the current one. In the event of a key collision,
/// the new value will overwrite the old one.
/// </summary>
void Add(IReadOnlyDictionary<string,object> values);
void Add(IReadOnlyDictionary<string, object> values);

/// <summary>
/// Builds the top-level meta data object.
/// </summary>
IDictionary<string, object> GetMeta();
IDictionary<string, object> Build();
}
}
Loading