From f1e1eb461415354f5863e15b0f949a000af1f0ee Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 16 Sep 2020 16:55:59 +0200 Subject: [PATCH 1/8] Generic resource definitions Made resource definitions a first-class pluggable extensibility point, similar to resource services and repositories. So developers now can derive from JsonApiResourceDefinition, implement IResourceDefinition, or insert base classes in the type hierarchy to share common logic. Such a base class can then be registered as an open generic, so that all resources will use that (unless the container finds a non-generic class, which is a better match) instead of the built-in JsonApiResourceDefinition. To accomplish this, I had to split up ResourceDefinition into two: - JsonApiResourceDefinition is highly pluggable like described above - ResourceHooksDefinition contains just the hook callbacks This way, the recursive resolve logic for resource hooks remains as-is. Since it did not use IResourceDefinitionProvider, that type has been replaced with IResourceDefinitionAccessor, which invokes the requested callbacks directly on the found class. What used to be IResourceDefinition (an intermediate type to invoke callbacks on) no longer exists. --- benchmarks/DependencyFactory.cs | 13 - .../JsonApiSerializerBenchmarks.cs | 5 +- .../Definitions/ArticleDefinition.cs | 2 +- .../Definitions/LockableDefinition.cs | 2 +- .../Definitions/PassportDefinition.cs | 2 +- .../Definitions/TagDefinition.cs | 2 +- .../JsonApiApplicationBuilder.cs | 41 +-- .../Configuration/ResourceContext.cs | 7 - .../Configuration/ResourceGraphBuilder.cs | 3 - .../ServiceCollectionExtensions.cs | 1 - .../Configuration/ServiceDiscoveryFacade.cs | 38 ++- .../Controllers/CoreJsonApiController.cs | 1 - .../Internal/Discovery/HooksDiscovery.cs | 2 +- .../Internal/Execution/HookExecutorHelper.cs | 2 +- .../Internal/Execution/IHookExecutorHelper.cs | 4 +- .../Hooks/Internal/ICreateHookContainer.cs | 6 +- .../Hooks/Internal/ICreateHookExecutor.cs | 12 +- .../Hooks/Internal/IDeleteHookContainer.cs | 2 +- .../Hooks/Internal/IDeleteHookExecutor.cs | 12 +- .../Hooks/Internal/IOnReturnHookExecutor.cs | 2 +- .../Hooks/Internal/IReadHookExecutor.cs | 7 +- .../Hooks/Internal/IResourceHookContainer.cs | 2 +- .../Hooks/Internal/IResourceHookExecutor.cs | 2 +- .../Hooks/Internal/IUpdateHookContainer.cs | 8 +- .../Hooks/Internal/IUpdateHookExecutor.cs | 16 +- .../Expressions/QueryableHandlerExpression.cs | 2 +- .../Queries/Internal/QueryLayerComposer.cs | 46 +--- ...ourceDefinitionQueryableParameterReader.cs | 2 +- ...ourceDefinitionQueryableParameterReader.cs | 9 +- .../Resources/IResourceDefinition.cs | 105 +++++++- .../Resources/IResourceDefinitionAccessor.cs | 44 ++++ .../Resources/IResourceDefinitionProvider.cs | 17 -- .../Resources/JsonApiResourceDefinition.cs | 121 +++++++++ .../Resources/QueryStringParameterHandlers.cs | 15 ++ .../Resources/ResourceDefinition.cs | 243 ------------------ .../Resources/ResourceDefinitionAccessor.cs | 97 +++++++ .../Resources/ResourceDefinitionProvider.cs | 27 -- .../Resources/ResourceFactory.cs | 1 - .../Resources/ResourceHooksDefinition.cs | 72 ++++++ .../Serialization/Building/MetaBuilder.cs | 2 +- .../Serialization/FieldsToSerialize.cs | 28 +- .../Serialization/IRequestMeta.cs | 2 +- .../ServiceDiscoveryFacadeTests.cs | 34 ++- .../Acceptance/Spec/DocumentTests/Meta.cs | 2 +- .../CallableResourceDefinition.cs | 6 +- .../ResourceDefinitionQueryCallbackTests.cs | 2 +- .../IntegrationTests/SoftDeletion/Company.cs | 2 +- .../SoftDeletion/CompanyResourceDefinition.cs | 30 --- .../SoftDeletion/Department.cs | 2 +- .../SoftDeletion/ISoftDeletable.cs | 7 + ...n.cs => SoftDeletionResourceDefinition.cs} | 11 +- .../SoftDeletion/SoftDeletionTests.cs | 4 +- ...Tests.cs => ResourceGraphBuilder_Tests.cs} | 1 - .../RequestScopedServiceProviderTests.cs | 3 +- .../UnitTests/ResourceHooks/DiscoveryTests.cs | 10 +- .../ResourceHooks/ResourceHooksTestsSetup.cs | 2 +- .../Services/DefaultResourceService_Tests.cs | 4 +- 57 files changed, 636 insertions(+), 511 deletions(-) create mode 100644 src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs delete mode 100644 src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs create mode 100644 src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs create mode 100644 src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs delete mode 100644 src/JsonApiDotNetCore/Resources/ResourceDefinition.cs create mode 100644 src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs delete mode 100644 src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs create mode 100644 src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs delete mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/CompanyResourceDefinition.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/ISoftDeletable.cs rename test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/{DepartmentResourceDefinition.cs => SoftDeletionResourceDefinition.cs} (71%) rename test/UnitTests/Builders/{ContextGraphBuilder_Tests.cs => ResourceGraphBuilder_Tests.cs} (97%) 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/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs index b28817b6f0..fd75f28797 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class ArticleDefinition : ResourceDefinition
+ public class ArticleDefinition : ResourceHooksDefinition
{ public ArticleDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs index 02f66eafaf..6f629c7398 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCoreExample.Definitions { - public abstract class LockableDefinition : ResourceDefinition where T : class, IIsLockable, IIdentifiable + public abstract class LockableDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable { protected LockableDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs index 84c1e8ec95..f4932a924f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class PassportDefinition : ResourceDefinition + public class PassportDefinition : ResourceHooksDefinition { public PassportDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs index bcc423c1d5..ebf2602372 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreExample.Definitions { - public class TagDefinition : ResourceDefinition + public class TagDefinition : ResourceHooksDefinition { public TagDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index d26b72dd69..327d07c20e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -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..1f1e5cf98e 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 { diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 278f0a104d..97afd3db6d 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -49,6 +49,11 @@ 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; @@ -96,7 +101,7 @@ internal void DiscoverResources() foreach (var descriptor in resourceDescriptors) { - AddResource(assembly, descriptor); + AddResource(descriptor); } } } @@ -111,9 +116,10 @@ internal void DiscoverInjectables() foreach (var descriptor in resourceDescriptors) { - AddResourceDefinition(assembly, descriptor); AddServices(assembly, descriptor); AddRepositories(assembly, descriptor); + AddResourceDefinitions(assembly, descriptor); + AddResourceHookDefinitions(assembly, descriptor); } } } @@ -128,26 +134,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 +161,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/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index a192e103b0..7c21f801c3 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -1,18 +1,117 @@ 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(); } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs new file mode 100644 index 0000000000..c04d20265d --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -0,0 +1,44 @@ +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); + } +} 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..cc87c0cbae --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -0,0 +1,121 @@ +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 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)> + { + } + } +} 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..225ae57158 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.Resources +{ + /// + public sealed 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; + } + + private 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..76a07d6881 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -17,7 +17,7 @@ public class MetaBuilder : IMetaBuilder where TResource : private readonly IHasMeta _resourceMeta; public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IRequestMeta requestMeta = null, - ResourceDefinition resourceDefinition = null) + ResourceHooksDefinition resourceDefinition = null) { _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); _options = options ?? throw new ArgumentNullException(nameof(options)); 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/IRequestMeta.cs index 6fa34c0d9a..ee2d2a9ca7 100644 --- a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Serialization { /// /// Service to add global top-level metadata to a . - /// Use on + /// Use on /// to specify top-level metadata per resource type. /// public interface IRequestMeta diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 23063e4932..ea56b0c3e1 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -28,11 +28,10 @@ public ServiceDiscoveryFacadeTests() 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(new LoggerFactory()); - _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -42,7 +41,6 @@ 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); } @@ -124,8 +122,24 @@ public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_T // 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, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); + + // Act + facade.DiscoverInjectables(); + + // Assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } + public sealed class TestModel : Identifiable { } public class TestModelService : JsonApiResourceService @@ -148,20 +162,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/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index 65ce8d8f67..29384a08f9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -180,7 +180,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>() as IHasMeta).GetMeta(); // Act var response = await client.SendAsync(request); 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(), From 94390ee53f2a09ebac29182f78558c17dd62f72d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 16 Sep 2020 17:43:05 +0200 Subject: [PATCH 2/8] Only scan for hooks when enabled in options Replaced IHasMeta with IResourceDefinition method --- ...efinition.cs => ArticleHooksDefinition.cs} | 4 +-- ...finition.cs => LockableHooksDefinition.cs} | 4 +-- ...finition.cs => PassportHooksDefinition.cs} | 4 +-- .../Definitions/PersonDefinition.cs | 31 +++++++++---------- .../Definitions/PersonHooksDefinition.cs | 24 ++++++++++++++ ...TagDefinition.cs => TagHooksDefinition.cs} | 4 +-- ...doDefinition.cs => TodoHooksDefinition.cs} | 4 +-- .../JsonApiApplicationBuilder.cs | 4 +-- .../ServiceCollectionExtensions.cs | 2 +- .../Configuration/ServiceDiscoveryFacade.cs | 10 ++++-- src/JsonApiDotNetCore/Resources/IHasMeta.cs | 12 ------- .../Resources/IResourceDefinition.cs | 5 +++ .../Resources/IResourceDefinitionAccessor.cs | 5 +++ .../Resources/JsonApiResourceDefinition.cs | 8 ++--- .../Resources/ResourceDefinitionAccessor.cs | 9 ++++++ .../Serialization/Building/MetaBuilder.cs | 17 +++++----- .../ServiceDiscoveryFacadeTests.cs | 18 +++++------ .../Acceptance/SerializationTests.cs | 12 ++++--- .../Acceptance/Spec/DocumentTests/Meta.cs | 3 +- .../Spec/FetchingRelationshipsTests.cs | 12 ++++--- 20 files changed, 115 insertions(+), 77 deletions(-) rename src/Examples/JsonApiDotNetCoreExample/Definitions/{ArticleDefinition.cs => ArticleHooksDefinition.cs} (84%) rename src/Examples/JsonApiDotNetCoreExample/Definitions/{LockableDefinition.cs => LockableHooksDefinition.cs} (78%) rename src/Examples/JsonApiDotNetCoreExample/Definitions/{PassportDefinition.cs => PassportHooksDefinition.cs} (90%) create mode 100644 src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs rename src/Examples/JsonApiDotNetCoreExample/Definitions/{TagDefinition.cs => TagHooksDefinition.cs} (81%) rename src/Examples/JsonApiDotNetCoreExample/Definitions/{TodoDefinition.cs => TodoHooksDefinition.cs} (86%) delete mode 100644 src/JsonApiDotNetCore/Resources/IHasMeta.cs 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 fd75f28797..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 : ResourceHooksDefinition
+ 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 6f629c7398..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 : ResourceHooksDefinition 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 f4932a924f..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 : ResourceHooksDefinition + 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 ebf2602372..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 : ResourceHooksDefinition + 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 327d07c20e..7c5f558df6 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -108,13 +108,13 @@ public void ConfigureMvc() /// public void DiscoverInjectables() { - _serviceDiscoveryFacade.DiscoverInjectables(); + _serviceDiscoveryFacade.DiscoverInjectables(_options.EnableResourceHooks); } /// /// Registers the remaining internals. /// - public void ConfigureServices(Type dbContextType) + public void ConfigureServiceContainer(Type dbContextType) { if (dbContextType != null) { diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 1f1e5cf98e..55e1b52028 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -56,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 97afd3db6d..add5bb92c3 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -105,8 +105,8 @@ internal void DiscoverResources() } } } - - internal void DiscoverInjectables() + + internal void DiscoverInjectables(bool enableResourceHooks) { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { @@ -119,7 +119,11 @@ internal void DiscoverInjectables() AddServices(assembly, descriptor); AddRepositories(assembly, descriptor); AddResourceDefinitions(assembly, descriptor); - AddResourceHookDefinitions(assembly, descriptor); + + if (enableResourceHooks) + { + AddResourceHookDefinitions(assembly, descriptor); + } } } } 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 7c21f801c3..ab246ceb1f 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -113,5 +113,10 @@ public interface IResourceDefinition /// ]]> /// 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 index c04d20265d..7dac3e616c 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -40,5 +40,10 @@ public interface IResourceDefinitionAccessor /// 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/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index cc87c0cbae..37c9d6e761 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -102,12 +102,10 @@ public virtual QueryStringParameterHandlers OnRegisterQueryableHandle return null; } - public object GetQueryableHandlerForQueryStringParameter(string parameterName) + /// + public virtual IReadOnlyDictionary GetMeta() { - if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); - - var handlers = OnRegisterQueryableHandlersForQueryStringParameters(); - return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; + return null; } /// diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 225ae57158..117eb119d4 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -75,6 +75,15 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri 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(); + } + private object GetResourceDefinition(Type resourceType) { var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index 76a07d6881..6eb3d92095 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 IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IRequestMeta _requestMeta; - private readonly IHasMeta _resourceMeta; - public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IRequestMeta requestMeta = null, - ResourceHooksDefinition resourceDefinition = null) + private Dictionary _meta = new Dictionary(); + + public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor, IRequestMeta requestMeta = null) { _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); _options = options ?? throw new ArgumentNullException(nameof(options)); + _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); _requestMeta = requestMeta; - _resourceMeta = resourceDefinition as IHasMeta; } /// @@ -59,9 +59,12 @@ public IDictionary GetMeta() Add(_requestMeta.GetMeta()); } - if (_resourceMeta != null) + // TODO: This looks wrong. We should be adding resource-level meta to each individual resource, instead of once at the top. + + var resourceMeta = _resourceDefinitionAccessor.GetMeta(typeof(TResource)); + if (resourceMeta != null) { - Add(_resourceMeta.GetMeta()); + Add(resourceMeta); } return _meta.Any() ? _meta : null; diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index ea56b0c3e1..3a73a87ca1 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -69,7 +69,7 @@ public void DiscoverResources_Adds_Resources_From_Current_Assembly_To_Graph() // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act facade.DiscoverResources(); @@ -87,8 +87,8 @@ public void DiscoverInjectables_Adds_Resource_Services_From_Current_Assembly_To_ facade.AddCurrentAssembly(); // Act - facade.DiscoverInjectables(); - + facade.DiscoverInjectables(false); + // Assert var services = _services.BuildServiceProvider(); var service = services.GetService>(); @@ -101,9 +101,9 @@ public void DiscoverInjectables_Adds_Resource_Repositories_From_Current_Assembly // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act - facade.DiscoverInjectables(); + facade.DiscoverInjectables(false); // Assert var services = _services.BuildServiceProvider(); @@ -116,9 +116,9 @@ public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_T // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act - facade.DiscoverInjectables(); + facade.DiscoverInjectables(false); // Assert var services = _services.BuildServiceProvider(); @@ -131,9 +131,9 @@ public void AddCurrentAssembly_Adds_Resource_Hooks_Definitions_From_Current_Asse // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); facade.AddCurrentAssembly(); - + // Act - facade.DiscoverInjectables(); + facade.DiscoverInjectables(true); // Assert var services = _services.BuildServiceProvider(); 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 29384a08f9..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"": { From 77acd599e552bc4f9e0c0e9a385d5199c7893580 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 17 Sep 2020 11:20:59 +0200 Subject: [PATCH 3/8] Fix broken cibuild --- src/JsonApiDotNetCore/Serialization/IRequestMeta.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs b/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs index ee2d2a9ca7..5af4fa7836 100644 --- a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs @@ -6,8 +6,7 @@ namespace JsonApiDotNetCore.Serialization { /// /// Service to add global top-level metadata to a . - /// Use on - /// to specify top-level metadata per resource type. + /// Use to specify top-level metadata per resource type. /// public interface IRequestMeta { From f196f108324e332db1472426741330fe1afb19fe Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 17 Sep 2020 13:35:49 +0200 Subject: [PATCH 4/8] Review feedback --- .../JsonApiApplicationBuilder.cs | 4 +-- .../Configuration/ServiceDiscoveryFacade.cs | 8 +++-- .../ServiceDiscoveryFacadeTests.cs | 29 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 7c5f558df6..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); } /// @@ -108,7 +108,7 @@ public void ConfigureMvc() /// public void DiscoverInjectables() { - _serviceDiscoveryFacade.DiscoverInjectables(_options.EnableResourceHooks); + _serviceDiscoveryFacade.DiscoverInjectables(); } /// diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index add5bb92c3..c01111f3b4 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -57,10 +57,11 @@ public class ServiceDiscoveryFacade 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) { @@ -70,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)); } /// @@ -106,7 +108,7 @@ internal void DiscoverResources() } } - internal void DiscoverInjectables(bool enableResourceHooks) + internal void DiscoverInjectables() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { @@ -120,7 +122,7 @@ internal void DiscoverInjectables(bool enableResourceHooks) AddRepositories(assembly, descriptor); AddResourceDefinitions(assembly, descriptor); - if (enableResourceHooks) + if (_options.EnableResourceHooks) { AddResourceHookDefinitions(assembly, descriptor); } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 3a73a87ca1..c370354a60 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -20,17 +20,16 @@ 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); _services.AddScoped(_ => dbResolverMock.Object); - _services.AddSingleton(options); + _services.AddSingleton(_options); _services.AddSingleton(new LoggerFactory()); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -42,14 +41,14 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => 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 @@ -67,7 +66,7 @@ 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 @@ -83,11 +82,11 @@ 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(false); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); @@ -99,11 +98,11 @@ 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(false); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); @@ -114,11 +113,11 @@ 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(false); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); @@ -129,11 +128,13 @@ public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_T public void AddCurrentAssembly_Adds_Resource_Hooks_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(); + _options.EnableResourceHooks = true; + // Act - facade.DiscoverInjectables(true); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); From 88868655146b2ebc8194ad97603b14a366b0b13a Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 17 Sep 2020 16:11:07 +0200 Subject: [PATCH 5/8] Made ResourceDefinitionAccessor.GetResourceDefinition protected --- src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 117eb119d4..7318e82236 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Resources { /// - public sealed class ResourceDefinitionAccessor : IResourceDefinitionAccessor + public class ResourceDefinitionAccessor : IResourceDefinitionAccessor { private readonly IResourceContextProvider _resourceContextProvider; private readonly IServiceProvider _serviceProvider; @@ -84,7 +84,7 @@ public IReadOnlyDictionary GetMeta(Type resourceType) return resourceDefinition.GetMeta(); } - private object GetResourceDefinition(Type resourceType) + protected object GetResourceDefinition(Type resourceType) { var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); From 7f09d15cc263165b799ae41f1fa644145a3d7f7d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 17 Sep 2020 17:49:57 +0200 Subject: [PATCH 6/8] Renamed IRequestMeta to IResponseMeta; updated documentation --- docs/usage/meta.md | 50 ++++++++++++------- .../Serialization/Building/MetaBuilder.cs | 12 ++--- .../{IRequestMeta.cs => IResponseMeta.cs} | 4 +- .../Extensibility/RequestMetaTests.cs | 6 +-- 4 files changed, 42 insertions(+), 30 deletions(-) rename src/JsonApiDotNetCore/Serialization/{IRequestMeta.cs => IResponseMeta.cs} (75%) diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 830065bfa3..b597d841fb 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 custom 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 injecting a service that implements `IResponseMeta`. +This is useful if you need access to other injected 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/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index 6eb3d92095..b9837ac7ba 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -13,16 +13,16 @@ public class MetaBuilder : IMetaBuilder where TResource : private readonly IPaginationContext _paginationContext; private readonly IJsonApiOptions _options; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly IRequestMeta _requestMeta; + private readonly IResponseMeta _responseMeta; private Dictionary _meta = new Dictionary(); - public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor, IRequestMeta requestMeta = 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)); _resourceDefinitionAccessor = resourceDefinitionAccessor ?? throw new ArgumentNullException(nameof(resourceDefinitionAccessor)); - _requestMeta = requestMeta; + _responseMeta = responseMeta; } /// @@ -54,13 +54,11 @@ public IDictionary GetMeta() _meta.Add(key, _paginationContext.TotalResourceCount); } - if (_requestMeta != null) + if (_responseMeta != null) { - Add(_requestMeta.GetMeta()); + Add(_responseMeta.GetMeta()); } - // TODO: This looks wrong. We should be adding resource-level meta to each individual resource, instead of once at the top. - var resourceMeta = _resourceDefinitionAccessor.GetMeta(typeof(TResource)); if (resourceMeta != null) { diff --git a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs similarity index 75% rename from src/JsonApiDotNetCore/Serialization/IRequestMeta.cs rename to src/JsonApiDotNetCore/Serialization/IResponseMeta.cs index 5af4fa7836..2c423ed74e 100644 --- a/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs @@ -5,10 +5,10 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Service to add global top-level metadata to a . + /// 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/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() { From c7b1edeab9df8ac15a9b21a925966eb5977cc6f6 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 17 Sep 2020 17:52:31 +0200 Subject: [PATCH 7/8] quotes --- docs/usage/meta.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/meta.md b/docs/usage/meta.md index b597d841fb..80d8dd4f0a 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -41,7 +41,7 @@ public class ResponseMetaService : IResponseMeta ## Resource Meta -Resource-specific metadata can be added by implementing `IResourceDefinition.GetMeta` (or overriding it on JsonApiResourceDefinition): +Resource-specific metadata can be added by implementing `IResourceDefinition.GetMeta` (or overriding it on `JsonApiResourceDefinition`): ```c# public class PersonDefinition : JsonApiResourceDefinition From 623b5f613244cbccc10755bc72c8d4bd71300d02 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 18 Sep 2020 14:02:15 +0200 Subject: [PATCH 8/8] Review feedback --- docs/usage/meta.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 80d8dd4f0a..2cefca3858 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -1,11 +1,11 @@ # Metadata -Top-level custom 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. +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. ## Global Meta -Global metadata can be added by injecting a service that implements `IResponseMeta`. -This is useful if you need access to other injected services to build the meta object. +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 ResponseMetaService : IResponseMeta