Skip to content

Commit bb1ad7a

Browse files
author
Bart Koelman
authored
Resource-specific meta (#845)
* Replaced calls to serviceProvider.GetService with GetRequiredService where possible. * Fixed invalid test name * Improved top-level meta test * Added resource-specific meta; converted tests * Post-merge fixes * Review: use overridden ResourceObjectBuilder.Build method instead of passing a boolean flag * added registration to docs
1 parent 262d0f2 commit bb1ad7a

File tree

56 files changed

+601
-527
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+601
-527
lines changed

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ public JsonApiSerializerBenchmarks()
2828
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
2929
IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph);
3030

31-
var metaBuilderMock = new Mock<IMetaBuilder<BenchmarkResource>>();
32-
var linkBuilderMock = new Mock<ILinkBuilder>();
33-
var includeBuilderMock = new Mock<IIncludedResourceObjectBuilder>();
31+
var metaBuilder = new Mock<IMetaBuilder>().Object;
32+
var linkBuilder = new Mock<ILinkBuilder>().Object;
33+
var includeBuilder = new Mock<IIncludedResourceObjectBuilder>().Object;
3434

3535
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());
3636

37-
_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilderMock.Object, linkBuilderMock.Object,
38-
includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options);
37+
_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder,
38+
includeBuilder, fieldsToSerialize, resourceObjectBuilder, options);
3939
}
4040

4141
private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)

docs/usage/meta.md

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
# Metadata
22

3-
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.
3+
We support two ways to add json:api meta to your responses: global and per resource.
44

55
## Global Meta
66

7-
Global metadata can be added by registering a service that implements `IResponseMeta`.
7+
Global metadata can be added to the root of the response document by registering a service that implements `IResponseMeta`.
88
This is useful if you need access to other registered services to build the meta object.
99

1010
```c#
11-
public class ResponseMetaService : IResponseMeta
12-
{
13-
public ResponseMetaService(/*...other dependencies here */) {
14-
// ...
15-
}
11+
// In Startup.ConfigureServices
12+
services.AddSingleton<IResponseMeta, CopyrightResponseMeta>();
1613

17-
public Dictionary<string, object> GetMeta()
14+
public sealed class CopyrightResponseMeta : IResponseMeta
15+
{
16+
public IReadOnlyDictionary<string, object> GetMeta()
1817
{
1918
return new Dictionary<string, object>
2019
{
21-
{"copyright", "Copyright 2018 Example Corp."},
22-
{"authors", new string[] {"John Doe"}}
20+
["copyright"] = "Copyright (C) 2002 Umbrella Corporation.",
21+
["authors"] = new[] {"Alice", "Red Queen"}
2322
};
2423
}
2524
}
@@ -28,14 +27,13 @@ public class ResponseMetaService : IResponseMeta
2827
```json
2928
{
3029
"meta": {
31-
"copyright": "Copyright 2018 Example Corp.",
30+
"copyright": "Copyright (C) 2002 Umbrella Corporation.",
3231
"authors": [
33-
"John Doe"
32+
"Alice",
33+
"Red Queen"
3434
]
3535
},
36-
"data": {
37-
// ...
38-
}
36+
"data": []
3937
}
4038
```
4139

@@ -50,23 +48,34 @@ public class PersonDefinition : JsonApiResourceDefinition<Person>
5048
{
5149
}
5250

53-
public override IReadOnlyDictionary<string, object> GetMeta()
51+
public override IReadOnlyDictionary<string, object> GetMeta(Person person)
5452
{
55-
return new Dictionary<string, object>
53+
if (person.IsEmployee)
5654
{
57-
["notice"] = "Check our intranet at http://www.example.com for personal details."
58-
};
55+
return new Dictionary<string, object>
56+
{
57+
["notice"] = "Check our intranet at http://www.example.com/employees/" + person.StringId + " for personal details."
58+
};
59+
}
60+
61+
return null;
5962
}
6063
}
6164
```
6265

6366
```json
6467
{
65-
"meta": {
66-
"notice": "Check our intranet at http://www.example.com for personal details."
67-
},
68-
"data": {
69-
// ...
70-
}
68+
"data": [
69+
{
70+
"type": "people",
71+
"id": "1",
72+
"attributes": {
73+
...
74+
},
75+
"meta": {
76+
"notice": "Check our intranet at http://www.example.com/employees/1 for personal details."
77+
}
78+
}
79+
]
7180
}
7281
```

src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCoreExample.Models;
5+
6+
namespace JsonApiDotNetCoreExample.Definitions
7+
{
8+
public sealed class TodoItemDefinition : JsonApiResourceDefinition<TodoItem>
9+
{
10+
public TodoItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
11+
{
12+
}
13+
14+
public override IDictionary<string, object> GetMeta(TodoItem resource)
15+
{
16+
if (resource.Description != null && resource.Description.StartsWith("Important:"))
17+
{
18+
return new Dictionary<string, object>
19+
{
20+
["hasHighPriority"] = true
21+
};
22+
}
23+
24+
return base.GetMeta(resource);
25+
}
26+
}
27+
}

src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public override void ConfigureServices(IServiceCollection services)
2929
ConfigureClock(services);
3030

3131
services.AddScoped<SkipCacheQueryStringParameterReader>();
32-
services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<SkipCacheQueryStringParameterReader>());
32+
services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<SkipCacheQueryStringParameterReader>());
3333

3434
services.AddDbContext<AppDbContext>(options =>
3535
{

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
4545
_mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder));
4646

4747
_intermediateProvider = services.BuildServiceProvider();
48-
var loggerFactory = _intermediateProvider.GetService<ILoggerFactory>();
48+
var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();
4949

5050
_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
5151
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
@@ -161,7 +161,7 @@ private void AddMiddlewareLayer()
161161
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
162162
_services.TryAddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
163163
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
164-
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetService<IJsonApiRoutingConvention>());
164+
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
165165
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
166166
_services.AddScoped<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
167167
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
@@ -235,21 +235,21 @@ private void AddQueryStringLayer()
235235
_services.AddScoped<INullsQueryStringParameterReader, NullsQueryStringParameterReader>();
236236
_services.AddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();
237237

238-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IIncludeQueryStringParameterReader>());
239-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IFilterQueryStringParameterReader>());
240-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<ISortQueryStringParameterReader>());
241-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<ISparseFieldSetQueryStringParameterReader>());
242-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IPaginationQueryStringParameterReader>());
243-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IDefaultsQueryStringParameterReader>());
244-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<INullsQueryStringParameterReader>());
245-
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<IResourceDefinitionQueryableParameterReader>());
246-
247-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IIncludeQueryStringParameterReader>());
248-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IFilterQueryStringParameterReader>());
249-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<ISortQueryStringParameterReader>());
250-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<ISparseFieldSetQueryStringParameterReader>());
251-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IPaginationQueryStringParameterReader>());
252-
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetService<IResourceDefinitionQueryableParameterReader>());
238+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IIncludeQueryStringParameterReader>());
239+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IFilterQueryStringParameterReader>());
240+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<ISortQueryStringParameterReader>());
241+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<ISparseFieldSetQueryStringParameterReader>());
242+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IPaginationQueryStringParameterReader>());
243+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IDefaultsQueryStringParameterReader>());
244+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<INullsQueryStringParameterReader>());
245+
_services.AddScoped<IQueryStringParameterReader>(sp => sp.GetRequiredService<IResourceDefinitionQueryableParameterReader>());
246+
247+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IIncludeQueryStringParameterReader>());
248+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IFilterQueryStringParameterReader>());
249+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<ISortQueryStringParameterReader>());
250+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<ISparseFieldSetQueryStringParameterReader>());
251+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IPaginationQueryStringParameterReader>());
252+
_services.AddScoped<IQueryConstraintProvider>(sp => sp.GetRequiredService<IResourceDefinitionQueryableParameterReader>());
253253

254254
_services.AddScoped<IQueryStringReader, QueryStringReader>();
255255
_services.AddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
@@ -271,7 +271,8 @@ private void AddSerializationLayer()
271271
_services.AddScoped<IResourceObjectBuilderSettingsProvider, ResourceObjectBuilderSettingsProvider>();
272272
_services.AddScoped<IJsonApiSerializerFactory, ResponseSerializerFactory>();
273273
_services.AddScoped<ILinkBuilder, LinkBuilder>();
274-
_services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>));
274+
_services.AddScoped<IResponseMeta, EmptyResponseMeta>();
275+
_services.AddScoped<IMetaBuilder, MetaBuilder>();
275276
_services.AddScoped(typeof(ResponseSerializer<>));
276277
_services.AddScoped(sp => sp.GetRequiredService<IJsonApiSerializerFactory>().GetSerializer());
277278
_services.AddScoped<IResourceObjectBuilder, ResponseResourceObjectBuilder>();

src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel.Design;
34
using System.Linq;
45
using System.Reflection;
56
using JsonApiDotNetCore.Errors;
7+
using JsonApiDotNetCore.Resources;
68
using JsonApiDotNetCore.Serialization.Building;
79
using JsonApiDotNetCore.Serialization.Client.Internal;
810
using JsonApiDotNetCore.Services;
@@ -70,7 +72,7 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection
7072
services.AddScoped<IResponseDeserializer, ResponseDeserializer>();
7173
services.AddScoped<IRequestSerializer>(sp =>
7274
{
73-
var graph = sp.GetService<IResourceGraph>();
75+
var graph = sp.GetRequiredService<IResourceGraph>();
7476
return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings()));
7577
});
7678
return services;

src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
2222
{
2323
if (context == null) throw new ArgumentNullException(nameof(context));
2424

25-
var reader = context.HttpContext.RequestServices.GetService<IJsonApiReader>();
25+
var reader = context.HttpContext.RequestServices.GetRequiredService<IJsonApiReader>();
2626
return await reader.ReadAsync(context);
2727
}
2828
}

src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
2222
{
2323
if (context == null) throw new ArgumentNullException(nameof(context));
2424

25-
var writer = context.HttpContext.RequestServices.GetService<IJsonApiWriter>();
25+
var writer = context.HttpContext.RequestServices.GetRequiredService<IJsonApiWriter>();
2626
await writer.WriteAsync(context);
2727
}
2828
}

src/JsonApiDotNetCore/Resources/IResourceDefinition.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public interface IResourceDefinition<TResource, TId>
115115
QueryStringParameterHandlers<TResource> OnRegisterQueryableHandlersForQueryStringParameters();
116116

117117
/// <summary>
118-
/// Enables to add json:api meta information, specific to this resource type.
118+
/// Enables to add json:api meta information, specific to this resource.
119119
/// </summary>
120-
IReadOnlyDictionary<string, object> GetMeta();
120+
IDictionary<string, object> GetMeta(TResource resource);
121121
}
122122
}

src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public interface IResourceDefinitionAccessor
4242
object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName);
4343

4444
/// <summary>
45-
/// Invokes <see cref="IResourceDefinition{TResource,TId}.GetMeta"/> for the specified resource type.
45+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.GetMeta"/> for the specified resource.
4646
/// </summary>
47-
IReadOnlyDictionary<string, object> GetMeta(Type resourceType);
47+
IDictionary<string, object> GetMeta(Type resourceType, IIdentifiable resourceInstance);
4848
}
4949
}

src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public virtual QueryStringParameterHandlers<TResource> OnRegisterQueryableHandle
103103
}
104104

105105
/// <inheritdoc />
106-
public virtual IReadOnlyDictionary<string, object> GetMeta()
106+
public virtual IDictionary<string, object> GetMeta(TResource resource)
107107
{
108108
return null;
109109
}

src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri
7676
}
7777

7878
/// <inheritdoc />
79-
public IReadOnlyDictionary<string, object> GetMeta(Type resourceType)
79+
public IDictionary<string, object> GetMeta(Type resourceType, IIdentifiable resourceInstance)
8080
{
8181
if (resourceType == null) throw new ArgumentNullException(nameof(resourceType));
8282

8383
dynamic resourceDefinition = GetResourceDefinition(resourceType);
84-
return resourceDefinition.GetMeta();
84+
return resourceDefinition.GetMeta((dynamic) resourceInstance);
8585
}
8686

8787
protected object GetResourceDefinition(Type resourceType)
Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
using System.Collections.Generic;
2-
using JsonApiDotNetCore.Resources;
32

43
namespace JsonApiDotNetCore.Serialization.Building
54
{
65
/// <summary>
7-
/// Builds the top-level meta object. This builder is generic to allow for
8-
/// different top-level meta objects depending on the associated resource of the request.
6+
/// Builds the top-level meta object.
97
/// </summary>
10-
/// <typeparam name="TResource">Associated resource for which to build the meta element.</typeparam>
11-
public interface IMetaBuilder<TResource> where TResource : class, IIdentifiable
8+
public interface IMetaBuilder
129
{
1310
/// <summary>
14-
/// Adds a key-value pair to the top-level meta object.
11+
/// Merges the specified dictionary with existing key/value pairs. In the event of a key collision,
12+
/// the value from the specified dictionary will overwrite the existing one.
1513
/// </summary>
16-
void Add(string key, object value);
17-
/// <summary>
18-
/// Joins the new dictionary with the current one. In the event of a key collision,
19-
/// the new value will overwrite the old one.
20-
/// </summary>
21-
void Add(IReadOnlyDictionary<string,object> values);
14+
void Add(IReadOnlyDictionary<string, object> values);
15+
2216
/// <summary>
2317
/// Builds the top-level meta data object.
2418
/// </summary>
25-
IDictionary<string, object> GetMeta();
19+
IDictionary<string, object> Build();
2620
}
2721
}

0 commit comments

Comments
 (0)