Skip to content

Commit 9584565

Browse files
author
Bart Koelman
authored
Generic resource definitions (#832)
* Generic resource definitions Made resource definitions a first-class pluggable extensibility point, similar to resource services and repositories. So developers now can derive from JsonApiResourceDefinition, implement IResourceDefinition, or insert base classes in the type hierarchy to share common logic. Such a base class can then be registered as an open generic, so that all resources will use that (unless the container finds a non-generic class, which is a better match) instead of the built-in JsonApiResourceDefinition. To accomplish this, I had to split up ResourceDefinition into two: - JsonApiResourceDefinition is highly pluggable like described above - ResourceHooksDefinition contains just the hook callbacks This way, the recursive resolve logic for resource hooks remains as-is. Since it did not use IResourceDefinitionProvider, that type has been replaced with IResourceDefinitionAccessor, which invokes the requested callbacks directly on the found class. What used to be IResourceDefinition (an intermediate type to invoke callbacks on) no longer exists. * Only scan for hooks when enabled in options Replaced IHasMeta with IResourceDefinition method * Fix broken cibuild * Review feedback * Made ResourceDefinitionAccessor.GetResourceDefinition protected * Renamed IRequestMeta to IResponseMeta; updated documentation * quotes * Review feedback
1 parent ca717d0 commit 9584565

File tree

65 files changed

+785
-608
lines changed

Some content is hidden

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

65 files changed

+785
-608
lines changed

benchmarks/DependencyFactory.cs

-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
using System;
21
using JsonApiDotNetCore.Configuration;
3-
using JsonApiDotNetCore.Resources;
42
using Microsoft.Extensions.Logging.Abstractions;
5-
using Moq;
63

74
namespace Benchmarks
85
{
@@ -14,15 +11,5 @@ public static IResourceGraph CreateResourceGraph(IJsonApiOptions options)
1411
builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
1512
return builder.Build();
1613
}
17-
18-
public static IResourceDefinitionProvider CreateResourceDefinitionProvider(IResourceGraph resourceGraph)
19-
{
20-
var resourceDefinition = new ResourceDefinition<BenchmarkResource>(resourceGraph);
21-
22-
var resourceDefinitionProviderMock = new Mock<IResourceDefinitionProvider>();
23-
resourceDefinitionProviderMock.Setup(provider => provider.Get(It.IsAny<Type>())).Returns(resourceDefinition);
24-
25-
return resourceDefinitionProviderMock.Object;
26-
}
2714
}
2815
}

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Middleware;
55
using JsonApiDotNetCore.Queries;
66
using JsonApiDotNetCore.QueryStrings.Internal;
7+
using JsonApiDotNetCore.Resources;
78
using JsonApiDotNetCore.Serialization;
89
using JsonApiDotNetCore.Serialization.Building;
910
using Moq;
@@ -46,9 +47,9 @@ private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resource
4647
new SparseFieldSetQueryStringParameterReader(request, resourceGraph)
4748
};
4849

49-
var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
50+
var accessor = new Mock<IResourceDefinitionAccessor>().Object;
5051

51-
return new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionProvider);
52+
return new FieldsToSerialize(resourceGraph, constraintProviders, accessor);
5253
}
5354

5455
[Benchmark]

docs/usage/meta.md

+32-18
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,60 @@
11
# Metadata
22

3-
Non-standard metadata can be added to your API responses in two ways: Resource and Request Meta. In the event of a key collision, the Request Meta will take precendence.
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.
44

5-
## Resource Meta
5+
## Global Meta
66

7-
Resource Meta is metadata defined on the resource itself by implementing the `IHasMeta` interface.
7+
Global metadata can be added by registering a service that implements `IResponseMeta`.
8+
This is useful if you need access to other registered services to build the meta object.
89

910
```c#
10-
public class Person : Identifiable, IHasMeta
11+
public class ResponseMetaService : IResponseMeta
1112
{
13+
public ResponseMetaService(/*...other dependencies here */) {
14+
// ...
15+
}
16+
1217
public Dictionary<string, object> GetMeta()
1318
{
1419
return new Dictionary<string, object>
1520
{
1621
{"copyright", "Copyright 2018 Example Corp."},
17-
{"authors", new[] {"John Doe"}}
22+
{"authors", new string[] {"John Doe"}}
1823
};
1924
}
2025
}
2126
```
2227

23-
## Request Meta
28+
```json
29+
{
30+
"meta": {
31+
"copyright": "Copyright 2018 Example Corp.",
32+
"authors": [
33+
"John Doe"
34+
]
35+
},
36+
"data": {
37+
// ...
38+
}
39+
}
40+
```
2441

25-
Request Meta can be added by injecting a service that implements `IRequestMeta`.
26-
This is useful if you need access to other injected services to build the meta object.
42+
## Resource Meta
43+
44+
Resource-specific metadata can be added by implementing `IResourceDefinition<TResource, TId>.GetMeta` (or overriding it on `JsonApiResourceDefinition`):
2745

2846
```c#
29-
public class RequestMetaService : IRequestMeta
47+
public class PersonDefinition : JsonApiResourceDefinition<Person>
3048
{
31-
public RequestMetaService(/*...other dependencies here */) {
32-
// ...
49+
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
50+
{
3351
}
3452

35-
public Dictionary<string, object> GetMeta()
53+
public override IReadOnlyDictionary<string, object> GetMeta()
3654
{
3755
return new Dictionary<string, object>
3856
{
39-
{"copyright", "Copyright 2018 Example Corp."},
40-
{"authors", new string[] {"John Doe"}}
57+
["notice"] = "Check our intranet at http://www.example.com for personal details."
4158
};
4259
}
4360
}
@@ -46,10 +63,7 @@ public class RequestMetaService : IRequestMeta
4663
```json
4764
{
4865
"meta": {
49-
"copyright": "Copyright 2018 Example Corp.",
50-
"authors": [
51-
"John Doe"
52-
]
66+
"notice": "Check our intranet at http://www.example.com for personal details."
5367
},
5468
"data": {
5569
// ...

src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs renamed to src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111
namespace JsonApiDotNetCoreExample.Definitions
1212
{
13-
public class ArticleDefinition : ResourceDefinition<Article>
13+
public class ArticleHooksDefinition : ResourceHooksDefinition<Article>
1414
{
15-
public ArticleDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
15+
public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
1616

1717
public override IEnumerable<Article> OnReturn(HashSet<Article> resources, ResourcePipeline pipeline)
1818
{

src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs renamed to src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
namespace JsonApiDotNetCoreExample.Definitions
1111
{
12-
public abstract class LockableDefinition<T> : ResourceDefinition<T> where T : class, IIsLockable, IIdentifiable
12+
public abstract class LockableHooksDefinition<T> : ResourceHooksDefinition<T> where T : class, IIsLockable, IIdentifiable
1313
{
14-
protected LockableDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
14+
protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
1515

1616
protected void DisallowLocked(IEnumerable<T> resources)
1717
{

src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs renamed to src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010

1111
namespace JsonApiDotNetCoreExample.Definitions
1212
{
13-
public class PassportDefinition : ResourceDefinition<Passport>
13+
public class PassportHooksDefinition : ResourceHooksDefinition<Passport>
1414
{
15-
public PassportDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
15+
public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
1616
{
1717
}
1818

src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs

+14-17
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
11
using System.Collections.Generic;
2-
using System.Linq;
32
using JsonApiDotNetCore.Configuration;
4-
using JsonApiDotNetCore.Hooks.Internal.Execution;
53
using JsonApiDotNetCore.Resources;
64
using JsonApiDotNetCoreExample.Models;
75

86
namespace JsonApiDotNetCoreExample.Definitions
97
{
10-
public class PersonDefinition : LockableDefinition<Person>, IHasMeta
8+
public class PersonDefinition : JsonApiResourceDefinition<Person>
119
{
12-
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
13-
14-
public override IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids, IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
15-
{
16-
BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
17-
return ids;
18-
}
19-
20-
public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
10+
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
2111
{
22-
resourcesByRelationship.GetByRelationship<Passport>().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
2312
}
2413

25-
public IReadOnlyDictionary<string, object> GetMeta()
14+
public override IReadOnlyDictionary<string, object> GetMeta()
2615
{
27-
return new Dictionary<string, object> {
28-
{ "copyright", "Copyright 2015 Example Corp." },
29-
{ "authors", new[] { "Jared Nance", "Maurits Moeys", "Harro van der Kroft" } }
16+
return new Dictionary<string, object>
17+
{
18+
["license"] = "MIT",
19+
["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/",
20+
["versions"] = new[]
21+
{
22+
"v4.0.0",
23+
"v3.1.0",
24+
"v2.5.2",
25+
"v1.3.1"
26+
}
3027
};
3128
}
3229
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Hooks.Internal.Execution;
5+
using JsonApiDotNetCoreExample.Models;
6+
7+
namespace JsonApiDotNetCoreExample.Definitions
8+
{
9+
public class PersonHooksDefinition : LockableHooksDefinition<Person>
10+
{
11+
public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
12+
13+
public override IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids, IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
14+
{
15+
BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
16+
return ids;
17+
}
18+
19+
public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
20+
{
21+
resourcesByRelationship.GetByRelationship<Passport>().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
22+
}
23+
}
24+
}

src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs renamed to src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
namespace JsonApiDotNetCoreExample.Definitions
99
{
10-
public class TagDefinition : ResourceDefinition<Tag>
10+
public class TagHooksDefinition : ResourceHooksDefinition<Tag>
1111
{
12-
public TagDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
12+
public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
1313

1414
public override IEnumerable<Tag> BeforeCreate(IResourceHashSet<Tag> affected, ResourcePipeline pipeline)
1515
{

src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs renamed to src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
namespace JsonApiDotNetCoreExample.Definitions
1111
{
12-
public class TodoDefinition : LockableDefinition<TodoItem>
12+
public class TodoHooksDefinition : LockableHooksDefinition<TodoItem>
1313
{
14-
public TodoDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
14+
public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
1515

1616
public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null)
1717
{

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+27-18
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
4646
var loggerFactory = _intermediateProvider.GetService<ILoggerFactory>();
4747

4848
_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
49-
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
49+
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
5050
}
5151

5252
/// <summary>
@@ -114,7 +114,7 @@ public void DiscoverInjectables()
114114
/// <summary>
115115
/// Registers the remaining internals.
116116
/// </summary>
117-
public void ConfigureServices(Type dbContextType)
117+
public void ConfigureServiceContainer(Type dbContextType)
118118
{
119119
if (dbContextType != null)
120120
{
@@ -127,28 +127,23 @@ public void ConfigureServices(Type dbContextType)
127127
_services.AddSingleton(new DbContextOptionsBuilder().Options);
128128
}
129129

130+
AddResourceLayer();
130131
AddRepositoryLayer();
131132
AddServiceLayer();
132133
AddMiddlewareLayer();
134+
AddSerializationLayer();
135+
AddQueryStringLayer();
133136

134-
_services.AddSingleton<IResourceContextProvider>(sp => sp.GetRequiredService<IResourceGraph>());
135-
136-
_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
137-
_services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>));
138-
_services.AddScoped<IResourceDefinitionProvider, ResourceDefinitionProvider>();
139-
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
140-
_services.AddScoped<IResourceFactory, ResourceFactory>();
141-
_services.AddScoped<IPaginationContext, PaginationContext>();
142-
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
143-
144-
AddServerSerialization();
145-
AddQueryStringParameterServices();
146-
147137
if (_options.EnableResourceHooks)
148138
{
149139
AddResourceHooks();
150140
}
151141

142+
_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
143+
_services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>));
144+
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
145+
_services.AddScoped<IPaginationContext, PaginationContext>();
146+
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
152147
_services.TryAddScoped<IInverseRelationships, InverseRelationships>();
153148
}
154149

@@ -173,6 +168,17 @@ private void AddMiddlewareLayer()
173168
_services.AddScoped<IFieldsToSerialize, FieldsToSerialize>();
174169
}
175170

171+
private void AddResourceLayer()
172+
{
173+
_services.AddScoped(typeof(IResourceDefinition<>), typeof(JsonApiResourceDefinition<>));
174+
_services.AddScoped(typeof(IResourceDefinition<,>), typeof(JsonApiResourceDefinition<,>));
175+
_services.AddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();
176+
177+
_services.AddScoped<IResourceFactory, ResourceFactory>();
178+
179+
_services.AddSingleton<IResourceContextProvider>(sp => sp.GetRequiredService<IResourceGraph>());
180+
}
181+
176182
private void AddRepositoryLayer()
177183
{
178184
_services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>));
@@ -208,11 +214,14 @@ private void AddServiceLayer()
208214
_services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>));
209215
_services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>));
210216

217+
_services.AddScoped(typeof(IResourceQueryService<>), typeof(JsonApiResourceService<>));
211218
_services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>));
219+
220+
_services.AddScoped(typeof(IResourceCommandService<>), typeof(JsonApiResourceService<>));
212221
_services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>));
213222
}
214223

215-
private void AddQueryStringParameterServices()
224+
private void AddQueryStringLayer()
216225
{
217226
_services.AddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
218227
_services.AddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
@@ -246,13 +255,13 @@ private void AddQueryStringParameterServices()
246255
private void AddResourceHooks()
247256
{
248257
_services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>));
249-
_services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>));
258+
_services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceHooksDefinition<>));
250259
_services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor));
251260
_services.AddTransient<IHookExecutorHelper, HookExecutorHelper>();
252261
_services.AddTransient<ITraversalHelper, TraversalHelper>();
253262
}
254263

255-
private void AddServerSerialization()
264+
private void AddSerializationLayer()
256265
{
257266
_services.AddScoped<IIncludedResourceObjectBuilder, IncludedResourceObjectBuilder>();
258267
_services.AddScoped<IJsonApiDeserializer, RequestDeserializer>();

src/JsonApiDotNetCore/Configuration/ResourceContext.cs

-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using JsonApiDotNetCore.Resources;
54
using JsonApiDotNetCore.Resources.Annotations;
65

76
namespace JsonApiDotNetCore.Configuration
@@ -26,12 +25,6 @@ public class ResourceContext
2625
/// </summary>
2726
public Type IdentityType { get; set; }
2827

29-
/// <summary>
30-
/// The concrete <see cref="ResourceDefinition{TResource}"/> type.
31-
/// We store this so that we don't need to re-compute the generic type.
32-
/// </summary>
33-
public Type ResourceDefinitionType { get; set; }
34-
3528
/// <summary>
3629
/// Exposed resource attributes.
3730
/// See https://jsonapi.org/format/#document-resource-object-attributes.

0 commit comments

Comments
 (0)