diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 8edd9c9b28..62aee6f301 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -1,8 +1,5 @@ -using System; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Logging.Abstractions; -using Moq; namespace Benchmarks { @@ -14,15 +11,5 @@ public static IResourceGraph CreateResourceGraph(IJsonApiOptions options) builder.Add(BenchmarkResourcePublicNames.Type); return builder.Build(); } - - public static IResourceDefinitionProvider CreateResourceDefinitionProvider(IResourceGraph resourceGraph) - { - var resourceDefinition = new ResourceDefinition(resourceGraph); - - var resourceDefinitionProviderMock = new Mock(); - resourceDefinitionProviderMock.Setup(provider => provider.Get(It.IsAny())).Returns(resourceDefinition); - - return resourceDefinitionProviderMock.Object; - } } } diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index 7f8b961548..bd11dbbbd7 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -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; @@ -46,9 +47,9 @@ private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resource new SparseFieldSetQueryStringParameterReader(request, resourceGraph) }; - var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph); + var accessor = new Mock().Object; - return new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionProvider); + return new FieldsToSerialize(resourceGraph, constraintProviders, accessor); } [Benchmark] diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 830065bfa3..2cefca3858 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -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 GetMeta() { return new Dictionary { {"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.GetMeta` (or overriding it on `JsonApiResourceDefinition`): ```c# -public class RequestMetaService : IRequestMeta +public class PersonDefinition : JsonApiResourceDefinition { - public RequestMetaService(/*...other dependencies here */) { - // ... + public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + { } - public Dictionary GetMeta() + public override IReadOnlyDictionary GetMeta() { return new Dictionary { - {"copyright", "Copyright 2018 Example Corp."}, - {"authors", new string[] {"John Doe"}} + ["notice"] = "Check our intranet at http://www.example.com for personal details." }; } } @@ -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": { // ... diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs similarity index 84% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs index b28817b6f0..9c91823669 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs @@ -10,9 +10,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class ArticleDefinition : ResourceDefinition
+ public class ArticleHooksDefinition : ResourceHooksDefinition
{ - public ArticleDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs similarity index 78% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs index 02f66eafaf..bd533d715c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs @@ -9,9 +9,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public abstract class LockableDefinition : ResourceDefinition where T : class, IIsLockable, IIdentifiable + public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable { - protected LockableDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } protected void DisallowLocked(IEnumerable resources) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs similarity index 90% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs index 84c1e8ec95..2e211305ca 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs @@ -10,9 +10,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class PassportDefinition : ResourceDefinition + public class PassportHooksDefinition : ResourceHooksDefinition { - public PassportDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs index 7041e1fd42..aaa45b75a7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs @@ -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, IHasMeta + public class PersonDefinition : JsonApiResourceDefinition { - public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } - - public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline); - return ids; - } - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { - resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); } - public IReadOnlyDictionary GetMeta() + public override IReadOnlyDictionary GetMeta() { - return new Dictionary { - { "copyright", "Copyright 2015 Example Corp." }, - { "authors", new[] { "Jared Nance", "Maurits Moeys", "Harro van der Kroft" } } + return new Dictionary + { + ["license"] = "MIT", + ["projectUrl"] = "https://github.com/json-api-dotnet/JsonApiDotNetCore/", + ["versions"] = new[] + { + "v4.0.0", + "v3.1.0", + "v2.5.2", + "v1.3.1" + } }; } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs new file mode 100644 index 0000000000..42c1b405e4 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs @@ -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 + { + public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + { + BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline); + return ids; + } + + public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + { + resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs similarity index 81% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs index bcc423c1d5..4ff4508bf7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TagDefinition : ResourceDefinition + public class TagHooksDefinition : ResourceHooksDefinition { - public TagDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeCreate(IResourceHashSet affected, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs similarity index 86% rename from src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs rename to src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs index bdcb687b86..c3fc7af2e5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoHooksDefinition.cs @@ -9,9 +9,9 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TodoDefinition : LockableDefinition + public class TodoHooksDefinition : LockableHooksDefinition { - public TodoDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index d26b72dd69..9f3db2ce18 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -46,7 +46,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv var loggerFactory = _intermediateProvider.GetService(); _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); - _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory); + _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, loggerFactory); } /// @@ -114,7 +114,7 @@ public void DiscoverInjectables() /// /// Registers the remaining internals. /// - public void ConfigureServices(Type dbContextType) + public void ConfigureServiceContainer(Type dbContextType) { if (dbContextType != null) { @@ -127,28 +127,23 @@ public void ConfigureServices(Type dbContextType) _services.AddSingleton(new DbContextOptionsBuilder().Options); } + AddResourceLayer(); AddRepositoryLayer(); AddServiceLayer(); AddMiddlewareLayer(); + AddSerializationLayer(); + AddQueryStringLayer(); - _services.AddSingleton(sp => sp.GetRequiredService()); - - _services.AddScoped(); - _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); - _services.AddScoped(); - _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - AddServerSerialization(); - AddQueryStringParameterServices(); - if (_options.EnableResourceHooks) { AddResourceHooks(); } + _services.AddScoped(); + _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); + _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); + _services.AddScoped(); + _services.AddScoped(); _services.TryAddScoped(); } @@ -173,6 +168,17 @@ private void AddMiddlewareLayer() _services.AddScoped(); } + private void AddResourceLayer() + { + _services.AddScoped(typeof(IResourceDefinition<>), typeof(JsonApiResourceDefinition<>)); + _services.AddScoped(typeof(IResourceDefinition<,>), typeof(JsonApiResourceDefinition<,>)); + _services.AddScoped(); + + _services.AddScoped(); + + _services.AddSingleton(sp => sp.GetRequiredService()); + } + private void AddRepositoryLayer() { _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); @@ -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(); _services.AddScoped(); @@ -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(); _services.AddTransient(); } - private void AddServerSerialization() + private void AddSerializationLayer() { _services.AddScoped(); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index b1c4803b30..9e73f76a52 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Configuration @@ -26,12 +25,6 @@ public class ResourceContext /// public Type IdentityType { get; set; } - /// - /// The concrete type. - /// We store this so that we don't need to re-compute the generic type. - /// - public Type ResourceDefinitionType { get; set; } - /// /// Exposed resource attributes. /// See https://jsonapi.org/format/#document-resource-object-attributes. diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index cd08d5a222..8aaaa35d83 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -108,7 +108,6 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu Attributes = GetAttributes(resourceType), Relationships = GetRelationships(resourceType), EagerLoads = GetEagerLoads(resourceType), - ResourceDefinitionType = GetResourceDefinitionType(resourceType) }; private IReadOnlyCollection GetAttributes(Type resourceType) @@ -269,8 +268,6 @@ private Type TypeOrElementType(Type type) return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type; } - private Type GetResourceDefinitionType(Type resourceType) => typeof(ResourceDefinition<>).MakeGenericType(resourceType); - private string FormatResourceName(Type resourceType) { var formatter = new ResourceNameFormatter(_options); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 5b5b7a652a..55e1b52028 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ using JsonApiDotNetCore.Serialization.Client.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Configuration { @@ -57,7 +56,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.AddResourceGraph(dbContextType, configureResourceGraph); applicationBuilder.ConfigureMvc(); applicationBuilder.DiscoverInjectables(); - applicationBuilder.ConfigureServices(dbContextType); + applicationBuilder.ConfigureServiceContainer(dbContextType); } /// diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 278f0a104d..c01111f3b4 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -49,13 +49,19 @@ public class ServiceDiscoveryFacade typeof(IResourceReadRepository<,>) }; + private static readonly HashSet _resourceDefinitionInterfaces = new HashSet { + typeof(IResourceDefinition<>), + typeof(IResourceDefinition<,>) + }; + private readonly ILogger _logger; private readonly IServiceCollection _services; private readonly ResourceGraphBuilder _resourceGraphBuilder; + private readonly IJsonApiOptions _options; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); private readonly Dictionary> _resourceDescriptorsPerAssemblyCache = new Dictionary>(); - public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, ILoggerFactory loggerFactory) + public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, IJsonApiOptions options, ILoggerFactory loggerFactory) { if (loggerFactory == null) { @@ -65,6 +71,7 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder _logger = loggerFactory.CreateLogger(); _services = services ?? throw new ArgumentNullException(nameof(services)); _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); + _options = options ?? throw new ArgumentNullException(nameof(options)); } /// @@ -96,11 +103,11 @@ internal void DiscoverResources() foreach (var descriptor in resourceDescriptors) { - AddResource(assembly, descriptor); + AddResource(descriptor); } } } - + internal void DiscoverInjectables() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) @@ -111,9 +118,14 @@ internal void DiscoverInjectables() foreach (var descriptor in resourceDescriptors) { - AddResourceDefinition(assembly, descriptor); AddServices(assembly, descriptor); AddRepositories(assembly, descriptor); + AddResourceDefinitions(assembly, descriptor); + + if (_options.EnableResourceHooks) + { + AddResourceHookDefinitions(assembly, descriptor); + } } } } @@ -128,26 +140,26 @@ private void AddDbContextResolvers(Assembly assembly) } } - private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor) + private void AddResource(ResourceDescriptor resourceDescriptor) { _resourceGraphBuilder.Add(resourceDescriptor.ResourceType, resourceDescriptor.IdType); } - private void AddResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) + private void AddResourceHookDefinitions(Assembly assembly, ResourceDescriptor identifiable) { try { - var resourceDefinition = TypeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceDefinition<>), identifiable.ResourceType) + var resourceDefinition = TypeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceHooksDefinition<>), identifiable.ResourceType) .SingleOrDefault(); if (resourceDefinition != null) { - _services.AddScoped(typeof(ResourceDefinition<>).MakeGenericType(identifiable.ResourceType), resourceDefinition); + _services.AddScoped(typeof(ResourceHooksDefinition<>).MakeGenericType(identifiable.ResourceType), resourceDefinition); } } catch (InvalidOperationException e) { - throw new InvalidConfigurationException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); + throw new InvalidConfigurationException($"Cannot define multiple ResourceHooksDefinition<> implementations for '{identifiable.ResourceType}'", e); } } @@ -155,24 +167,28 @@ private void AddServices(Assembly assembly, ResourceDescriptor resourceDescripto { foreach (var serviceInterface in ServiceInterfaces) { - RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + RegisterImplementations(assembly, serviceInterface, resourceDescriptor); } } private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var serviceInterface in _repositoryInterfaces) + foreach (var repositoryInterface in _repositoryInterfaces) { - RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + RegisterImplementations(assembly, repositoryInterface, resourceDescriptor); } } - - private void RegisterServiceImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) + + private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resourceDescriptor) { - if (resourceDescriptor.IdType == typeof(Guid) && interfaceType.GetTypeInfo().GenericTypeParameters.Length == 1) + foreach (var resourceDefinitionInterface in _resourceDefinitionInterfaces) { - return; + RegisterImplementations(assembly, resourceDefinitionInterface, resourceDescriptor); } + } + + private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) + { var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? new[] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } : new[] { resourceDescriptor.ResourceType }; var (implementation, registrationInterface) = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index 2c36df54bc..7a603cad6b 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs index 0c87bbfa2b..95a16007ca 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Hooks.Internal.Discovery /// public class HooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable { - private readonly Type _boundResourceDefinitionType = typeof(ResourceDefinition); + private readonly Type _boundResourceDefinitionType = typeof(ResourceHooksDefinition); private readonly ResourceHook[] _allHooks; private readonly ResourceHook[] _databaseValuesAttributeAllowed = { diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs index 03c8ffa8d5..fc6457e019 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs @@ -45,7 +45,7 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, // so we need not even bother. if (!_hookContainers.TryGetValue(targetResource, out IResourceHookContainer container)) { - container = _genericProcessorFactory.Get(typeof(ResourceDefinition<>), targetResource); + container = _genericProcessorFactory.Get(typeof(ResourceHooksDefinition<>), targetResource); _hookContainers[targetResource] = container; } if (container == null) return null; diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs index b04ef1aae5..810a3f3dd5 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs @@ -16,7 +16,7 @@ internal interface IHookExecutorHelper { /// /// For a particular ResourceHook and for a given model type, checks if - /// the ResourceDefinition has an implementation for the hook + /// the ResourceHooksDefinition has an implementation for the hook /// and if so, return it. /// /// Also caches the retrieves containers so we don't need to reflectively @@ -26,7 +26,7 @@ internal interface IHookExecutorHelper /// /// For a particular ResourceHook and for a given model type, checks if - /// the ResourceDefinition has an implementation for the hook + /// the ResourceHooksDefinition has an implementation for the hook /// and if so, return it. /// /// Also caches the retrieves containers so we don't need to reflectively diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs index 4a0369617b..11570d6212 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs @@ -24,7 +24,7 @@ public interface ICreateHookContainer where TResource : class, IIdent /// /// If new relationships are to be created with the to-be-created resources, /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// For each of these relationships, the /// hook is fired after the execution of this hook. /// /// The transformed resource set @@ -38,7 +38,7 @@ public interface ICreateHookContainer where TResource : class, IIdent /// /// If relationships were created with the created resources, this will /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// For each of these relationships, the /// hook is fired after the execution of this hook. /// /// The transformed resource set @@ -46,4 +46,4 @@ public interface ICreateHookContainer where TResource : class, IIdent /// An enum indicating from where the hook was triggered. void AfterCreate(HashSet resources, ResourcePipeline pipeline); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs index e2f90560fb..470a90d1db 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs @@ -11,10 +11,10 @@ public interface ICreateHookExecutor /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. /// The returned set will be used in the actual operation in . /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// - /// Fires the + /// Fires the /// hook for any secondary (nested) resource for values within parameter /// /// The transformed set @@ -25,10 +25,10 @@ public interface ICreateHookExecutor /// /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// - /// Fires the + /// Fires the /// hook for any secondary (nested) resource for values within parameter /// /// Target resources for the Before cycle. diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs index 9f9a8c41d5..7ab640fd49 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs @@ -23,7 +23,7 @@ public interface IDeleteHookContainer where TResource : class, IIdent /// /// If by the deletion of these resources any other resources are affected /// implicitly by the removal of their relationships (eg - /// in the case of an one-to-one relationship), the + /// in the case of an one-to-one relationship), the /// hook is fired for these resources. /// /// The transformed resource set diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs index 205d2cacc6..efab29ac87 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs @@ -11,10 +11,10 @@ public interface IDeleteHookExecutor /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. /// The returned set will be used in the actual operation in . /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// - /// Fires the + /// Fires the /// hook for any resources that are indirectly (implicitly) affected by this operation. /// Eg: when deleting a resource that has relationships set to other resources, /// these other resources are implicitly affected by the delete operation. @@ -28,8 +28,8 @@ public interface IDeleteHookExecutor /// /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// /// Target resources for the Before cycle. /// An enum indicating from where the hook was triggered. @@ -37,4 +37,4 @@ public interface IDeleteHookExecutor /// The type of the root resources void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs index 3bea4012fa..30d10479f3 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs @@ -12,7 +12,7 @@ public interface IOnReturnHookExecutor /// /// Executes the On Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the for every unique + /// Fires the for every unique /// resource type occurring in parameter . /// /// The transformed set diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs index 0abbbe583f..920341045d 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs @@ -13,9 +13,8 @@ public interface IReadHookExecutor /// /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook where T = for the requested - /// resources as well as any related relationship. + /// Fires the + /// hook for the requested resources as well as any related relationship. /// /// An enum indicating from where the hook was triggered. /// StringId of the requested resource in the case of @@ -25,7 +24,7 @@ public interface IReadHookExecutor /// /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the for every unique + /// Fires the for every unique /// resource type occurring in parameter . /// /// Target resources for the Before cycle. diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs index 60e0913fa4..b9cf89cd4d 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface IResourceHookContainer { } /// - /// Implement this interface to implement business logic hooks on . + /// Implement this interface to implement business logic hooks on . /// public interface IResourceHookContainer : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs index 3faa9f7798..6c27e2569b 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Hooks.Internal { /// /// Transient service responsible for executing Resource Hooks as defined - /// in . see methods in + /// in . see methods in /// , and /// for more information. /// diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs index f0aa20df5a..dcf26d3fd0 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs @@ -25,12 +25,12 @@ public interface IUpdateHookContainer where TResource : class, IIdent /// /// If new relationships are to be created with the to-be-updated resources, /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// For each of these relationships, the /// hook is fired after the execution of this hook. /// /// If by the creation of these relationships, any other relationships (eg /// in the case of an already populated one-to-one relationship) are implicitly - /// affected, the + /// affected, the /// hook is fired for these. /// /// The transformed resource set @@ -65,7 +65,7 @@ public interface IUpdateHookContainer where TResource : class, IIdent /// /// If relationships were updated with the updated resources, this will /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// For each of these relationships, the /// hook is fired after the execution of this hook. /// /// The unique set of affected resources. @@ -91,7 +91,7 @@ public interface IUpdateHookContainer where TResource : class, IIdent /// and by this the relationship to a different Person was implicitly removed, /// this hook will be fired for the latter Person. /// - /// See for information about + /// See for information about /// when this hook is fired. /// /// diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs index ba1320a03e..9884e88260 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs @@ -14,13 +14,13 @@ public interface IUpdateHookExecutor /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. /// The returned set will be used in the actual operation in . /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// - /// Fires the + /// Fires the /// hook for any secondary (nested) resource for values within parameter /// - /// Fires the + /// Fires the /// hook for any resources that are indirectly (implicitly) affected by this operation. /// Eg: when updating a one-to-one relationship of a resource which already /// had this relationship populated, then this update will indirectly affect @@ -34,10 +34,10 @@ public interface IUpdateHookExecutor /// /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook where T = for values in parameter . + /// Fires the + /// hook for values in parameter . /// - /// Fires the + /// Fires the /// hook for any secondary (nested) resource for values within parameter /// /// Target resources for the Before cycle. @@ -45,4 +45,4 @@ public interface IUpdateHookExecutor /// The type of the root resources void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index 4f7e3d41f2..37bb6cde07 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Holds a expression, used for custom query string handlers from s. + /// Holds a expression, used for custom query string handlers from s. /// public class QueryableHandlerExpression : QueryExpression { diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 1cbbaa47d4..58d723f13f 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -13,20 +13,20 @@ public class QueryLayerComposer : IQueryLayerComposer { private readonly IEnumerable _constraintProviders; private readonly IResourceContextProvider _resourceContextProvider; - private readonly IResourceDefinitionProvider _resourceDefinitionProvider; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IJsonApiOptions _options; private readonly IPaginationContext _paginationContext; public QueryLayerComposer( IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, - IResourceDefinitionProvider resourceDefinitionProvider, + IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext) { _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _resourceDefinitionProvider = resourceDefinitionProvider ?? throw new ArgumentNullException(nameof(resourceDefinitionProvider)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); _options = options ?? throw new ArgumentNullException(nameof(options)); _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); } @@ -235,12 +235,7 @@ protected virtual IReadOnlyCollection GetIncludeElemen { if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); - var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); - if (resourceDefinition != null) - { - includeElements = resourceDefinition.OnApplyIncludes(includeElements); - } - + includeElements = _resourceDefinitionAccessor.OnApplyIncludes(resourceContext.ResourceType, includeElements); return includeElements; } @@ -252,12 +247,7 @@ protected virtual FilterExpression GetFilter(IReadOnlyCollection().ToArray(); var filter = filters.Length > 1 ? new LogicalExpression(LogicalOperator.And, filters) : filters.FirstOrDefault(); - var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); - if (resourceDefinition != null) - { - filter = resourceDefinition.OnApplyFilter(filter); - } - + filter = _resourceDefinitionAccessor.OnApplyFilter(resourceContext.ResourceType, filter); return filter; } @@ -267,12 +257,8 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); var sort = expressionsInScope.OfType().FirstOrDefault(); - - var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); - if (resourceDefinition != null) - { - sort = resourceDefinition.OnApplySort(sort); - } + + sort = _resourceDefinitionAccessor.OnApplySort(resourceContext.ResourceType, sort); if (sort == null) { @@ -289,12 +275,8 @@ protected virtual PaginationExpression GetPagination(IReadOnlyCollection().FirstOrDefault(); - - var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); - if (resourceDefinition != null) - { - pagination = resourceDefinition.OnApplyPagination(pagination); - } + + pagination = _resourceDefinitionAccessor.OnApplyPagination(resourceContext.ResourceType, pagination); pagination ??= new PaginationExpression(PageNumber.ValueOne, _options.DefaultPageSize); @@ -308,14 +290,10 @@ protected virtual IDictionary GetSparseField var attributes = expressionsInScope.OfType().SelectMany(sparseFieldSet => sparseFieldSet.Attributes).ToHashSet(); - var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); - if (resourceDefinition != null) - { - var tempExpression = attributes.Any() ? new SparseFieldSetExpression(attributes) : null; - tempExpression = resourceDefinition.OnApplySparseFieldSet(tempExpression); + var tempExpression = attributes.Any() ? new SparseFieldSetExpression(attributes) : null; + tempExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, tempExpression); - attributes = tempExpression == null ? new HashSet() : tempExpression.Attributes.ToHashSet(); - } + attributes = tempExpression == null ? new HashSet() : tempExpression.Attributes.ToHashSet(); if (!attributes.Any()) { diff --git a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs index d2531b6918..5443dd58e8 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.QueryStrings { /// - /// Reads custom query string parameters for which handlers on are registered + /// Reads custom query string parameters for which handlers on are registered /// and produces a set of query constraints from it. /// public interface IResourceDefinitionQueryableParameterReader : IQueryStringParameterReader, IQueryConstraintProvider diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index acce8a5148..310f4cd87f 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -14,13 +14,13 @@ namespace JsonApiDotNetCore.QueryStrings.Internal public class ResourceDefinitionQueryableParameterReader : IResourceDefinitionQueryableParameterReader { private readonly IJsonApiRequest _request; - private readonly IResourceDefinitionProvider _resourceDefinitionProvider; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly List _constraints = new List(); - public ResourceDefinitionQueryableParameterReader(IJsonApiRequest request, IResourceDefinitionProvider resourceDefinitionProvider) + public ResourceDefinitionQueryableParameterReader(IJsonApiRequest request, IResourceDefinitionAccessor resourceDefinitionAccessor) { _request = request ?? throw new ArgumentNullException(nameof(request)); - _resourceDefinitionProvider = resourceDefinitionProvider ?? throw new ArgumentNullException(nameof(resourceDefinitionProvider)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); } /// @@ -54,8 +54,7 @@ private object GetQueryableHandler(string parameterName) } var resourceType = _request.PrimaryResource.ResourceType; - var resourceDefinition = _resourceDefinitionProvider.Get(resourceType); - return resourceDefinition?.GetQueryableHandlerForQueryStringParameter(parameterName); + return _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName); } /// diff --git a/src/JsonApiDotNetCore/Resources/IHasMeta.cs b/src/JsonApiDotNetCore/Resources/IHasMeta.cs deleted file mode 100644 index a22408d78d..0000000000 --- a/src/JsonApiDotNetCore/Resources/IHasMeta.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Resources -{ - /// - /// When implemented by a class, indicates it provides json:api meta key/value pairs. - /// - public interface IHasMeta - { - IReadOnlyDictionary GetMeta(); - } -} diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index a192e103b0..ab246ceb1f 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -1,18 +1,122 @@ using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Queries.Expressions; namespace JsonApiDotNetCore.Resources { /// - /// Used internally to track resource extensibility endpoints. Do not implement this interface directly. + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. + /// The goal here is to reduce the need for overriding the service and repository layers. /// - public interface IResourceDefinition + /// The resource type. + public interface IResourceDefinition : IResourceDefinition + where TResource : class, IIdentifiable { + } + + /// + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. + /// The goal here is to reduce the need for overriding the service and repository layers. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceDefinition + where TResource : class, IIdentifiable + { + /// + /// Enables to extend, replace or remove includes that are being applied on this resource type. + /// + /// + /// An optional existing set of includes, coming from query string. Never null, but may be empty. + /// + /// + /// The new set of includes. Return an empty collection to remove all inclusions (never return null). + /// IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes); + + /// + /// Enables to extend, replace or remove a filter that is being applied on a set of this resource type. + /// + /// + /// An optional existing filter, coming from query string. Can be null. + /// + /// + /// The new filter, or null to disable the existing filter. + /// FilterExpression OnApplyFilter(FilterExpression existingFilter); + + /// + /// Enables to extend, replace or remove a sort order that is being applied on a set of this resource type. + /// Tip: Use to build from a lambda expression. + /// + /// + /// An optional existing sort order, coming from query string. Can be null. + /// + /// + /// The new sort order, or null to disable the existing sort order and sort by ID. + /// SortExpression OnApplySort(SortExpression existingSort); + + /// + /// Enables to extend, replace or remove pagination that is being applied on a set of this resource type. + /// + /// + /// An optional existing pagination, coming from query string. Can be null. + /// + /// + /// The changed pagination, or null to use the first page with default size from options. + /// To disable paging, set to null. + /// PaginationExpression OnApplyPagination(PaginationExpression existingPagination); + + /// + /// Enables to extend, replace or remove a sparse fieldset that is being applied on a set of this resource type. + /// Tip: Use and + /// to safely change the fieldset without worrying about nulls. + /// + /// + /// This method executes twice for a single request: first to select which fields to retrieve from the data store and then to + /// select which fields to serialize. Including extra fields from this method will retrieve them, but not include them in the json output. + /// This enables you to expose calculated properties whose value depends on a field that is not in the sparse fieldset. + /// + /// The incoming sparse fieldset from query string. + /// At query execution time, this is null if the query string contains no sparse fieldset. + /// At serialization time, this contains all viewable fields if the query string contains no sparse fieldset. + /// + /// + /// The new sparse fieldset, or null to discard the existing sparse fieldset and select all viewable fields. + /// SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet); - object GetQueryableHandlerForQueryStringParameter(string parameterName); + + /// + /// Enables to adapt the Entity Framework Core query, based on custom query string parameters. + /// Note this only works on primary resource requests, such as /articles, but not on /blogs/1/articles or /blogs?include=articles. + /// + /// + /// source + /// .Include(model => model.Children) + /// .Where(model => model.LastUpdateTime > DateTime.Now.AddMonths(-1)), + /// ["isHighRisk"] = FilterByHighRisk + /// }; + /// } + /// + /// private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue) + /// { + /// bool isFilterOnHighRisk = bool.Parse(parameterValue); + /// return isFilterOnHighRisk ? source.Where(model => model.RiskLevel >= 5) : source.Where(model => model.RiskLevel < 5); + /// } + /// ]]> + /// + QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters(); + + /// + /// Enables to add json:api meta information, specific to this resource type. + /// + IReadOnlyDictionary GetMeta(); } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs new file mode 100644 index 0000000000..7dac3e616c --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Queries.Expressions; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Retrieves a instance from the D/I container and invokes a callback on it. + /// + public interface IResourceDefinitionAccessor + { + /// + /// Invokes for the specified resource type. + /// + IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes); + + /// + /// Invokes for the specified resource type. + /// + FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter); + + /// + /// Invokes for the specified resource type. + /// + SortExpression OnApplySort(Type resourceType, SortExpression existingSort); + + /// + /// Invokes for the specified resource type. + /// + PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination); + + /// + /// Invokes for the specified resource type. + /// + SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet); + + /// + /// Invokes for the specified resource type, + /// then returns the expression for the specified parameter name. + /// + object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName); + + /// + /// Invokes for the specified resource type. + /// + IReadOnlyDictionary GetMeta(Type resourceType); + } +} diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs deleted file mode 100644 index 07a17148a4..0000000000 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Resources -{ - /// - /// Retrieves a from the DI container. - /// Abstracts away the creation of the corresponding generic type and usage - /// of the service provider to do so. - /// - public interface IResourceDefinitionProvider - { - /// - /// Retrieves the resource definition associated to . - /// - IResourceDefinition Get(Type resourceType); - } -} diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs new file mode 100644 index 0000000000..37c9d6e761 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. + /// The goal here is to reduce the need for overriding the service and repository layers. + /// + /// The resource type. + public class JsonApiResourceDefinition : JsonApiResourceDefinition, IResourceDefinition + where TResource : class, IIdentifiable + { + public JsonApiResourceDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } + } + + /// + public class JsonApiResourceDefinition : IResourceDefinition + where TResource : class, IIdentifiable + { + protected IResourceGraph ResourceGraph { get; } + + public JsonApiResourceDefinition(IResourceGraph resourceGraph) + { + ResourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + } + + /// + public virtual IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + { + return existingIncludes; + } + + /// + public virtual FilterExpression OnApplyFilter(FilterExpression existingFilter) + { + return existingFilter; + } + + /// + public virtual SortExpression OnApplySort(SortExpression existingSort) + { + return existingSort; + } + + /// + /// Creates a from a lambda expression. + /// + /// + /// model.CreatedAt, ListSortDirection.Ascending), + /// (model => model.Password, ListSortDirection.Descending) + /// }); + /// ]]> + /// + protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySelectors) + { + if (keySelectors == null) + { + throw new ArgumentNullException(nameof(keySelectors)); + } + + List sortElements = new List(); + + foreach (var (keySelector, sortDirection) in keySelectors) + { + bool isAscending = sortDirection == ListSortDirection.Ascending; + var attribute = ResourceGraph.GetAttributes(keySelector).Single(); + + var sortElement = new SortElementExpression(new ResourceFieldChainExpression(attribute), isAscending); + sortElements.Add(sortElement); + } + + return new SortExpression(sortElements); + } + + /// + public virtual PaginationExpression OnApplyPagination(PaginationExpression existingPagination) + { + return existingPagination; + } + + /// + public virtual SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) + { + return existingSparseFieldSet; + } + + /// + public virtual QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() + { + return null; + } + + /// + public virtual IReadOnlyDictionary GetMeta() + { + return null; + } + + /// + /// This is an alias type intended to simplify the implementation's method signature. + /// See for usage details. + /// + public sealed class PropertySortOrder : List<(Expression> KeySelector, ListSortDirection SortDirection)> + { + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs b/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs new file mode 100644 index 0000000000..5914fd174f --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// This is an alias type intended to simplify the implementation's method signature. + /// See for usage details. + /// + public sealed class QueryStringParameterHandlers : Dictionary, StringValues, IQueryable>> + { + } +} diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinition.cs deleted file mode 100644 index ee446b9334..0000000000 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinition.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Linq.Expressions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Hooks.Internal; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Queries.Expressions; -using Microsoft.Extensions.Primitives; - -namespace JsonApiDotNetCore.Resources -{ - /// - /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. - /// It is intended to improve the developer experience and reduce boilerplate for commonly required features. - /// The goal of this class is to reduce the frequency with which developers have to override the service and repository layers. - /// - /// The resource type. - public class ResourceDefinition : IResourceDefinition, IResourceHookContainer where TResource : class, IIdentifiable - { - protected IResourceGraph ResourceGraph { get; } - - public ResourceDefinition(IResourceGraph resourceGraph) - { - ResourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); - } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { return ids; } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - - /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { return resources; } - - /// - /// Enables to extend, replace or remove includes that are being applied on this resource type. - /// - /// - /// An optional existing set of includes, coming from query string. Never null, but may be empty. - /// - /// - /// The new set of includes. Return an empty collection to remove all inclusions (never return null). - /// - public virtual IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) - { - return existingIncludes; - } - - /// - /// Enables to extend, replace or remove a filter that is being applied on a set of this resource type. - /// - /// - /// An optional existing filter, coming from query string. Can be null. - /// - /// - /// The new filter, or null to disable the existing filter. - /// - public virtual FilterExpression OnApplyFilter(FilterExpression existingFilter) - { - return existingFilter; - } - - /// - /// Enables to extend, replace or remove a sort order that is being applied on a set of this resource type. - /// Tip: Use to build from a lambda expression. - /// - /// - /// An optional existing sort order, coming from query string. Can be null. - /// - /// - /// The new sort order, or null to disable the existing sort order and sort by ID. - /// - public virtual SortExpression OnApplySort(SortExpression existingSort) - { - return existingSort; - } - - /// - /// Creates a from a lambda expression. - /// - /// - /// model.CreatedAt, ListSortDirection.Ascending), - /// (model => model.Password, ListSortDirection.Descending) - /// }); - /// ]]> - /// - protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySelectors) - { - if (keySelectors == null) - { - throw new ArgumentNullException(nameof(keySelectors)); - } - - List sortElements = new List(); - - foreach (var (keySelector, sortDirection) in keySelectors) - { - bool isAscending = sortDirection == ListSortDirection.Ascending; - var attribute = ResourceGraph.GetAttributes(keySelector).Single(); - - var sortElement = new SortElementExpression(new ResourceFieldChainExpression(attribute), isAscending); - sortElements.Add(sortElement); - } - - return new SortExpression(sortElements); - } - - /// - /// Enables to extend, replace or remove pagination that is being applied on a set of this resource type. - /// - /// - /// An optional existing pagination, coming from query string. Can be null. - /// - /// - /// The changed pagination, or null to use the first page with default size from options. - /// To disable paging, set to null. - /// - public virtual PaginationExpression OnApplyPagination(PaginationExpression existingPagination) - { - return existingPagination; - } - - /// - /// Enables to extend, replace or remove a sparse fieldset that is being applied on a set of this resource type. - /// Tip: Use and - /// to safely change the fieldset without worrying about nulls. - /// - /// - /// This method executes twice for a single request: first to select which fields to retrieve from the data store and then to - /// select which fields to serialize. Including extra fields from this method will retrieve them, but not include them in the json output. - /// This enables you to expose calculated properties whose value depends on a field that is not in the sparse fieldset. - /// - /// The incoming sparse fieldset from query string. - /// At query execution time, this is null if the query string contains no sparse fieldset. - /// At serialization time, this contains all viewable fields if the query string contains no sparse fieldset. - /// - /// - /// The new sparse fieldset, or null to discard the existing sparse fieldset and select all viewable fields. - /// - public virtual SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) - { - return existingSparseFieldSet; - } - - /// - /// Enables to adapt the Entity Framework Core query, based on custom query string parameters. - /// Note this only works on primary resource requests, such as /articles, but not on /blogs/1/articles or /blogs?include=articles. - /// - /// - /// source - /// .Include(model => model.Children) - /// .Where(model => model.LastUpdateTime > DateTime.Now.AddMonths(-1)), - /// ["isHighRisk"] = FilterByHighRisk - /// }; - /// } - /// - /// private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue) - /// { - /// bool isFilterOnHighRisk = bool.Parse(parameterValue); - /// return isFilterOnHighRisk ? source.Where(model => model.RiskLevel >= 5) : source.Where(model => model.RiskLevel < 5); - /// } - /// ]]> - /// - protected virtual QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() - { - return new QueryStringParameterHandlers(); - } - - public object GetQueryableHandlerForQueryStringParameter(string parameterName) - { - if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); - - var handlers = OnRegisterQueryableHandlersForQueryStringParameters(); - return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; - } - - /// - /// This is an alias type intended to simplify the implementation's method signature. - /// See for usage details. - /// - public sealed class PropertySortOrder : List<(Expression> KeySelector, ListSortDirection SortDirection)> - { - } - - /// - /// This is an alias type intended to simplify the implementation's method signature. - /// See for usage details. - /// - public sealed class QueryStringParameterHandlers : Dictionary, StringValues, IQueryable>> - { - } - } -} diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs new file mode 100644 index 0000000000..7318e82236 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.Resources +{ + /// + public class ResourceDefinitionAccessor : IResourceDefinitionAccessor + { + private readonly IResourceContextProvider _resourceContextProvider; + private readonly IServiceProvider _serviceProvider; + + public ResourceDefinitionAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) + { + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + public IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.OnApplyIncludes(existingIncludes); + } + + /// + public FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.OnApplyFilter(existingFilter); + } + + /// + public SortExpression OnApplySort(Type resourceType, SortExpression existingSort) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.OnApplySort(existingSort); + } + + /// + public PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.OnApplyPagination(existingPagination); + } + + /// + public SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.OnApplySparseFieldSet(existingSparseFieldSet); + } + + /// + public object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + var handlers = resourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters(); + + return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; + } + + /// + public IReadOnlyDictionary GetMeta(Type resourceType) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + dynamic resourceDefinition = GetResourceDefinition(resourceType); + return resourceDefinition.GetMeta(); + } + + protected object GetResourceDefinition(Type resourceType) + { + var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + + if (resourceContext.IdentityType == typeof(int)) + { + var intResourceDefinitionType = typeof(IResourceDefinition<>).MakeGenericType(resourceContext.ResourceType); + var intResourceDefinition = _serviceProvider.GetService(intResourceDefinitionType); + + if (intResourceDefinition != null) + { + return intResourceDefinition; + } + } + + var resourceDefinitionType = typeof(IResourceDefinition<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + return _serviceProvider.GetRequiredService(resourceDefinitionType); + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs deleted file mode 100644 index 74e41aa304..0000000000 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; - -namespace JsonApiDotNetCore.Resources -{ - /// - internal sealed class ResourceDefinitionProvider : IResourceDefinitionProvider - { - private readonly IResourceGraph _resourceContextProvider; - private readonly IRequestScopedServiceProvider _serviceProvider; - - public ResourceDefinitionProvider(IResourceGraph resourceContextProvider, IRequestScopedServiceProvider serviceProvider) - { - _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } - - /// - public IResourceDefinition Get(Type resourceType) - { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); - - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); - return (IResourceDefinition)_serviceProvider.GetService(resourceContext.ResourceDefinitionType); - } - } -} diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 5352ea4bd7..bf3c5e3f23 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs new file mode 100644 index 0000000000..4687c5b531 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Execution; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. + /// It is intended to improve the developer experience and reduce boilerplate for commonly required features. + /// The goal of this class is to reduce the frequency with which developers have to override the service and repository layers. + /// + /// The resource type. + public class ResourceHooksDefinition : IResourceHookContainer where TResource : class, IIdentifiable + { + protected IResourceGraph ResourceGraph { get; } + + public ResourceHooksDefinition(IResourceGraph resourceGraph) + { + ResourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) { return resources; } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { return ids; } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { return resources; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index f73bb728b6..b9837ac7ba 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -10,19 +10,19 @@ namespace JsonApiDotNetCore.Serialization.Building /// public class MetaBuilder : IMetaBuilder where TResource : class, IIdentifiable { - private Dictionary _meta = new Dictionary(); private readonly IPaginationContext _paginationContext; private readonly IJsonApiOptions _options; - private readonly IRequestMeta _requestMeta; - private readonly IHasMeta _resourceMeta; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly IResponseMeta _responseMeta; + + private Dictionary _meta = new Dictionary(); - public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IRequestMeta requestMeta = null, - ResourceDefinition resourceDefinition = null) + public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor, IResponseMeta responseMeta = null) { _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); _options = options ?? throw new ArgumentNullException(nameof(options)); - _requestMeta = requestMeta; - _resourceMeta = resourceDefinition as IHasMeta; + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); + _responseMeta = responseMeta; } /// @@ -54,14 +54,15 @@ public IDictionary GetMeta() _meta.Add(key, _paginationContext.TotalResourceCount); } - if (_requestMeta != null) + if (_responseMeta != null) { - Add(_requestMeta.GetMeta()); + Add(_responseMeta.GetMeta()); } - if (_resourceMeta != null) + var resourceMeta = _resourceDefinitionAccessor.GetMeta(typeof(TResource)); + if (resourceMeta != null) { - Add(_resourceMeta.GetMeta()); + Add(resourceMeta); } return _meta.Any() ? _meta : null; diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index 5f5a113430..f08cbab1e6 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -14,16 +14,16 @@ public class FieldsToSerialize : IFieldsToSerialize { private readonly IResourceGraph _resourceGraph; private readonly IEnumerable _constraintProviders; - private readonly IResourceDefinitionProvider _resourceDefinitionProvider; + private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; public FieldsToSerialize( IResourceGraph resourceGraph, IEnumerable constraintProviders, - IResourceDefinitionProvider resourceDefinitionProvider) + IResourceDefinitionAccessor resourceDefinitionAccessor) { _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); - _resourceDefinitionProvider = resourceDefinitionProvider ?? throw new ArgumentNullException(nameof(resourceDefinitionProvider)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); } /// @@ -46,20 +46,16 @@ public IReadOnlyCollection GetAttributes(Type resourceType, Relat sparseFieldSetAttributes = GetViewableAttributes(resourceType); } - var resourceDefinition = _resourceDefinitionProvider.Get(resourceType); - if (resourceDefinition != null) - { - var inputExpression = sparseFieldSetAttributes.Any() ? new SparseFieldSetExpression(sparseFieldSetAttributes) : null; - var outputExpression = resourceDefinition.OnApplySparseFieldSet(inputExpression); + var inputExpression = sparseFieldSetAttributes.Any() ? new SparseFieldSetExpression(sparseFieldSetAttributes) : null; + var outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceType, inputExpression); - if (outputExpression == null) - { - sparseFieldSetAttributes = GetViewableAttributes(resourceType); - } - else - { - sparseFieldSetAttributes.IntersectWith(outputExpression.Attributes); - } + if (outputExpression == null) + { + sparseFieldSetAttributes = GetViewableAttributes(resourceType); + } + else + { + sparseFieldSetAttributes.IntersectWith(outputExpression.Attributes); } return sparseFieldSetAttributes; diff --git a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs similarity index 51% rename from src/JsonApiDotNetCore/Serialization/IRequestMeta.cs rename to src/JsonApiDotNetCore/Serialization/IResponseMeta.cs index 6fa34c0d9a..2c423ed74e 100644 --- a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs @@ -5,11 +5,10 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Service to add global top-level metadata to a . - /// Use on - /// to specify top-level metadata per resource type. + /// Service to add global top-level json:api meta to a response . + /// Use to specify top-level metadata per resource type. /// - public interface IRequestMeta + public interface IResponseMeta { IReadOnlyDictionary GetMeta(); } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 23063e4932..c370354a60 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -20,19 +20,17 @@ namespace DiscoveryTests public sealed class ServiceDiscoveryFacadeTests { private readonly IServiceCollection _services = new ServiceCollection(); + private readonly JsonApiOptions _options = new JsonApiOptions(); private readonly ResourceGraphBuilder _resourceGraphBuilder; public ServiceDiscoveryFacadeTests() { - var options = new JsonApiOptions(); - var dbResolverMock = new Mock(); dbResolverMock.Setup(m => m.GetContext()).Returns(new Mock().Object); - TestModelRepository._dbContextResolver = dbResolverMock.Object; + _services.AddScoped(_ => dbResolverMock.Object); - _services.AddSingleton(options); + _services.AddSingleton(_options); _services.AddSingleton(new LoggerFactory()); - _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -42,16 +40,15 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _services.AddTransient(_ => new Mock().Object); - _resourceGraphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + _resourceGraphBuilder = new ResourceGraphBuilder(_options, NullLoggerFactory.Instance); } [Fact] public void DiscoverResources_Adds_Resources_From_Added_Assembly_To_Graph() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); facade.AddAssembly(typeof(Person).Assembly); // Act @@ -69,9 +66,9 @@ public void DiscoverResources_Adds_Resources_From_Added_Assembly_To_Graph() public void DiscoverResources_Adds_Resources_From_Current_Assembly_To_Graph() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act facade.DiscoverResources(); @@ -85,12 +82,12 @@ public void DiscoverResources_Adds_Resources_From_Current_Assembly_To_Graph() public void DiscoverInjectables_Adds_Resource_Services_From_Current_Assembly_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); // Act facade.DiscoverInjectables(); - + // Assert var services = _services.BuildServiceProvider(); var service = services.GetService>(); @@ -101,9 +98,9 @@ public void DiscoverInjectables_Adds_Resource_Services_From_Current_Assembly_To_ public void DiscoverInjectables_Adds_Resource_Repositories_From_Current_Assembly_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act facade.DiscoverInjectables(); @@ -116,16 +113,34 @@ public void DiscoverInjectables_Adds_Resource_Repositories_From_Current_Assembly public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); + Assert.IsType(services.GetService>()); } + + [Fact] + public void AddCurrentAssembly_Adds_Resource_Hooks_Definitions_From_Current_Assembly_To_Container() + { + // Arrange + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); + + _options.EnableResourceHooks = true; + + // Act + facade.DiscoverInjectables(); + + // Assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } + public sealed class TestModel : Identifiable { } public class TestModelService : JsonApiResourceService @@ -148,20 +163,24 @@ public TestModelService( public class TestModelRepository : EntityFrameworkCoreRepository { - internal static IDbContextResolver _dbContextResolver; - public TestModelRepository( ITargetedFields targetedFields, + IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, _dbContextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) { } } - public class TestModelResourceDefinition : ResourceDefinition + public class TestModelResourceHooksDefinition : ResourceHooksDefinition + { + public TestModelResourceHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + } + + public class TestModelResourceDefinition : JsonApiResourceDefinition { public TestModelResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs index 46d3074325..326240b6cc 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -21,12 +21,12 @@ public RequestMetaTests(IntegrationTestContext testContex testContext.ConfigureServicesBeforeStartup(services => { - services.AddScoped(); + services.AddScoped(); }); } [Fact] - public async Task Injecting_IRequestMeta_Adds_Meta_Data() + public async Task Injecting_IResponseMeta_Adds_Meta_Data() { // Arrange var route = "/api/v1/people"; @@ -43,7 +43,7 @@ public async Task Injecting_IRequestMeta_Adds_Meta_Data() } } - public sealed class TestRequestMeta : IRequestMeta + public sealed class TestResponseMeta : IResponseMeta { public IReadOnlyDictionary GetMeta() { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs index 882251b145..7b5f67905f 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs @@ -48,11 +48,13 @@ public async Task When_getting_person_it_must_match_JSON_text() var expected = @"{ ""meta"": { - ""copyright"": ""Copyright 2015 Example Corp."", - ""authors"": [ - ""Jared Nance"", - ""Maurits Moeys"", - ""Harro van der Kroft"" + ""license"": ""MIT"", + ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", + ""versions"": [ + ""v4.0.0"", + ""v3.1.0"", + ""v2.5.2"", + ""v1.3.1"" ] }, ""links"": { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index 65ce8d8f67..422af54634 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Definitions; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; @@ -180,7 +181,7 @@ public async Task ResourceThatImplements_IHasMeta_Contains_MetaData() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - var expectedMeta = (_fixture.GetService>() as IHasMeta).GetMeta(); + var expectedMeta = _fixture.GetService>().GetMeta(); // Act var response = await client.SendAsync(request); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index d351fc510e..152bc31d82 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -162,11 +162,13 @@ public async Task When_getting_related_missing_to_one_resource_it_should_succeed var expected = @"{ ""meta"": { - ""copyright"": ""Copyright 2015 Example Corp."", - ""authors"": [ - ""Jared Nance"", - ""Maurits Moeys"", - ""Harro van der Kroft"" + ""license"": ""MIT"", + ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", + ""versions"": [ + ""v4.0.0"", + ""v3.1.0"", + ""v2.5.2"", + ""v1.3.1"" ] }, ""links"": { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs index 87bef32537..a3de6f4b0d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs @@ -16,7 +16,7 @@ public interface IUserRolesService bool AllowIncludeOwner { get; } } - public sealed class CallableResourceDefinition : ResourceDefinition + public sealed class CallableResourceDefinition : JsonApiResourceDefinition { private readonly IUserRolesService _userRolesService; private static readonly PageSize _maxPageSize = new PageSize(5); @@ -98,11 +98,11 @@ public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExp .Excluding(resource => resource.RiskLevel, ResourceGraph); } - protected override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() + public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() { // Use case: 'isHighRisk' query string parameter can be used to add extra filter on IQueryable. - return new QueryStringParameterHandlers + return new QueryStringParameterHandlers { ["isHighRisk"] = FilterByHighRisk }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs index e9a27ebda4..ac3c661242 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -20,7 +20,7 @@ public ResourceDefinitionQueryCallbackTests(IntegrationTestContext { - services.AddScoped, CallableResourceDefinition>(); + services.AddScoped, CallableResourceDefinition>(); services.AddSingleton(); }); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs index 585c965b3d..77128d4dbc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { - public sealed class Company : Identifiable + public sealed class Company : Identifiable, ISoftDeletable { [Attr] public string Name { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompanyResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompanyResourceDefinition.cs deleted file mode 100644 index c424a3ac98..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompanyResourceDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion -{ - public sealed class CompanyResourceDefinition : ResourceDefinition - { - private readonly IResourceGraph _resourceGraph; - - public CompanyResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) - { - _resourceGraph = resourceGraph; - } - - public override FilterExpression OnApplyFilter(FilterExpression existingFilter) - { - var resourceContext = _resourceGraph.GetResourceContext(); - var isSoftDeletedAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Company.IsSoftDeleted)); - - var isNotSoftDeleted = new ComparisonExpression(ComparisonOperator.Equals, - new ResourceFieldChainExpression(isSoftDeletedAttribute), new LiteralConstantExpression("false")); - - return existingFilter == null - ? (FilterExpression) isNotSoftDeleted - : new LogicalExpression(LogicalOperator.And, new[] {isNotSoftDeleted, existingFilter}); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs index e2688c4110..06154b5a84 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { - public sealed class Department : Identifiable + public sealed class Department : Identifiable, ISoftDeletable { [Attr] public string Name { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/ISoftDeletable.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/ISoftDeletable.cs new file mode 100644 index 0000000000..8aa1b29847 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/ISoftDeletable.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion +{ + public interface ISoftDeletable + { + bool IsSoftDeleted { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs similarity index 71% rename from test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentResourceDefinition.cs rename to test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs index 987e955629..b3e32ba74c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/DepartmentResourceDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs @@ -5,19 +5,21 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion { - public sealed class DepartmentResourceDefinition : ResourceDefinition + public class SoftDeletionResourceDefinition : JsonApiResourceDefinition + where TResource : class, IIdentifiable, ISoftDeletable { private readonly IResourceGraph _resourceGraph; - public DepartmentResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public SoftDeletionResourceDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { _resourceGraph = resourceGraph; } public override FilterExpression OnApplyFilter(FilterExpression existingFilter) { - var resourceContext = _resourceGraph.GetResourceContext(); - var isSoftDeletedAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Department.IsSoftDeleted)); + var resourceContext = _resourceGraph.GetResourceContext(); + var isSoftDeletedAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(ISoftDeletable.IsSoftDeleted)); var isNotSoftDeleted = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(isSoftDeletedAttribute), new LiteralConstantExpression("false")); @@ -26,5 +28,6 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) ? (FilterExpression) isNotSoftDeleted : new LogicalExpression(LogicalOperator.And, new[] {isNotSoftDeleted, existingFilter}); } + } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs index a891b5065d..8e4e52e5b4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs @@ -20,8 +20,8 @@ public SoftDeletionTests(IntegrationTestContext { - services.AddScoped, CompanyResourceDefinition>(); - services.AddScoped, DepartmentResourceDefinition>(); + services.AddScoped, SoftDeletionResourceDefinition>(); + services.AddScoped, SoftDeletionResourceDefinition>(); }); } diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs similarity index 97% rename from test/UnitTests/Builders/ContextGraphBuilder_Tests.cs rename to test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs index fb28bf4227..34535d9d5c 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ResourceGraphBuilder_Tests.cs @@ -46,7 +46,6 @@ public void Can_Build_ResourceGraph_Using_Builder() var nonDbResource = resourceGraph.GetResourceContext("nonDbResources"); Assert.Equal(typeof(DbResource), dbResource.ResourceType); Assert.Equal(typeof(NonDbResource), nonDbResource.ResourceType); - Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceDefinitionType); } [Fact] diff --git a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs index 6fe5da75ab..581aad751f 100644 --- a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs +++ b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace UnitTests.Internal @@ -18,7 +19,7 @@ public void When_http_context_is_unavailable_it_must_fail() var provider = new RequestScopedServiceProvider(new HttpContextAccessor()); // Act - Action action = () => provider.GetService(serviceType); + Action action = () => provider.GetRequiredService(serviceType); // Assert var exception = Assert.Throws(action); diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index e05da73ba0..3985ba327e 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -14,7 +14,7 @@ namespace UnitTests.ResourceHooks public sealed class DiscoveryTests { public class Dummy : Identifiable { } - public sealed class DummyResourceDefinition : ResourceDefinition + public sealed class DummyResourceDefinition : ResourceHooksDefinition { public DummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } @@ -25,7 +25,7 @@ public override void AfterDelete(HashSet resources, ResourcePipeline pipe private IServiceProvider MockProvider(object service) where TResource : class, IIdentifiable { var services = new ServiceCollection(); - services.AddScoped((_) => (ResourceDefinition)service); + services.AddScoped((_) => (ResourceHooksDefinition)service); return services.BuildServiceProvider(); } @@ -40,7 +40,7 @@ public void HookDiscovery_StandardResourceDefinition_CanDiscover() } public class AnotherDummy : Identifiable { } - public abstract class ResourceDefinitionBase : ResourceDefinition where T : class, IIdentifiable + public abstract class ResourceDefinitionBase : ResourceHooksDefinition where T : class, IIdentifiable { protected ResourceDefinitionBase(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } @@ -63,7 +63,7 @@ public void HookDiscovery_InheritanceSubclass_CanDiscover() } public class YetAnotherDummy : Identifiable { } - public sealed class YetAnotherDummyResourceDefinition : ResourceDefinition + public sealed class YetAnotherDummyResourceDefinition : ResourceHooksDefinition { public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } @@ -95,7 +95,7 @@ public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); } - public sealed class GenericDummyResourceDefinition : ResourceDefinition where TResource : class, IIdentifiable + public sealed class GenericDummyResourceDefinition : ResourceHooksDefinition where TResource : class, IIdentifiable { public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 8a1cfa0c24..86498d24a2 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -342,7 +342,7 @@ private void SetupProcessorFactoryForResourceDefinition( ) where TModel : class, IIdentifiable { - processorFactory.Setup(c => c.Get(typeof(ResourceDefinition<>), typeof(TModel))) + processorFactory.Setup(c => c.Get(typeof(ResourceHooksDefinition<>), typeof(TModel))) .Returns(modelResource); processorFactory.Setup(c => c.Get(typeof(IHooksDiscovery<>), typeof(TModel))) diff --git a/test/UnitTests/Services/DefaultResourceService_Tests.cs b/test/UnitTests/Services/DefaultResourceService_Tests.cs index 986db2427e..c9b7e2b28b 100644 --- a/test/UnitTests/Services/DefaultResourceService_Tests.cs +++ b/test/UnitTests/Services/DefaultResourceService_Tests.cs @@ -74,9 +74,9 @@ private JsonApiResourceService GetService() var changeTracker = new ResourceChangeTracker(options, _resourceGraph, new TargetedFields()); var serviceProvider = new ServiceContainer(); var resourceFactory = new ResourceFactory(serviceProvider); - var resourceDefinitionProvider = new ResourceDefinitionProvider(_resourceGraph, new TestScopedServiceProvider(serviceProvider)); + var resourceDefinitionAccessor = new Mock().Object; var paginationContext = new PaginationContext(); - var composer = new QueryLayerComposer(new List(), _resourceGraph, resourceDefinitionProvider, options, paginationContext); + var composer = new QueryLayerComposer(new List(), _resourceGraph, resourceDefinitionAccessor, options, paginationContext); var request = new JsonApiRequest { PrimaryResource = _resourceGraph.GetResourceContext(),