Skip to content

Generic resource definitions #832

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
Sep 18, 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
13 changes: 0 additions & 13 deletions benchmarks/DependencyFactory.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using System;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;

namespace Benchmarks
{
Expand All @@ -14,15 +11,5 @@ public static IResourceGraph CreateResourceGraph(IJsonApiOptions options)
builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
return builder.Build();
}

public static IResourceDefinitionProvider CreateResourceDefinitionProvider(IResourceGraph resourceGraph)
{
var resourceDefinition = new ResourceDefinition<BenchmarkResource>(resourceGraph);

var resourceDefinitionProviderMock = new Mock<IResourceDefinitionProvider>();
resourceDefinitionProviderMock.Setup(provider => provider.Get(It.IsAny<Type>())).Returns(resourceDefinition);

return resourceDefinitionProviderMock.Object;
}
}
}
5 changes: 3 additions & 2 deletions benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.QueryStrings.Internal;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Building;
using Moq;
Expand Down Expand Up @@ -46,9 +47,9 @@ private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resource
new SparseFieldSetQueryStringParameterReader(request, resourceGraph)
};

var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
var accessor = new Mock<IResourceDefinitionAccessor>().Object;

return new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionProvider);
return new FieldsToSerialize(resourceGraph, constraintProviders, accessor);
}

[Benchmark]
Expand Down
50 changes: 32 additions & 18 deletions docs/usage/meta.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,60 @@
# Metadata

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.
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.

## Resource Meta
## Global Meta

Resource Meta is metadata defined on the resource itself by implementing the `IHasMeta` interface.
Global metadata can be added 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 Person : Identifiable, IHasMeta
public class ResponseMetaService : IResponseMeta
{
public ResponseMetaService(/*...other dependencies here */) {
// ...
}

public Dictionary<string, object> GetMeta()
{
return new Dictionary<string, object>
{
{"copyright", "Copyright 2018 Example Corp."},
{"authors", new[] {"John Doe"}}
{"authors", new string[] {"John Doe"}}
};
}
}
```

## Request Meta
```json
{
"meta": {
"copyright": "Copyright 2018 Example Corp.",
"authors": [
"John Doe"
]
},
"data": {
// ...
}
}
```

Request Meta can be added by injecting a service that implements `IRequestMeta`.
This is useful if you need access to other injected services to build the meta object.
## Resource Meta

Resource-specific metadata can be added by implementing `IResourceDefinition<TResource, TId>.GetMeta` (or overriding it on `JsonApiResourceDefinition`):

```c#
public class RequestMetaService : IRequestMeta
public class PersonDefinition : JsonApiResourceDefinition<Person>
{
public RequestMetaService(/*...other dependencies here */) {
// ...
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
{
}

public Dictionary<string, object> GetMeta()
public override IReadOnlyDictionary<string, object> GetMeta()
{
return new Dictionary<string, object>
{
{"copyright", "Copyright 2018 Example Corp."},
{"authors", new string[] {"John Doe"}}
["notice"] = "Check our intranet at http://www.example.com for personal details."
};
}
}
Expand All @@ -46,10 +63,7 @@ public class RequestMetaService : IRequestMeta
```json
{
"meta": {
"copyright": "Copyright 2018 Example Corp.",
"authors": [
"John Doe"
]
"notice": "Check our intranet at http://www.example.com for personal details."
},
"data": {
// ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

namespace JsonApiDotNetCoreExample.Definitions
{
public class ArticleDefinition : ResourceDefinition<Article>
public class ArticleHooksDefinition : ResourceHooksDefinition<Article>
{
public ArticleDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

public override IEnumerable<Article> OnReturn(HashSet<Article> resources, ResourcePipeline pipeline)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

namespace JsonApiDotNetCoreExample.Definitions
{
public abstract class LockableDefinition<T> : ResourceDefinition<T> where T : class, IIsLockable, IIdentifiable
public abstract class LockableHooksDefinition<T> : ResourceHooksDefinition<T> where T : class, IIsLockable, IIdentifiable
{
protected LockableDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

protected void DisallowLocked(IEnumerable<T> resources)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

namespace JsonApiDotNetCoreExample.Definitions
{
public class PassportDefinition : ResourceDefinition<Passport>
public class PassportHooksDefinition : ResourceHooksDefinition<Passport>
{
public PassportDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Hooks.Internal.Execution;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCoreExample.Models;

namespace JsonApiDotNetCoreExample.Definitions
{
public class PersonDefinition : LockableDefinition<Person>, IHasMeta
public class PersonDefinition : JsonApiResourceDefinition<Person>
{
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

public override IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids, IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
{
BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
return ids;
}

public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
{
resourcesByRelationship.GetByRelationship<Passport>().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
}

public IReadOnlyDictionary<string, object> GetMeta()
public override IReadOnlyDictionary<string, object> GetMeta()
{
return new Dictionary<string, object> {
{ "copyright", "Copyright 2015 Example Corp." },
{ "authors", new[] { "Jared Nance", "Maurits Moeys", "Harro van der Kroft" } }
return new Dictionary<string, object>
{
["license"] = "MIT",
["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/",
["versions"] = new[]
{
"v4.0.0",
"v3.1.0",
"v2.5.2",
"v1.3.1"
}
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Hooks.Internal.Execution;
using JsonApiDotNetCoreExample.Models;

namespace JsonApiDotNetCoreExample.Definitions
{
public class PersonHooksDefinition : LockableHooksDefinition<Person>
{
public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

public override IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids, IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
{
BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
return ids;
}

public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary<Person> resourcesByRelationship, ResourcePipeline pipeline)
{
resourcesByRelationship.GetByRelationship<Passport>().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

namespace JsonApiDotNetCoreExample.Definitions
{
public class TagDefinition : ResourceDefinition<Tag>
public class TagHooksDefinition : ResourceHooksDefinition<Tag>
{
public TagDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

public override IEnumerable<Tag> BeforeCreate(IResourceHashSet<Tag> affected, ResourcePipeline pipeline)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

namespace JsonApiDotNetCoreExample.Definitions
{
public class TodoDefinition : LockableDefinition<TodoItem>
public class TodoHooksDefinition : LockableHooksDefinition<TodoItem>
{
public TodoDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }

public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null)
{
Expand Down
45 changes: 27 additions & 18 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv
var loggerFactory = _intermediateProvider.GetService<ILoggerFactory>();

_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory);
}

/// <summary>
Expand Down Expand Up @@ -114,7 +114,7 @@ public void DiscoverInjectables()
/// <summary>
/// Registers the remaining internals.
/// </summary>
public void ConfigureServices(Type dbContextType)
public void ConfigureServiceContainer(Type dbContextType)
{
if (dbContextType != null)
{
Expand All @@ -127,28 +127,23 @@ public void ConfigureServices(Type dbContextType)
_services.AddSingleton(new DbContextOptionsBuilder().Options);
}

AddResourceLayer();
AddRepositoryLayer();
AddServiceLayer();
AddMiddlewareLayer();
AddSerializationLayer();
AddQueryStringLayer();

_services.AddSingleton<IResourceContextProvider>(sp => sp.GetRequiredService<IResourceGraph>());

_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
_services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>));
_services.AddScoped<IResourceDefinitionProvider, ResourceDefinitionProvider>();
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
_services.AddScoped<IResourceFactory, ResourceFactory>();
_services.AddScoped<IPaginationContext, PaginationContext>();
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();

AddServerSerialization();
AddQueryStringParameterServices();

if (_options.EnableResourceHooks)
{
AddResourceHooks();
}

_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
_services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>));
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
_services.AddScoped<IPaginationContext, PaginationContext>();
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
_services.TryAddScoped<IInverseRelationships, InverseRelationships>();
}

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

private void AddResourceLayer()
{
_services.AddScoped(typeof(IResourceDefinition<>), typeof(JsonApiResourceDefinition<>));
_services.AddScoped(typeof(IResourceDefinition<,>), typeof(JsonApiResourceDefinition<,>));
_services.AddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();

_services.AddScoped<IResourceFactory, ResourceFactory>();

_services.AddSingleton<IResourceContextProvider>(sp => sp.GetRequiredService<IResourceGraph>());
}

private void AddRepositoryLayer()
{
_services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>));
Expand Down Expand Up @@ -208,11 +214,14 @@ private void AddServiceLayer()
_services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>));
_services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>));

_services.AddScoped(typeof(IResourceQueryService<>), typeof(JsonApiResourceService<>));
_services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>));

_services.AddScoped(typeof(IResourceCommandService<>), typeof(JsonApiResourceService<>));
_services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>));
}

private void AddQueryStringParameterServices()
private void AddQueryStringLayer()
{
_services.AddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
_services.AddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
Expand Down Expand Up @@ -246,13 +255,13 @@ private void AddQueryStringParameterServices()
private void AddResourceHooks()
{
_services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>));
_services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>));
_services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceHooksDefinition<>));
_services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor));
_services.AddTransient<IHookExecutorHelper, HookExecutorHelper>();
_services.AddTransient<ITraversalHelper, TraversalHelper>();
}

private void AddServerSerialization()
private void AddSerializationLayer()
{
_services.AddScoped<IIncludedResourceObjectBuilder, IncludedResourceObjectBuilder>();
_services.AddScoped<IJsonApiDeserializer, RequestDeserializer>();
Expand Down
7 changes: 0 additions & 7 deletions src/JsonApiDotNetCore/Configuration/ResourceContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

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

/// <summary>
/// The concrete <see cref="ResourceDefinition{TResource}"/> type.
/// We store this so that we don't need to re-compute the generic type.
/// </summary>
public Type ResourceDefinitionType { get; set; }

/// <summary>
/// Exposed resource attributes.
/// See https://jsonapi.org/format/#document-resource-object-attributes.
Expand Down
Loading