diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 467603226e..6e3bcf2b61 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -38,7 +38,9 @@ public JsonApiDeserializerBenchmarks() IResourceGraph resourceGraph = _dependencyFactory.CreateResourceGraph(options); var serviceContainer = new ServiceContainer(); - serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new ResourceDefinitionAccessor(resourceGraph, serviceContainer)); + var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, serviceContainer); + + serviceContainer.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor); serviceContainer.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph)); var targetedFields = new TargetedFields(); @@ -46,7 +48,8 @@ public JsonApiDeserializerBenchmarks() var resourceFactory = new ResourceFactory(serviceContainer); var httpContextAccessor = new HttpContextAccessor(); - _jsonApiDeserializer = new RequestDeserializer(resourceGraph, resourceFactory, targetedFields, httpContextAccessor, request, options); + _jsonApiDeserializer = new RequestDeserializer(resourceGraph, resourceFactory, targetedFields, httpContextAccessor, request, options, + resourceDefinitionAccessor); } [Benchmark] diff --git a/docs/internals/queries.md b/docs/internals/queries.md index 9268ff24dc..b5e5c2cf19 100644 --- a/docs/internals/queries.md +++ b/docs/internals/queries.md @@ -18,7 +18,7 @@ Processing a request involves the following steps: - The readers also implement `IQueryConstraintProvider`, which exposes expressions through `ExpressionInScope` objects. - `QueryLayerComposer` (used from `JsonApiResourceService`) collects all query constraints. - It combines them with default options and `IResourceDefinition` overrides and composes a tree of `QueryLayer` objects. - - It lifts the tree for nested endpoints like /blogs/1/articles and rewrites includes. + - It lifts the tree for secondary endpoints like /blogs/1/articles and rewrites includes. - `JsonApiResourceService` contains no more usage of `IQueryable`. - `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees. `QueryBuilder` depends on `QueryClauseBuilder` implementations that visit the tree nodes, transforming them to `System.Linq.Expression` equivalents. diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md index adf156ba1f..5f0ca406be 100644 --- a/docs/usage/extensibility/resource-definitions.md +++ b/docs/usage/extensibility/resource-definitions.md @@ -202,8 +202,8 @@ public class EmployeeDefinition : JsonApiResourceDefinition { } - public override IReadOnlyCollection OnApplyIncludes( - IReadOnlyCollection existingIncludes) + public override IImmutableList OnApplyIncludes( + IImmutableList existingIncludes) { if (existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Employee.Manager))) diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md index 2e4dcd43dc..2c157ae432 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -18,9 +18,10 @@ public class TodoItemService : JsonApiResourceService public TodoItemService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker) + IResourceChangeTracker resourceChangeTracker, + IResourceDefinitionAccessor resourceDefinitionAccessor) : base(repositoryAccessor, queryLayerComposer, paginationContext, options, - loggerFactory, request, resourceChangeTracker) + loggerFactory, request, resourceChangeTracker, resourceDefinitionAccessor) { _notificationService = notificationService; } diff --git a/docs/usage/reading/including-relationships.md b/docs/usage/reading/including-relationships.md index dba43fcb9f..f22d2321aa 100644 --- a/docs/usage/reading/including-relationships.md +++ b/docs/usage/reading/including-relationships.md @@ -62,7 +62,7 @@ which is equivalent to: GET /api/articles?include=author&include=author.livingAddress&include=author.livingAddress.country ``` -This can be used on nested endpoints too: +This can be used on secondary endpoints too: ```http GET /api/blogs/1/articles?include=author.livingAddress.country diff --git a/docs/usage/reading/pagination.md b/docs/usage/reading/pagination.md index 77772288b3..ea4e30e621 100644 --- a/docs/usage/reading/pagination.md +++ b/docs/usage/reading/pagination.md @@ -6,9 +6,7 @@ Resources can be paginated. This request would fetch the second page of 10 artic GET /articles?page[size]=10&page[number]=2 HTTP/1.1 ``` -## Nesting - -Pagination can be used on nested endpoints, such as: +Pagination can be used on secondary endpoints, such as: ```http GET /blogs/1/articles?page[number]=2 HTTP/1.1 diff --git a/docs/usage/reading/sorting.md b/docs/usage/reading/sorting.md index dfadc325fa..707720e8d9 100644 --- a/docs/usage/reading/sorting.md +++ b/docs/usage/reading/sorting.md @@ -34,9 +34,9 @@ GET /api/blogs?sort=count(articles) HTTP/1.1 This sorts the list of blogs by their number of articles. -## Nesting +## Secondary endpoints -Sorting can be used on nested endpoints, such as: +Sorting can be used on secondary endpoints, such as: ```http GET /api/blogs/1/articles?sort=caption HTTP/1.1 diff --git a/docs/usage/reading/sparse-fieldset-selection.md b/docs/usage/reading/sparse-fieldset-selection.md index 7d90bf9d26..5c08bc6ae4 100644 --- a/docs/usage/reading/sparse-fieldset-selection.md +++ b/docs/usage/reading/sparse-fieldset-selection.md @@ -2,15 +2,15 @@ As an alternative to returning all fields (attributes and relationships) from a resource, the `fields[]` query string parameter can be used to select a subset. Put the resource type to apply the fieldset on between the brackets. -This can be used on the resource being requested, as well as on nested endpoints and/or included resources. +This can be used on primary and secondary endpoints. The selection is applied on both primary and included resources. -Top-level example: +Primary endpoint example: ```http GET /articles?fields[articles]=title,body,comments HTTP/1.1 ``` -Nested endpoint example: +Secondary endpoint example: ```http GET /api/blogs/1/articles?fields[articles]=title,body,comments HTTP/1.1 diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs index 43b57b98e5..927bed7f57 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemDefinition.cs @@ -36,13 +36,13 @@ private SortExpression GetDefaultSortOrder() }); } - public override Task OnWritingAsync(TodoItem resource, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { resource.CreatedAt = _systemClock.UtcNow; } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { resource.LastModifiedAt = _systemClock.UtcNow; } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs index 13fb0bbe48..5b07948005 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -14,8 +14,9 @@ public sealed class DbContextARepository : EntityFrameworkCoreReposit where TResource : class, IIdentifiable { public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs index 0b65caa945..afa7ed4bde 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -14,8 +14,9 @@ public sealed class DbContextBRepository : EntityFrameworkCoreReposit where TResource : class, IIdentifiable { public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs index f934e7dc9b..df227d12d9 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs @@ -73,8 +73,7 @@ public async Task CreateAsync(WorkItem resource, CancellationToken can return workItems.Single(); } - public Task AddToToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, - CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(int leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -84,7 +83,7 @@ public Task UpdateAsync(int id, WorkItem resource, CancellationToken c throw new NotImplementedException(); } - public Task SetRelationshipAsync(int primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(int leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -99,7 +98,7 @@ public async Task DeleteAsync(int id, CancellationToken cancellationToken) }, cancellationToken: cancellationToken))); } - public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, + public Task RemoveFromToManyRelationshipAsync(int leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index 79ea1bf6ba..6bd8e7a57e 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -56,7 +56,7 @@ public void Validate(IEnumerable operations) private void ValidateOperation(OperationContainer operation) { - if (operation.Kind == OperationKind.CreateResource) + if (operation.Kind == WriteOperationKind.CreateResource) { DeclareLocalId(operation.Resource); } @@ -70,7 +70,7 @@ private void ValidateOperation(OperationContainer operation) AssertLocalIdIsAssigned(secondaryResource); } - if (operation.Kind == OperationKind.CreateResource) + if (operation.Kind == WriteOperationKind.CreateResource) { AssignLocalId(operation); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index f387316720..b6d7bae21d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -44,37 +44,37 @@ protected virtual IOperationProcessor ResolveProcessor(OperationContainer operat return (IOperationProcessor)_serviceProvider.GetRequiredService(processorType); } - private static Type GetProcessorInterface(OperationKind kind) + private static Type GetProcessorInterface(WriteOperationKind writeOperation) { - switch (kind) + switch (writeOperation) { - case OperationKind.CreateResource: + case WriteOperationKind.CreateResource: { return typeof(ICreateProcessor<,>); } - case OperationKind.UpdateResource: + case WriteOperationKind.UpdateResource: { return typeof(IUpdateProcessor<,>); } - case OperationKind.DeleteResource: + case WriteOperationKind.DeleteResource: { return typeof(IDeleteProcessor<,>); } - case OperationKind.SetRelationship: + case WriteOperationKind.SetRelationship: { return typeof(ISetRelationshipProcessor<,>); } - case OperationKind.AddToRelationship: + case WriteOperationKind.AddToRelationship: { return typeof(IAddToRelationshipProcessor<,>); } - case OperationKind.RemoveFromRelationship: + case WriteOperationKind.RemoveFromRelationship: { return typeof(IRemoveFromRelationshipProcessor<,>); } default: { - throw new NotSupportedException($"Unknown operation kind '{kind}'."); + throw new NotSupportedException($"Unknown write operation kind '{writeOperation}'."); } } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 1fb60d985e..ffb2e19efc 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -118,7 +118,7 @@ protected virtual async Task ProcessOperationAsync(Operation protected void TrackLocalIdsForOperation(OperationContainer operation) { - if (operation.Kind == OperationKind.CreateResource) + if (operation.Kind == WriteOperationKind.CreateResource) { DeclareLocalId(operation.Resource); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs index 127316556d..775e896ebc 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs @@ -26,10 +26,10 @@ public virtual async Task ProcessAsync(OperationContainer op { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId)operation.Resource.GetTypedId(); - ISet secondaryResourceIds = operation.GetSecondaryResources(); + var leftId = (TId)operation.Resource.GetTypedId(); + ISet rightResourceIds = operation.GetSecondaryResources(); - await _service.AddToToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, secondaryResourceIds, cancellationToken); + await _service.AddToToManyRelationshipAsync(leftId, operation.Request.Relationship.PublicName, rightResourceIds, cancellationToken); return null; } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs index b41169510d..74197f417f 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs @@ -26,10 +26,10 @@ public virtual async Task ProcessAsync(OperationContainer op { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId)operation.Resource.GetTypedId(); - ISet secondaryResourceIds = operation.GetSecondaryResources(); + var leftId = (TId)operation.Resource.GetTypedId(); + ISet rightResourceIds = operation.GetSecondaryResources(); - await _service.RemoveFromToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, secondaryResourceIds, cancellationToken); + await _service.RemoveFromToManyRelationshipAsync(leftId, operation.Request.Relationship.PublicName, rightResourceIds, cancellationToken); return null; } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs index 8147435de6..92bd69942e 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs @@ -29,10 +29,10 @@ public virtual async Task ProcessAsync(OperationContainer op { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId)operation.Resource.GetTypedId(); + var leftId = (TId)operation.Resource.GetTypedId(); object rightValue = GetRelationshipRightValue(operation); - await _service.SetRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, rightValue, cancellationToken); + await _service.SetRelationshipAsync(leftId, operation.Request.Relationship.PublicName, rightValue, cancellationToken); return null; } diff --git a/src/JsonApiDotNetCore/CollectionConverter.cs b/src/JsonApiDotNetCore/CollectionConverter.cs index 212f2a6f3b..1f403b4ccd 100644 --- a/src/JsonApiDotNetCore/CollectionConverter.cs +++ b/src/JsonApiDotNetCore/CollectionConverter.cs @@ -8,13 +8,14 @@ namespace JsonApiDotNetCore { internal sealed class CollectionConverter { - private static readonly Type[] HashSetCompatibleCollectionTypes = + private static readonly ISet HashSetCompatibleCollectionTypes = new HashSet { typeof(HashSet<>), - typeof(ICollection<>), typeof(ISet<>), - typeof(IEnumerable<>), - typeof(IReadOnlyCollection<>) + typeof(IReadOnlySet<>), + typeof(ICollection<>), + typeof(IReadOnlyCollection<>), + typeof(IEnumerable<>) }; /// @@ -45,19 +46,18 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType /// /// Returns a compatible collection type that can be instantiated, for example IList{Article} -> List{Article} or ISet{Article} -> HashSet{Article} /// - public Type ToConcreteCollectionType(Type collectionType) + private Type ToConcreteCollectionType(Type collectionType) { if (collectionType.IsInterface && collectionType.IsGenericType) { - Type genericTypeDefinition = collectionType.GetGenericTypeDefinition(); + Type openCollectionType = collectionType.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(ISet<>) || - genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(IReadOnlyCollection<>)) + if (HashSetCompatibleCollectionTypes.Contains(openCollectionType)) { return typeof(HashSet<>).MakeGenericType(collectionType.GenericTypeArguments[0]); } - if (genericTypeDefinition == typeof(IList<>) || genericTypeDefinition == typeof(IReadOnlyList<>)) + if (openCollectionType == typeof(IList<>) || openCollectionType == typeof(IReadOnlyList<>)) { return typeof(List<>).MakeGenericType(collectionType.GenericTypeArguments[0]); } diff --git a/src/JsonApiDotNetCore/CollectionExtensions.cs b/src/JsonApiDotNetCore/CollectionExtensions.cs index 68997f60b3..41596c7b22 100644 --- a/src/JsonApiDotNetCore/CollectionExtensions.cs +++ b/src/JsonApiDotNetCore/CollectionExtensions.cs @@ -19,7 +19,7 @@ public static bool IsNullOrEmpty(this IEnumerable source) return !source.Any(); } - public static int FindIndex(this IList source, Predicate match) + public static int FindIndex(this IReadOnlyList source, Predicate match) { ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(match, nameof(match)); @@ -34,5 +34,41 @@ public static int FindIndex(this IList source, Predicate match) return -1; } + + public static bool DictionaryEqual(this IReadOnlyDictionary first, IReadOnlyDictionary second, + IEqualityComparer valueComparer = null) + { + if (first == second) + { + return true; + } + + if (first == null || second == null) + { + return false; + } + + if (first.Count != second.Count) + { + return false; + } + + IEqualityComparer effectiveValueComparer = valueComparer ?? EqualityComparer.Default; + + foreach ((TKey firstKey, TValue firstValue) in first) + { + if (!second.TryGetValue(firstKey, out TValue secondValue)) + { + return false; + } + + if (!effectiveValueComparer.Equals(firstValue, secondValue)) + { + return false; + } + } + + return true; + } } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs index a452bf30c9..62f4e729a6 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs @@ -10,14 +10,14 @@ namespace JsonApiDotNetCore.Configuration public interface IResourceContextProvider { /// - /// Gets all registered resource contexts. + /// Gets the metadata for all registered resources. /// - IReadOnlyCollection GetResourceContexts(); + IReadOnlySet GetResourceContexts(); /// - /// Gets the resource metadata for the specified exposed resource name. + /// Gets the resource metadata for the resource that is publicly exposed by the specified name. /// - ResourceContext GetResourceContext(string resourceName); + ResourceContext GetResourceContext(string publicName); /// /// Gets the resource metadata for the specified resource type. diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index 7ccc306716..f4d57f135c 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -41,7 +41,7 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt var httpContextAccessor = _serviceProvider.GetRequiredService(); - if (httpContextAccessor.HttpContext!.Request.Method == HttpMethods.Patch || request.OperationKind == OperationKind.UpdateResource) + if (httpContextAccessor.HttpContext!.Request.Method == HttpMethods.Patch || request.WriteOperation == WriteOperationKind.UpdateResource) { var targetedFields = _serviceProvider.GetRequiredService(); return IsFieldTargeted(entry, targetedFields); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index 4ba2975a10..5a18a8c2f1 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -10,39 +10,39 @@ namespace JsonApiDotNetCore.Configuration /// Provides metadata for a resource, such as its attributes and relationships. /// [PublicAPI] - public class ResourceContext + public sealed class ResourceContext { private IReadOnlyCollection _fields; /// /// The publicly exposed resource name. /// - public string PublicName { get; set; } + public string PublicName { get; } /// /// The CLR type of the resource. /// - public Type ResourceType { get; set; } + public Type ResourceType { get; } /// /// The identity type of the resource. /// - public Type IdentityType { get; set; } + public Type IdentityType { get; } /// /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. /// - public IReadOnlyCollection Attributes { get; set; } + public IReadOnlyCollection Attributes { get; } /// /// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships. /// - public IReadOnlyCollection Relationships { get; set; } + public IReadOnlyCollection Relationships { get; } /// /// Related entities that are not exposed as resource relationships. /// - public IReadOnlyCollection EagerLoads { get; set; } + public IReadOnlyCollection EagerLoads { get; } /// /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. @@ -56,7 +56,7 @@ public class ResourceContext /// /// In the process of building the resource graph, this value is set based on usage. /// - public LinkTypes TopLevelLinks { get; internal set; } = LinkTypes.NotConfigured; + public LinkTypes TopLevelLinks { get; } /// /// Configures which links to show in the object for this resource type. Defaults to @@ -65,7 +65,7 @@ public class ResourceContext /// /// In the process of building the resource graph, this value is set based on usage. /// - public LinkTypes ResourceLinks { get; internal set; } = LinkTypes.NotConfigured; + public LinkTypes ResourceLinks { get; } /// /// Configures which links to show in the object for all relationships of this resource type. @@ -75,11 +75,83 @@ public class ResourceContext /// /// In the process of building the resource graph, this value is set based on usage. /// - public LinkTypes RelationshipLinks { get; internal set; } = LinkTypes.NotConfigured; + public LinkTypes RelationshipLinks { get; } + + public ResourceContext(string publicName, Type resourceType, Type identityType, IReadOnlyCollection attributes, + IReadOnlyCollection relationships, IReadOnlyCollection eagerLoads, + LinkTypes topLevelLinks = LinkTypes.NotConfigured, LinkTypes resourceLinks = LinkTypes.NotConfigured, + LinkTypes relationshipLinks = LinkTypes.NotConfigured) + { + ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + ArgumentGuard.NotNull(identityType, nameof(identityType)); + ArgumentGuard.NotNull(attributes, nameof(attributes)); + ArgumentGuard.NotNull(relationships, nameof(relationships)); + ArgumentGuard.NotNull(eagerLoads, nameof(eagerLoads)); + + PublicName = publicName; + ResourceType = resourceType; + IdentityType = identityType; + Attributes = attributes; + Relationships = relationships; + EagerLoads = eagerLoads; + TopLevelLinks = topLevelLinks; + ResourceLinks = resourceLinks; + RelationshipLinks = relationshipLinks; + } public override string ToString() { return PublicName; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (ResourceContext)obj; + + return PublicName == other.PublicName && ResourceType == other.ResourceType && IdentityType == other.IdentityType && + Attributes.SequenceEqual(other.Attributes) && Relationships.SequenceEqual(other.Relationships) && EagerLoads.SequenceEqual(other.EagerLoads) && + TopLevelLinks == other.TopLevelLinks && ResourceLinks == other.ResourceLinks && RelationshipLinks == other.RelationshipLinks; + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + hashCode.Add(PublicName); + hashCode.Add(ResourceType); + hashCode.Add(IdentityType); + + foreach (AttrAttribute attribute in Attributes) + { + hashCode.Add(attribute); + } + + foreach (RelationshipAttribute relationship in Relationships) + { + hashCode.Add(relationship); + } + + foreach (EagerLoadAttribute eagerLoad in EagerLoads) + { + hashCode.Add(eagerLoad); + } + + hashCode.Add(TopLevelLinks); + hashCode.Add(ResourceLinks); + hashCode.Add(RelationshipLinks); + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 14cb0547cd..26ada5f798 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -14,27 +14,27 @@ namespace JsonApiDotNetCore.Configuration public sealed class ResourceGraph : IResourceGraph { private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); - private readonly IReadOnlyCollection _resources; + private readonly IReadOnlySet _resourceContexts; - public ResourceGraph(IReadOnlyCollection resources) + public ResourceGraph(IReadOnlySet resourceContexts) { - ArgumentGuard.NotNull(resources, nameof(resources)); + ArgumentGuard.NotNull(resourceContexts, nameof(resourceContexts)); - _resources = resources; + _resourceContexts = resourceContexts; } /// - public IReadOnlyCollection GetResourceContexts() + public IReadOnlySet GetResourceContexts() { - return _resources; + return _resourceContexts; } /// - public ResourceContext GetResourceContext(string resourceName) + public ResourceContext GetResourceContext(string publicName) { - ArgumentGuard.NotNullNorEmpty(resourceName, nameof(resourceName)); + ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); - return _resources.SingleOrDefault(resourceContext => resourceContext.PublicName == resourceName); + return _resourceContexts.SingleOrDefault(resourceContext => resourceContext.PublicName == publicName); } /// @@ -43,8 +43,8 @@ public ResourceContext GetResourceContext(Type resourceType) ArgumentGuard.NotNull(resourceType, nameof(resourceType)); return IsLazyLoadingProxyForResourceType(resourceType) - ? _resources.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType.BaseType) - : _resources.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType); + ? _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType.BaseType) + : _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType); } private bool IsLazyLoadingProxyForResourceType(Type resourceType) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 64eca6dc41..24bf42dc24 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -17,7 +17,7 @@ public class ResourceGraphBuilder { private readonly IJsonApiOptions _options; private readonly ILogger _logger; - private readonly List _resources = new(); + private readonly HashSet _resourceContexts = new(); private readonly TypeLocator _typeLocator = new(); public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) @@ -34,20 +34,7 @@ public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactor /// public IResourceGraph Build() { - _resources.ForEach(SetResourceLinksOptions); - return new ResourceGraph(_resources); - } - - private void SetResourceLinksOptions(ResourceContext resourceContext) - { - var attribute = (ResourceLinksAttribute)resourceContext.ResourceType.GetCustomAttribute(typeof(ResourceLinksAttribute)); - - if (attribute != null) - { - resourceContext.RelationshipLinks = attribute.RelationshipLinks; - resourceContext.ResourceLinks = attribute.ResourceLinks; - resourceContext.TopLevelLinks = attribute.TopLevelLinks; - } + return new ResourceGraph(_resourceContexts); } /// @@ -102,7 +89,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - if (_resources.Any(resourceContext => resourceContext.ResourceType == resourceType)) + if (_resourceContexts.Any(resourceContext => resourceContext.ResourceType == resourceType)) { return this; } @@ -113,7 +100,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu Type effectiveIdType = idType ?? _typeLocator.TryGetIdType(resourceType); ResourceContext resourceContext = CreateResourceContext(effectivePublicName, resourceType, effectiveIdType); - _resources.Add(resourceContext); + _resourceContexts.Add(resourceContext); } else { @@ -125,15 +112,16 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) { - return new() - { - PublicName = publicName, - ResourceType = resourceType, - IdentityType = idType, - Attributes = GetAttributes(resourceType), - Relationships = GetRelationships(resourceType), - EagerLoads = GetEagerLoads(resourceType) - }; + IReadOnlyCollection attributes = GetAttributes(resourceType); + IReadOnlyCollection relationships = GetRelationships(resourceType); + IReadOnlyCollection eagerLoads = GetEagerLoads(resourceType); + + var linksAttribute = (ResourceLinksAttribute)resourceType.GetCustomAttribute(typeof(ResourceLinksAttribute)); + + return linksAttribute == null + ? new ResourceContext(publicName, resourceType, idType, attributes, relationships, eagerLoads) + : new ResourceContext(publicName, resourceType, idType, attributes, relationships, eagerLoads, linksAttribute.TopLevelLinks, + linksAttribute.ResourceLinks, linksAttribute.RelationshipLinks); } private IReadOnlyCollection GetAttributes(Type resourceType) diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs index acb62b44d8..1ca196c51b 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Controllers.Annotations /// Used on an ASP.NET Core controller class to indicate which query string parameters are blocked. /// /// { } /// ]]> /// ParameterNames { get; } + public static readonly DisableQueryStringAttribute Empty = new(JsonApiQueryStringParameters.None); + + public IReadOnlySet ParameterNames { get; } /// /// Disables one or more of the builtin query parameters for a controller. /// - public DisableQueryStringAttribute(StandardQueryStringParameters parameters) + public DisableQueryStringAttribute(JsonApiQueryStringParameters parameters) { - var parameterNames = new List(); + var parameterNames = new HashSet(); - foreach (StandardQueryStringParameters value in Enum.GetValues(typeof(StandardQueryStringParameters))) + foreach (JsonApiQueryStringParameters value in Enum.GetValues(typeof(JsonApiQueryStringParameters))) { - if (value != StandardQueryStringParameters.None && value != StandardQueryStringParameters.All && parameters.HasFlag(value)) + if (value != JsonApiQueryStringParameters.None && value != JsonApiQueryStringParameters.All && parameters.HasFlag(value)) { parameterNames.Add(value.ToString()); } @@ -44,16 +45,16 @@ public DisableQueryStringAttribute(StandardQueryStringParameters parameters) /// /// It is allowed to use a comma-separated list of strings to indicate which query parameters should be disabled, because the user may have defined - /// custom query parameters that are not included in the enum. + /// custom query parameters that are not included in the enum. /// public DisableQueryStringAttribute(string parameterNames) { ArgumentGuard.NotNullNorEmpty(parameterNames, nameof(parameterNames)); - ParameterNames = parameterNames.Split(",").ToList(); + ParameterNames = parameterNames.Split(",").ToHashSet(); } - public bool ContainsParameter(StandardQueryStringParameters parameter) + public bool ContainsParameter(JsonApiQueryStringParameters parameter) { string name = parameter.ToString(); return ParameterNames.Contains(name); diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 1386ee76a3..c7bddff7b6 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -211,36 +211,36 @@ public virtual async Task PostAsync([FromBody] TResource resource /// Adds resources to a to-many relationship. Example: POST /articles/1/revisions HTTP/1.1 /// /// - /// The identifier of the primary resource. + /// Identifies the left side of the relationship. /// /// /// The relationship to add resources to. /// - /// + /// /// The set of resources to add to the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { id, relationshipName, - secondaryResourceIds + rightResourceIds }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); if (_addToRelationship == null) { throw new RequestMethodNotAllowedException(HttpMethod.Post); } - await _addToRelationship.AddToToManyRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + await _addToRelationship.AddToToManyRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); return NoContent(); } @@ -279,25 +279,25 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource /// /articles/1/relationships/revisions HTTP/1.1 /// /// - /// The identifier of the primary resource. + /// Identifies the left side of the relationship. /// /// /// The relationship for which to perform a complete replacement. /// - /// + /// /// The resource or set of resources to assign to the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object rightValue, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { id, relationshipName, - secondaryResourceIds + rightValue }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); @@ -307,7 +307,7 @@ public virtual async Task PatchRelationshipAsync(TId id, string r throw new RequestMethodNotAllowedException(HttpMethod.Patch); } - await _setRelationship.SetRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + await _setRelationship.SetRelationshipAsync(id, relationshipName, rightValue, cancellationToken); return NoContent(); } @@ -336,36 +336,36 @@ public virtual async Task DeleteAsync(TId id, CancellationToken c /// Removes resources from a to-many relationship. Example: DELETE /articles/1/relationships/revisions HTTP/1.1 /// /// - /// The identifier of the primary resource. + /// Identifies the left side of the relationship. /// /// /// The relationship to remove resources from. /// - /// + /// /// The set of resources to remove from the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { id, relationshipName, - secondaryResourceIds + rightResourceIds }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); if (_removeFromRelationship == null) { throw new RequestMethodNotAllowedException(HttpMethod.Delete); } - await _removeFromRelationship.RemoveFromToManyRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + await _removeFromRelationship.RemoveFromToManyRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); return NoContent(); } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 3c648dd7b9..64feb432ee 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -132,7 +132,7 @@ protected virtual void ValidateClientGeneratedIds(IEnumerable operat foreach (OperationContainer operation in operations) { - if (operation.Kind == OperationKind.CreateResource || operation.Kind == OperationKind.UpdateResource) + if (operation.Kind == WriteOperationKind.CreateResource || operation.Kind == WriteOperationKind.UpdateResource) { _targetedFields.Attributes = operation.TargetedFields.Attributes; _targetedFields.Relationships = operation.TargetedFields.Relationships; diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs index cfc9399957..f2ed5c938b 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs @@ -40,10 +40,10 @@ public override async Task PostAsync([FromBody] TResource resourc /// [HttpPost("{id}/relationships/{relationshipName}")] - public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { - return await base.PostRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.PostRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); } /// @@ -55,10 +55,10 @@ public override async Task PatchAsync(TId id, [FromBody] TResourc /// [HttpPatch("{id}/relationships/{relationshipName}")] - public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object rightValue, CancellationToken cancellationToken) { - return await base.PatchRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.PatchRelationshipAsync(id, relationshipName, rightValue, cancellationToken); } /// @@ -70,10 +70,10 @@ public override async Task DeleteAsync(TId id, CancellationToken /// [HttpDelete("{id}/relationships/{relationshipName}")] - public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { - return await base.DeleteRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.DeleteRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index a88c276a0b..5fa5557ab7 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -81,10 +81,10 @@ public override async Task PostAsync([FromBody] TResource resourc /// [HttpPost("{id}/relationships/{relationshipName}")] - public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { - return await base.PostRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.PostRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); } /// @@ -96,10 +96,10 @@ public override async Task PatchAsync(TId id, [FromBody] TResourc /// [HttpPatch("{id}/relationships/{relationshipName}")] - public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object rightValue, CancellationToken cancellationToken) { - return await base.PatchRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.PatchRelationshipAsync(id, relationshipName, rightValue, cancellationToken); } /// @@ -111,10 +111,10 @@ public override async Task DeleteAsync(TId id, CancellationToken /// [HttpDelete("{id}/relationships/{relationshipName}")] - public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { - return await base.DeleteRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); + return await base.DeleteRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs index 0a2db9e061..3d3c5b163b 100644 --- a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs @@ -17,7 +17,7 @@ public RequestMethodNotAllowedException(HttpMethod method) : base(new Error(HttpStatusCode.MethodNotAllowed) { Title = "The request method is not allowed.", - Detail = $"Resource does not support {method} requests." + Detail = $"Endpoint does not support {method} requests." }) { Method = method; diff --git a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs index f21dcad2b4..efe5a54f00 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs @@ -16,7 +16,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE if (context.HttpContext.IsJsonApiRequest()) { - if (!(context.Result is ObjectResult objectResult) || objectResult.Value == null) + if (context.Result is not ObjectResult objectResult || objectResult.Value == null) { if (context.Result is IStatusCodeActionResult statusCodeResult) { diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs index 081d398b39..888c01544a 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; @@ -14,18 +13,6 @@ public interface IJsonApiRequest /// public EndpointKind Kind { get; } - /// - /// The request URL prefix. This may be an absolute or relative path, depending on . - /// - /// - /// - /// - [Obsolete("This value is calculated for backwards compatibility, but it is no longer used and will be removed in a future version.")] - string BasePath { get; } - /// /// The ID of the primary (top-level) resource for this request. This would be null in "/blogs", "123" in "/blogs/123" or "/blogs/123/author". /// @@ -59,9 +46,9 @@ public interface IJsonApiRequest bool IsReadOnly { get; } /// - /// In case of an atomic:operations request, this indicates the kind of operation currently being processed. + /// In case of a non-readonly request, this indicates the kind of write operation currently being processed. /// - OperationKind? OperationKind { get; } + WriteOperationKind? WriteOperation { get; } /// /// In case of an atomic:operations request, identifies the overarching transaction. diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 7bb694879e..3ad6355d2e 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -13,7 +11,6 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.Net.Http.Headers; @@ -62,11 +59,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin return; } - SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, options, resourceContextProvider, httpContext.Request); + SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request); httpContext.RegisterJsonApiRequest(); } - else if (IsOperationsRequest(routeValues)) + else if (IsRouteForOperations(routeValues)) { if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) @@ -212,13 +209,11 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri } private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, - IJsonApiOptions options, IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) + IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) { request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method; - request.Kind = EndpointKind.Primary; request.PrimaryResource = primaryResourceContext; request.PrimaryId = GetPrimaryRequestId(routeValues); - request.BasePath = GetBasePath(primaryResourceContext.PublicName, options, httpRequest); string relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); @@ -226,6 +221,17 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext { request.Kind = IsRouteForRelationship(routeValues) ? EndpointKind.Relationship : EndpointKind.Secondary; + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + request.WriteOperation = + httpRequest.Method == HttpMethod.Post.Method ? WriteOperationKind.AddToRelationship : + httpRequest.Method == HttpMethod.Patch.Method ? WriteOperationKind.SetRelationship : + httpRequest.Method == HttpMethod.Delete.Method ? WriteOperationKind.RemoveFromRelationship : null; + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + RelationshipAttribute requestRelationship = primaryResourceContext.Relationships.SingleOrDefault(relationship => relationship.PublicName == relationshipName); @@ -235,66 +241,29 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext request.SecondaryResource = resourceContextProvider.GetResourceContext(requestRelationship.RightType); } } - - bool isGetAll = request.PrimaryId == null && request.IsReadOnly; - request.IsCollection = isGetAll || request.Relationship is HasManyAttribute; - } - - private static string GetPrimaryRequestId(RouteValueDictionary routeValues) - { - return routeValues.TryGetValue("id", out object id) ? (string)id : null; - } - - private static string GetBasePath(string resourceName, IJsonApiOptions options, HttpRequest httpRequest) - { - var builder = new StringBuilder(); - - if (!options.UseRelativeLinks) + else { - builder.Append(httpRequest.Scheme); - builder.Append("://"); - builder.Append(httpRequest.Host); - } + request.Kind = EndpointKind.Primary; - if (httpRequest.PathBase.HasValue) - { - builder.Append(httpRequest.PathBase); - } + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true - string customRoute = GetCustomRoute(resourceName, options.Namespace, httpRequest.HttpContext); + request.WriteOperation = + httpRequest.Method == HttpMethod.Post.Method ? WriteOperationKind.CreateResource : + httpRequest.Method == HttpMethod.Patch.Method ? WriteOperationKind.UpdateResource : + httpRequest.Method == HttpMethod.Delete.Method ? WriteOperationKind.DeleteResource : null; - if (!string.IsNullOrEmpty(customRoute)) - { - builder.Append('/'); - builder.Append(customRoute); - } - else if (!string.IsNullOrEmpty(options.Namespace)) - { - builder.Append('/'); - builder.Append(options.Namespace); + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore } - return builder.ToString(); + bool isGetAll = request.PrimaryId == null && request.IsReadOnly; + request.IsCollection = isGetAll || request.Relationship is HasManyAttribute; } - private static string GetCustomRoute(string resourceName, string apiNamespace, HttpContext httpContext) + private static string GetPrimaryRequestId(RouteValueDictionary routeValues) { - if (resourceName != null) - { - Endpoint endpoint = httpContext.GetEndpoint(); - var routeAttribute = endpoint?.Metadata.GetMetadata(); - - if (routeAttribute != null && httpContext.Request.Path.Value != null) - { - List trimmedComponents = httpContext.Request.Path.Value.Trim('/').Split('/').ToList(); - int resourceNameIndex = trimmedComponents.FindIndex(component => component == resourceName); - string[] newComponents = trimmedComponents.Take(resourceNameIndex).ToArray(); - string customRoute = string.Join('/', newComponents); - return customRoute == apiNamespace ? null : customRoute; - } - } - - return null; + return routeValues.TryGetValue("id", out object id) ? (string)id : null; } private static string GetRelationshipNameForSecondaryRequest(RouteValueDictionary routeValues) @@ -308,7 +277,7 @@ private static bool IsRouteForRelationship(RouteValueDictionary routeValues) return actionName.EndsWith("Relationship", StringComparison.Ordinal); } - private static bool IsOperationsRequest(RouteValueDictionary routeValues) + private static bool IsRouteForOperations(RouteValueDictionary routeValues) { string actionName = (string)routeValues["action"]; return actionName == "PostOperations"; @@ -318,7 +287,6 @@ private static void SetupOperationsRequest(JsonApiRequest request, IJsonApiOptio { request.IsReadOnly = false; request.Kind = EndpointKind.AtomicOperations; - request.BasePath = GetBasePath(null, options, httpRequest); } } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs index b732529e2d..89bd6fa722 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs @@ -11,9 +11,6 @@ public sealed class JsonApiRequest : IJsonApiRequest /// public EndpointKind Kind { get; set; } - /// - public string BasePath { get; set; } - /// public string PrimaryId { get; set; } @@ -33,7 +30,7 @@ public sealed class JsonApiRequest : IJsonApiRequest public bool IsReadOnly { get; set; } /// - public OperationKind? OperationKind { get; set; } + public WriteOperationKind? WriteOperation { get; set; } /// public string TransactionId { get; set; } @@ -44,16 +41,13 @@ public void CopyFrom(IJsonApiRequest other) ArgumentGuard.NotNull(other, nameof(other)); Kind = other.Kind; -#pragma warning disable CS0618 // Type or member is obsolete - BasePath = other.BasePath; -#pragma warning restore CS0618 // Type or member is obsolete PrimaryId = other.PrimaryId; PrimaryResource = other.PrimaryResource; SecondaryResource = other.SecondaryResource; Relationship = other.Relationship; IsCollection = other.IsCollection; IsReadOnly = other.IsReadOnly; - OperationKind = other.OperationKind; + WriteOperation = other.WriteOperation; TransactionId = other.TransactionId; } } diff --git a/src/JsonApiDotNetCore/Middleware/OperationKind.cs b/src/JsonApiDotNetCore/Middleware/WriteOperationKind.cs similarity index 82% rename from src/JsonApiDotNetCore/Middleware/OperationKind.cs rename to src/JsonApiDotNetCore/Middleware/WriteOperationKind.cs index e3a528f2b0..aeec6c1cab 100644 --- a/src/JsonApiDotNetCore/Middleware/OperationKind.cs +++ b/src/JsonApiDotNetCore/Middleware/WriteOperationKind.cs @@ -1,9 +1,10 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Lists the functional operation kinds of a resource request or an atomic:operations request. + /// Lists the functional write operations, originating from a POST/PATCH/DELETE request against a single resource/relationship or a POST request against + /// a list of operations. /// - public enum OperationKind + public enum WriteOperationKind { /// /// Create a new resource with attributes, relationships or both. diff --git a/src/JsonApiDotNetCore/ObjectExtensions.cs b/src/JsonApiDotNetCore/ObjectExtensions.cs index e7351f5527..8657b64e96 100644 --- a/src/JsonApiDotNetCore/ObjectExtensions.cs +++ b/src/JsonApiDotNetCore/ObjectExtensions.cs @@ -26,5 +26,13 @@ public static List AsList(this T element) element }; } + + public static HashSet AsHashSet(this T element) + { + return new() + { + element + }; + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs similarity index 80% rename from src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs rename to src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs index 5e723bb5bf..3c049e86b4 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/AnyExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using JetBrains.Annotations; @@ -11,12 +11,12 @@ namespace JsonApiDotNetCore.Queries.Expressions /// Represents the "any" filter function, resulting from text such as: any(name,'Jack','Joe') /// [PublicAPI] - public class EqualsAnyOfExpression : FilterExpression + public class AnyExpression : FilterExpression { public ResourceFieldChainExpression TargetAttribute { get; } - public IReadOnlyCollection Constants { get; } + public IImmutableSet Constants { get; } - public EqualsAnyOfExpression(ResourceFieldChainExpression targetAttribute, IReadOnlyCollection constants) + public AnyExpression(ResourceFieldChainExpression targetAttribute, IImmutableSet constants) { ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); ArgumentGuard.NotNull(constants, nameof(constants)); @@ -32,7 +32,7 @@ public EqualsAnyOfExpression(ResourceFieldChainExpression targetAttribute, IRead public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) { - return visitor.VisitEqualsAnyOf(this, argument); + return visitor.VisitAny(this, argument); } public override string ToString() @@ -43,7 +43,7 @@ public override string ToString() builder.Append('('); builder.Append(TargetAttribute); builder.Append(','); - builder.Append(string.Join(",", Constants.Select(constant => constant.ToString()))); + builder.Append(string.Join(",", Constants.Select(constant => constant.ToString()).OrderBy(value => value))); builder.Append(')'); return builder.ToString(); @@ -61,9 +61,9 @@ public override bool Equals(object obj) return false; } - var other = (EqualsAnyOfExpression)obj; + var other = (AnyExpression)obj; - return TargetAttribute.Equals(other.TargetAttribute) && Constants.SequenceEqual(other.Constants); + return TargetAttribute.Equals(other.TargetAttribute) && Constants.SetEquals(other.Constants); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs similarity index 85% rename from src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs rename to src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs index 5aaa958feb..97f3bb5544 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/HasExpression.cs @@ -9,12 +9,12 @@ namespace JsonApiDotNetCore.Queries.Expressions /// Represents the "has" filter function, resulting from text such as: has(articles) or has(articles,equals(isHidden,'false')) /// [PublicAPI] - public class CollectionNotEmptyExpression : FilterExpression + public class HasExpression : FilterExpression { public ResourceFieldChainExpression TargetCollection { get; } public FilterExpression Filter { get; } - public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection, FilterExpression filter) + public HasExpression(ResourceFieldChainExpression targetCollection, FilterExpression filter) { ArgumentGuard.NotNull(targetCollection, nameof(targetCollection)); @@ -24,7 +24,7 @@ public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollectio public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) { - return visitor.VisitCollectionNotEmpty(this, argument); + return visitor.VisitHas(this, argument); } public override string ToString() @@ -57,7 +57,7 @@ public override bool Equals(object obj) return false; } - var other = (CollectionNotEmptyExpression)obj; + var other = (HasExpression)obj; return TargetCollection.Equals(other.TargetCollection) && Equals(Filter, other.Filter); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index d2b3417b99..dbe3b9b0dd 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JsonApiDotNetCore.Resources.Annotations; @@ -63,15 +64,15 @@ public IReadOnlyCollection GetRelationshipChains(I /// } /// ]]> /// - public IncludeExpression FromRelationshipChains(IReadOnlyCollection chains) + public IncludeExpression FromRelationshipChains(IEnumerable chains) { ArgumentGuard.NotNull(chains, nameof(chains)); - IReadOnlyCollection elements = ConvertChainsToElements(chains); + IImmutableList elements = ConvertChainsToElements(chains); return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty; } - private static IReadOnlyCollection ConvertChainsToElements(IReadOnlyCollection chains) + private static IImmutableList ConvertChainsToElements(IEnumerable chains) { var rootNode = new MutableIncludeNode(null); @@ -80,7 +81,7 @@ private static IReadOnlyCollection ConvertChainsToElem ConvertChainToElement(chain, rootNode); } - return rootNode.Children.Values.Select(child => child.ToExpression()).ToArray(); + return rootNode.Children.Values.Select(child => child.ToExpression()).ToImmutableArray(); } private static void ConvertChainToElement(ResourceFieldChainExpression chain, MutableIncludeNode rootNode) @@ -137,10 +138,13 @@ public override object VisitIncludeElement(IncludeElementExpression expression, private void FlushChain(IncludeElementExpression expression) { - List fieldsInChain = _parentRelationshipStack.Reverse().ToList(); - fieldsInChain.Add(expression.Relationship); + ImmutableArray.Builder chainBuilder = + ImmutableArray.CreateBuilder(_parentRelationshipStack.Count + 1); - Chains.Add(new ResourceFieldChainExpression(fieldsInChain)); + chainBuilder.AddRange(_parentRelationshipStack.Reverse()); + chainBuilder.Add(expression.Relationship); + + Chains.Add(new ResourceFieldChainExpression(chainBuilder.ToImmutable())); } } @@ -157,7 +161,7 @@ public MutableIncludeNode(RelationshipAttribute relationship) public IncludeElementExpression ToExpression() { - IncludeElementExpression[] elementChildren = Children.Values.Select(child => child.ToExpression()).ToArray(); + ImmutableArray elementChildren = Children.Values.Select(child => child.ToExpression()).ToImmutableArray(); return new IncludeElementExpression(_relationship, elementChildren); } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index f279f4ede4..a63db4c707 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using JetBrains.Annotations; @@ -14,14 +14,14 @@ namespace JsonApiDotNetCore.Queries.Expressions public class IncludeElementExpression : QueryExpression { public RelationshipAttribute Relationship { get; } - public IReadOnlyCollection Children { get; } + public IImmutableList Children { get; } public IncludeElementExpression(RelationshipAttribute relationship) - : this(relationship, Array.Empty()) + : this(relationship, ImmutableArray.Empty) { } - public IncludeElementExpression(RelationshipAttribute relationship, IReadOnlyCollection children) + public IncludeElementExpression(RelationshipAttribute relationship, IImmutableList children) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(children, nameof(children)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index 54054db8ec..482ba0158d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; @@ -15,9 +16,9 @@ public class IncludeExpression : QueryExpression public static readonly IncludeExpression Empty = new(); - public IReadOnlyCollection Elements { get; } + public IImmutableList Elements { get; } - public IncludeExpression(IReadOnlyCollection elements) + public IncludeExpression(IImmutableList elements) { ArgumentGuard.NotNullNorEmpty(elements, nameof(elements)); @@ -26,7 +27,7 @@ public IncludeExpression(IReadOnlyCollection elements) private IncludeExpression() { - Elements = Array.Empty(); + Elements = ImmutableArray.Empty; } public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index ab991aad9a..30d77e8a5b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using Humanizer; @@ -14,9 +14,14 @@ namespace JsonApiDotNetCore.Queries.Expressions public class LogicalExpression : FilterExpression { public LogicalOperator Operator { get; } - public IReadOnlyCollection Terms { get; } + public IImmutableList Terms { get; } - public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection terms) + public LogicalExpression(LogicalOperator @operator, params FilterExpression[] terms) + : this(@operator, terms.ToImmutableArray()) + { + } + + public LogicalExpression(LogicalOperator @operator, IImmutableList terms) { ArgumentGuard.NotNull(terms, nameof(terms)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs index 1850ab0b02..706d3d9e15 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; @@ -11,9 +11,9 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class PaginationQueryStringValueExpression : QueryExpression { - public IReadOnlyCollection Elements { get; } + public IImmutableList Elements { get; } - public PaginationQueryStringValueExpression(IReadOnlyCollection elements) + public PaginationQueryStringValueExpression(IImmutableList elements) { ArgumentGuard.NotNullNorEmpty(elements, nameof(elements)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index 5def9a208a..bd4d1e4de8 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Collections.Immutable; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -54,11 +53,11 @@ public override QueryExpression VisitLogical(LogicalExpression expression, TArgu { if (expression != null) { - IReadOnlyCollection newTerms = VisitSequence(expression.Terms, argument); + IImmutableList newTerms = VisitList(expression.Terms, argument); if (newTerms.Count == 1) { - return newTerms.First(); + return newTerms[0]; } if (newTerms.Count != 0) @@ -85,7 +84,7 @@ public override QueryExpression VisitNot(NotExpression expression, TArgument arg return null; } - public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, TArgument argument) + public override QueryExpression VisitHas(HasExpression expression, TArgument argument) { if (expression != null) { @@ -93,7 +92,7 @@ public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpres { FilterExpression newFilter = expression.Filter != null ? Visit(expression.Filter, argument) as FilterExpression : null; - var newExpression = new CollectionNotEmptyExpression(newTargetCollection, newFilter); + var newExpression = new HasExpression(newTargetCollection, newFilter); return newExpression.Equals(expression) ? expression : newExpression; } } @@ -135,7 +134,7 @@ public override QueryExpression VisitSort(SortExpression expression, TArgument a { if (expression != null) { - IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); + IImmutableList newElements = VisitList(expression.Elements, argument); if (newElements.Count != 0) { @@ -180,14 +179,14 @@ public override QueryExpression VisitMatchText(MatchTextExpression expression, T return null; } - public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expression, TArgument argument) + public override QueryExpression VisitAny(AnyExpression expression, TArgument argument) { if (expression != null) { var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; - IReadOnlyCollection newConstants = VisitSequence(expression.Constants, argument); + IImmutableSet newConstants = VisitSet(expression.Constants, argument); - var newExpression = new EqualsAnyOfExpression(newTargetAttribute, newConstants); + var newExpression = new AnyExpression(newTargetAttribute, newConstants); return newExpression.Equals(expression) ? expression : newExpression; } @@ -198,7 +197,8 @@ public override QueryExpression VisitSparseFieldTable(SparseFieldTableExpression { if (expression != null) { - var newTable = new Dictionary(); + ImmutableDictionary.Builder newTable = + ImmutableDictionary.CreateBuilder(); foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in expression.Table) { @@ -210,7 +210,7 @@ public override QueryExpression VisitSparseFieldTable(SparseFieldTableExpression if (newTable.Count > 0) { - var newExpression = new SparseFieldTableExpression(newTable); + var newExpression = new SparseFieldTableExpression(newTable.ToImmutable()); return newExpression.Equals(expression) ? expression : newExpression; } } @@ -242,7 +242,7 @@ public override QueryExpression PaginationQueryStringValue(PaginationQueryString { if (expression != null) { - IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); + IImmutableList newElements = VisitList(expression.Elements, argument); var newExpression = new PaginationQueryStringValueExpression(newElements); return newExpression.Equals(expression) ? expression : newExpression; @@ -268,7 +268,7 @@ public override QueryExpression VisitInclude(IncludeExpression expression, TArgu { if (expression != null) { - IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); + IImmutableList newElements = VisitList(expression.Elements, argument); if (newElements.Count == 0) { @@ -286,7 +286,7 @@ public override QueryExpression VisitIncludeElement(IncludeElementExpression exp { if (expression != null) { - IReadOnlyCollection newElements = VisitSequence(expression.Children, argument); + IImmutableList newElements = VisitList(expression.Children, argument); var newExpression = new IncludeElementExpression(expression.Relationship, newElements); return newExpression.Equals(expression) ? expression : newExpression; @@ -300,20 +300,36 @@ public override QueryExpression VisitQueryableHandler(QueryableHandlerExpression return expression; } - protected virtual IReadOnlyCollection VisitSequence(IEnumerable elements, TArgument argument) + protected virtual IImmutableList VisitList(IImmutableList elements, TArgument argument) where TExpression : QueryExpression { - var newElements = new List(); + ImmutableArray.Builder arrayBuilder = ImmutableArray.CreateBuilder(elements.Count); foreach (TExpression element in elements) { if (Visit(element, argument) is TExpression newElement) { - newElements.Add(newElement); + arrayBuilder.Add(newElement); } } - return newElements; + return arrayBuilder.ToImmutable(); + } + + protected virtual IImmutableSet VisitSet(IImmutableSet elements, TArgument argument) + where TExpression : QueryExpression + { + ImmutableHashSet.Builder setBuilder = ImmutableHashSet.CreateBuilder(); + + foreach (TExpression element in elements) + { + if (Visit(element, argument) is TExpression newElement) + { + setBuilder.Add(newElement); + } + } + + return setBuilder.ToImmutable(); } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs index f16a7b0424..dad2958931 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs @@ -48,7 +48,7 @@ public virtual TResult VisitNot(NotExpression expression, TArgument argument) return DefaultVisit(expression, argument); } - public virtual TResult VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, TArgument argument) + public virtual TResult VisitHas(HasExpression expression, TArgument argument) { return DefaultVisit(expression, argument); } @@ -78,7 +78,7 @@ public virtual TResult VisitMatchText(MatchTextExpression expression, TArgument return DefaultVisit(expression, argument); } - public virtual TResult VisitEqualsAnyOf(EqualsAnyOfExpression expression, TArgument argument) + public virtual TResult VisitAny(AnyExpression expression, TArgument argument) { return DefaultVisit(expression, argument); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index 2ebec58d17..502cd03976 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; @@ -12,16 +12,16 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class ResourceFieldChainExpression : IdentifierExpression { - public IReadOnlyCollection Fields { get; } + public IImmutableList Fields { get; } public ResourceFieldChainExpression(ResourceFieldAttribute field) { ArgumentGuard.NotNull(field, nameof(field)); - Fields = field.AsArray(); + Fields = ImmutableArray.Create(field); } - public ResourceFieldChainExpression(IReadOnlyCollection fields) + public ResourceFieldChainExpression(IImmutableList fields) { ArgumentGuard.NotNullNorEmpty(fields, nameof(fields)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs index 9f9c74b668..38f68df707 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; @@ -11,9 +11,9 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class SortExpression : QueryExpression { - public IReadOnlyCollection Elements { get; } + public IImmutableList Elements { get; } - public SortExpression(IReadOnlyCollection elements) + public SortExpression(IImmutableList elements) { ArgumentGuard.NotNullNorEmpty(elements, nameof(elements)); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index 7511ab309a..bf96dad409 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; @@ -12,9 +12,9 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class SparseFieldSetExpression : QueryExpression { - public IReadOnlyCollection Fields { get; } + public IImmutableSet Fields { get; } - public SparseFieldSetExpression(IReadOnlyCollection fields) + public SparseFieldSetExpression(IImmutableSet fields) { ArgumentGuard.NotNullNorEmpty(fields, nameof(fields)); @@ -28,7 +28,7 @@ public override TResult Accept(QueryExpressionVisitor child.PublicName)); + return string.Join(",", Fields.Select(child => child.PublicName).OrderBy(name => name)); } public override bool Equals(object obj) @@ -45,7 +45,7 @@ public override bool Equals(object obj) var other = (SparseFieldSetExpression)obj; - return Fields.SequenceEqual(other.Fields); + return Fields.SetEquals(other.Fields); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs index 62010b7e11..35aff8b711 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Immutable; using System.Linq.Expressions; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -36,9 +35,8 @@ private static SparseFieldSetExpression IncludeField(SparseFieldSetExpression sp return sparseFieldSet; } - HashSet fieldSet = sparseFieldSet.Fields.ToHashSet(); - fieldSet.Add(fieldToInclude); - return new SparseFieldSetExpression(fieldSet); + IImmutableSet newSparseFieldSet = sparseFieldSet.Fields.Add(fieldToInclude); + return new SparseFieldSetExpression(newSparseFieldSet); } public static SparseFieldSetExpression Excluding(this SparseFieldSetExpression sparseFieldSet, @@ -70,9 +68,8 @@ private static SparseFieldSetExpression ExcludeField(SparseFieldSetExpression sp return sparseFieldSet; } - HashSet fieldSet = sparseFieldSet.Fields.ToHashSet(); - fieldSet.Remove(fieldToExclude); - return new SparseFieldSetExpression(fieldSet); + IImmutableSet newSparseFieldSet = sparseFieldSet.Fields.Remove(fieldToExclude); + return new SparseFieldSetExpression(newSparseFieldSet); } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs index 4fdc92ec60..9cf7922349 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Immutable; using System.Text; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -13,9 +12,9 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class SparseFieldTableExpression : QueryExpression { - public IReadOnlyDictionary Table { get; } + public IImmutableDictionary Table { get; } - public SparseFieldTableExpression(IReadOnlyDictionary table) + public SparseFieldTableExpression(IImmutableDictionary table) { ArgumentGuard.NotNullNorEmpty(table, nameof(table), "entries"); @@ -61,7 +60,7 @@ public override bool Equals(object obj) var other = (SparseFieldTableExpression)obj; - return Table.SequenceEqual(other.Table); + return Table.DictionaryEqual(other.Table); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs index 35094f0e43..c3fa8428e4 100644 --- a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Queries { @@ -59,12 +57,5 @@ QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, Resourc /// Builds a query for a to-many relationship with a filter to match on its left and right resource IDs. /// QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, TId leftId, ICollection rightResourceIds); - - /// - /// Provides access to the request-scoped instance. This method has been added solely to prevent introducing a - /// breaking change in the constructor and will be removed in the next major version. - /// - [Obsolete] - IResourceDefinitionAccessor GetResourceDefinitionAccessor(); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index d4a7f04acd..5461fb994c 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Immutable; using System.Reflection; using Humanizer; using JetBrains.Annotations; @@ -106,28 +105,28 @@ protected LogicalExpression ParseLogical(string operatorName) EatText(operatorName); EatSingleCharacterToken(TokenKind.OpenParen); - var terms = new List(); + ImmutableArray.Builder termsBuilder = ImmutableArray.CreateBuilder(); FilterExpression term = ParseFilter(); - terms.Add(term); + termsBuilder.Add(term); EatSingleCharacterToken(TokenKind.Comma); term = ParseFilter(); - terms.Add(term); + termsBuilder.Add(term); while (TokenStack.TryPeek(out Token nextToken) && nextToken.Kind == TokenKind.Comma) { EatSingleCharacterToken(TokenKind.Comma); term = ParseFilter(); - terms.Add(term); + termsBuilder.Add(term); } EatSingleCharacterToken(TokenKind.CloseParen); var logicalOperator = Enum.Parse(operatorName.Pascalize()); - return new LogicalExpression(logicalOperator, terms); + return new LogicalExpression(logicalOperator, termsBuilder.ToImmutable()); } protected ComparisonExpression ParseComparison(string operatorName) @@ -152,13 +151,13 @@ protected ComparisonExpression ParseComparison(string operatorName) if (leftTerm is ResourceFieldChainExpression leftChain) { - if (leftChainRequirements.HasFlag(FieldChainRequirements.EndsInToOne) && !(rightTerm is NullConstantExpression)) + if (leftChainRequirements.HasFlag(FieldChainRequirements.EndsInToOne) && rightTerm is not NullConstantExpression) { // Run another pass over left chain to have it fail when chain ends in relationship. OnResolveFieldChain(leftChain.ToString(), FieldChainRequirements.EndsInAttribute); } - PropertyInfo leftProperty = leftChain.Fields.Last().Property; + PropertyInfo leftProperty = leftChain.Fields[^1].Property; if (leftProperty.Name == nameof(Identifiable.Id) && rightTerm is LiteralConstantExpression rightConstant) { @@ -187,7 +186,7 @@ protected MatchTextExpression ParseTextMatch(string matchFunctionName) return new MatchTextExpression(targetAttribute, constant, matchKind); } - protected EqualsAnyOfExpression ParseAny() + protected AnyExpression ParseAny() { EatText(Keywords.Any); EatSingleCharacterToken(TokenKind.OpenParen); @@ -196,42 +195,55 @@ protected EqualsAnyOfExpression ParseAny() EatSingleCharacterToken(TokenKind.Comma); - var constants = new List(); + ImmutableHashSet.Builder constantsBuilder = ImmutableHashSet.CreateBuilder(); LiteralConstantExpression constant = ParseConstant(); - constants.Add(constant); + constantsBuilder.Add(constant); EatSingleCharacterToken(TokenKind.Comma); constant = ParseConstant(); - constants.Add(constant); + constantsBuilder.Add(constant); while (TokenStack.TryPeek(out Token nextToken) && nextToken.Kind == TokenKind.Comma) { EatSingleCharacterToken(TokenKind.Comma); constant = ParseConstant(); - constants.Add(constant); + constantsBuilder.Add(constant); } EatSingleCharacterToken(TokenKind.CloseParen); - PropertyInfo targetAttributeProperty = targetAttribute.Fields.Last().Property; + IImmutableSet constantSet = constantsBuilder.ToImmutable(); + + PropertyInfo targetAttributeProperty = targetAttribute.Fields[^1].Property; if (targetAttributeProperty.Name == nameof(Identifiable.Id)) { - for (int index = 0; index < constants.Count; index++) - { - string stringId = constants[index].Value; - string id = DeObfuscateStringId(targetAttributeProperty.ReflectedType, stringId); - constants[index] = new LiteralConstantExpression(id); - } + constantSet = DeObfuscateIdConstants(constantSet, targetAttributeProperty); + } + + return new AnyExpression(targetAttribute, constantSet); + } + + private IImmutableSet DeObfuscateIdConstants(IImmutableSet constantSet, + PropertyInfo targetAttributeProperty) + { + ImmutableHashSet.Builder idConstantsBuilder = ImmutableHashSet.CreateBuilder(); + + foreach (LiteralConstantExpression idConstant in constantSet) + { + string stringId = idConstant.Value; + string id = DeObfuscateStringId(targetAttributeProperty.ReflectedType, stringId); + + idConstantsBuilder.Add(new LiteralConstantExpression(id)); } - return new EqualsAnyOfExpression(targetAttribute, constants); + return idConstantsBuilder.ToImmutable(); } - protected CollectionNotEmptyExpression ParseHas() + protected HasExpression ParseHas() { EatText(Keywords.Has); EatSingleCharacterToken(TokenKind.OpenParen); @@ -243,12 +255,12 @@ protected CollectionNotEmptyExpression ParseHas() { EatSingleCharacterToken(TokenKind.Comma); - filter = ParseFilterInHas((HasManyAttribute)targetCollection.Fields.Last()); + filter = ParseFilterInHas((HasManyAttribute)targetCollection.Fields[^1]); } EatSingleCharacterToken(TokenKind.CloseParen); - return new CollectionNotEmptyExpression(targetCollection, filter); + return new HasExpression(targetCollection, filter); } private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship) @@ -332,7 +344,7 @@ private string DeObfuscateStringId(Type resourceType, string stringId) return tempResource.GetTypedId().ToString(); } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { if (chainRequirements == FieldChainRequirements.EndsInToMany) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index 30f0a0b819..b6cb69d6b9 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -57,7 +58,7 @@ protected IncludeExpression ParseInclude(int? maximumDepth) return IncludeChainConverter.FromRelationshipChains(chains); } - private static void ValidateMaximumIncludeDepth(int? maximumDepth, IReadOnlyCollection chains) + private static void ValidateMaximumIncludeDepth(int? maximumDepth, IEnumerable chains) { if (maximumDepth != null) { @@ -72,7 +73,7 @@ private static void ValidateMaximumIncludeDepth(int? maximumDepth, IReadOnlyColl } } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { return ChainResolver.ResolveRelationshipChain(_resourceContextInScope, path, _validateSingleRelationshipCallback); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 156c5281dc..54c56b2f13 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -38,20 +38,21 @@ public PaginationQueryStringValueExpression Parse(string source, ResourceContext protected PaginationQueryStringValueExpression ParsePagination() { - var elements = new List(); + ImmutableArray.Builder elementsBuilder = + ImmutableArray.CreateBuilder(); PaginationElementQueryStringValueExpression element = ParsePaginationElement(); - elements.Add(element); + elementsBuilder.Add(element); while (TokenStack.Any()) { EatSingleCharacterToken(TokenKind.Comma); element = ParsePaginationElement(); - elements.Add(element); + elementsBuilder.Add(element); } - return new PaginationQueryStringValueExpression(elements); + return new PaginationQueryStringValueExpression(elementsBuilder.ToImmutable()); } protected PaginationElementQueryStringValueExpression ParsePaginationElement() @@ -105,7 +106,7 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement() return null; } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { return ChainResolver.ResolveToManyChain(_resourceContextInScope, path, _validateSingleFieldCallback); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 48fcb44a07..99a195bb6c 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -28,7 +29,7 @@ protected QueryExpressionParser(IResourceContextProvider resourceContextProvider /// /// Takes a dotted path and walks the resource graph to produce a chain of fields. /// - protected abstract IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements); + protected abstract IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements); protected virtual void Tokenize(string source) { @@ -40,7 +41,7 @@ protected ResourceFieldChainExpression ParseFieldChain(FieldChainRequirements ch { if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text) { - IReadOnlyCollection chain = OnResolveFieldChain(token.Value, chainRequirements); + IImmutableList chain = OnResolveFieldChain(token.Value, chainRequirements); if (chain.Any()) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index 4944f090b1..feabc655ea 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; @@ -60,7 +60,7 @@ protected QueryStringParameterScopeExpression ParseQueryStringParameterScope() return new QueryStringParameterScopeExpression(name, scope); } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { if (chainRequirements == FieldChainRequirements.EndsInToMany) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 029c76fcb1..71c86ed9ba 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; @@ -23,10 +23,10 @@ public ResourceFieldChainResolver(IResourceContextProvider resourceContextProvid /// /// Resolves a chain of relationships that ends in a to-many relationship, for example: blogs.owner.articles.comments /// - public IReadOnlyCollection ResolveToManyChain(ResourceContext resourceContext, string path, + public IImmutableList ResolveToManyChain(ResourceContext resourceContext, string path, Action validateCallback = null) { - var chain = new List(); + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); ResourceContext nextResourceContext = resourceContext; @@ -37,7 +37,7 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo validateCallback?.Invoke(relationship, nextResourceContext, path); - chain.Add(relationship); + chainBuilder.Add(relationship); nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); } @@ -46,8 +46,8 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo validateCallback?.Invoke(lastToManyRelationship, nextResourceContext, path); - chain.Add(lastToManyRelationship); - return chain; + chainBuilder.Add(lastToManyRelationship); + return chainBuilder.ToImmutable(); } /// @@ -62,10 +62,10 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo /// articles.revisions.author /// /// - public IReadOnlyCollection ResolveRelationshipChain(ResourceContext resourceContext, string path, + public IImmutableList ResolveRelationshipChain(ResourceContext resourceContext, string path, Action validateCallback = null) { - var chain = new List(); + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); ResourceContext nextResourceContext = resourceContext; foreach (string publicName in path.Split(".")) @@ -74,11 +74,11 @@ public IReadOnlyCollection ResolveRelationshipChain(Reso validateCallback?.Invoke(relationship, nextResourceContext, path); - chain.Add(relationship); + chainBuilder.Add(relationship); nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); } - return chain; + return chainBuilder.ToImmutable(); } /// @@ -88,10 +88,10 @@ public IReadOnlyCollection ResolveRelationshipChain(Reso /// /// name /// - public IReadOnlyCollection ResolveToOneChainEndingInAttribute(ResourceContext resourceContext, string path, + public IImmutableList ResolveToOneChainEndingInAttribute(ResourceContext resourceContext, string path, Action validateCallback = null) { - var chain = new List(); + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); ResourceContext nextResourceContext = resourceContext; @@ -102,7 +102,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); - chain.Add(toOneRelationship); + chainBuilder.Add(toOneRelationship); nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } @@ -111,8 +111,8 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr validateCallback?.Invoke(lastAttribute, nextResourceContext, path); - chain.Add(lastAttribute); - return chain; + chainBuilder.Add(lastAttribute); + return chainBuilder.ToImmutable(); } /// @@ -124,10 +124,10 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr /// comments /// /// - public IReadOnlyCollection ResolveToOneChainEndingInToMany(ResourceContext resourceContext, string path, + public IImmutableList ResolveToOneChainEndingInToMany(ResourceContext resourceContext, string path, Action validateCallback = null) { - var chain = new List(); + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); ResourceContext nextResourceContext = resourceContext; @@ -138,7 +138,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); - chain.Add(toOneRelationship); + chainBuilder.Add(toOneRelationship); nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } @@ -148,8 +148,8 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa validateCallback?.Invoke(toManyRelationship, nextResourceContext, path); - chain.Add(toManyRelationship); - return chain; + chainBuilder.Add(toManyRelationship); + return chainBuilder.ToImmutable(); } /// @@ -161,10 +161,10 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa /// author.address /// /// - public IReadOnlyCollection ResolveToOneChainEndingInAttributeOrToOne(ResourceContext resourceContext, string path, + public IImmutableList ResolveToOneChainEndingInAttributeOrToOne(ResourceContext resourceContext, string path, Action validateCallback = null) { - var chain = new List(); + ImmutableArray.Builder chainBuilder = ImmutableArray.CreateBuilder(); string[] publicNameParts = path.Split("."); ResourceContext nextResourceContext = resourceContext; @@ -175,7 +175,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); - chain.Add(toOneRelationship); + chainBuilder.Add(toOneRelationship); nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); } @@ -191,8 +191,8 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr validateCallback?.Invoke(lastField, nextResourceContext, path); - chain.Add(lastField); - return chain; + chainBuilder.Add(lastField); + return chainBuilder.ToImmutable(); } private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) @@ -213,7 +213,7 @@ private RelationshipAttribute GetToManyRelationship(string publicName, ResourceC { RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); - if (!(relationship is HasManyAttribute)) + if (relationship is not HasManyAttribute) { throw new QueryParseException(path == publicName ? $"Relationship '{publicName}' must be a to-many relationship on resource '{resourceContext.PublicName}'." @@ -227,7 +227,7 @@ private RelationshipAttribute GetToOneRelationship(string publicName, ResourceCo { RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); - if (!(relationship is HasOneAttribute)) + if (relationship is not HasOneAttribute) { throw new QueryParseException(path == publicName ? $"Relationship '{publicName}' must be a to-one relationship on resource '{resourceContext.PublicName}'." diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index 3b146b1785..d9f0413935 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -40,17 +40,18 @@ protected SortExpression ParseSort() { SortElementExpression firstElement = ParseSortElement(); - List elements = firstElement.AsList(); + ImmutableArray.Builder elementsBuilder = ImmutableArray.CreateBuilder(); + elementsBuilder.Add(firstElement); while (TokenStack.Any()) { EatSingleCharacterToken(TokenKind.Comma); SortElementExpression nextElement = ParseSortElement(); - elements.Add(nextElement); + elementsBuilder.Add(nextElement); } - return new SortExpression(elements); + return new SortExpression(elementsBuilder.ToImmutable()); } protected SortElementExpression ParseSortElement() @@ -75,7 +76,7 @@ protected SortElementExpression ParseSortElement() return new SortElementExpression(targetAttribute, isAscending); } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { if (chainRequirements == FieldChainRequirements.EndsInToMany) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index 7ce7168a0d..dd45d1fe24 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -38,30 +38,30 @@ public SparseFieldSetExpression Parse(string source, ResourceContext resourceCon protected SparseFieldSetExpression ParseSparseFieldSet() { - var fields = new Dictionary(); + ImmutableHashSet.Builder fieldSetBuilder = ImmutableHashSet.CreateBuilder(); while (TokenStack.Any()) { - if (fields.Count > 0) + if (fieldSetBuilder.Count > 0) { EatSingleCharacterToken(TokenKind.Comma); } ResourceFieldChainExpression nextChain = ParseFieldChain(FieldChainRequirements.EndsInAttribute, "Field name expected."); ResourceFieldAttribute nextField = nextChain.Fields.Single(); - fields[nextField.PublicName] = nextField; + fieldSetBuilder.Add(nextField); } - return fields.Any() ? new SparseFieldSetExpression(fields.Values) : null; + return fieldSetBuilder.Any() ? new SparseFieldSetExpression(fieldSetBuilder.ToImmutable()) : null; } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { ResourceFieldAttribute field = ChainResolver.GetField(path, _resourceContext, path); _validateSingleFieldCallback?.Invoke(field, _resourceContext, path); - return field.AsArray(); + return ImmutableArray.Create(field); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index 361710e345..be9764f1ff 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; @@ -54,19 +54,19 @@ private ResourceContext ParseResourceName() throw new QueryParseException("Resource type expected."); } - private ResourceContext GetResourceContext(string resourceName) + private ResourceContext GetResourceContext(string publicName) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceName); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(publicName); if (resourceContext == null) { - throw new QueryParseException($"Resource type '{resourceName}' does not exist."); + throw new QueryParseException($"Resource type '{publicName}' does not exist."); } return resourceContext; } - protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) + protected override IImmutableList OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { throw new NotSupportedException(); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 070faee6f2..a61e9c6555 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -124,7 +125,7 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection includeElements = + IImmutableList includeElements = ProcessIncludeSet(include.Elements, topLayer, new List(), constraints); return !ReferenceEquals(includeElements, include.Elements) @@ -132,13 +133,13 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection ProcessIncludeSet(IReadOnlyCollection includeElements, - QueryLayer parentLayer, ICollection parentRelationshipChain, ICollection constraints) + private IImmutableList ProcessIncludeSet(IImmutableList includeElements, QueryLayer parentLayer, + ICollection parentRelationshipChain, ICollection constraints) { - IReadOnlyCollection includeElementsEvaluated = - GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); + IImmutableList includeElementsEvaluated = + GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? ImmutableArray.Empty; - var updatesInChildren = new Dictionary>(); + var updatesInChildren = new Dictionary>(); foreach (IncludeElementExpression includeElement in includeElementsEvaluated) { @@ -180,7 +181,7 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl if (includeElement.Children.Any()) { - IReadOnlyCollection updatedChildren = + IImmutableList updatedChildren = ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); if (!ReferenceEquals(includeElement.Children, updatedChildren)) @@ -194,18 +195,19 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl return !updatesInChildren.Any() ? includeElementsEvaluated : ApplyIncludeElementUpdates(includeElementsEvaluated, updatesInChildren); } - private static IReadOnlyCollection ApplyIncludeElementUpdates(IEnumerable includeElements, - IDictionary> updatesInChildren) + private static IImmutableList ApplyIncludeElementUpdates(IImmutableList includeElements, + IDictionary> updatesInChildren) { - List newIncludeElements = includeElements.ToList(); + ImmutableArray.Builder newElementsBuilder = ImmutableArray.CreateBuilder(includeElements.Count); + newElementsBuilder.AddRange(includeElements); - foreach ((IncludeElementExpression existingElement, IReadOnlyCollection updatedChildren) in updatesInChildren) + foreach ((IncludeElementExpression existingElement, IImmutableList updatedChildren) in updatesInChildren) { - int existingIndex = newIncludeElements.IndexOf(existingElement); - newIncludeElements[existingIndex] = new IncludeElementExpression(existingElement.Relationship, updatedChildren); + int existingIndex = newElementsBuilder.IndexOf(existingElement); + newElementsBuilder[existingIndex] = new IncludeElementExpression(existingElement.Relationship, updatedChildren); } - return newIncludeElements; + return newElementsBuilder.ToImmutable(); } /// @@ -253,7 +255,7 @@ public QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondary private IDictionary GetProjectionForRelationship(ResourceContext secondaryResourceContext) { - IReadOnlyCollection secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); + IImmutableSet secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } @@ -269,7 +271,7 @@ public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, IncludeExpression innerInclude = secondaryLayer.Include; secondaryLayer.Include = null; - IReadOnlyCollection primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); + IImmutableSet primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); Dictionary primaryProjection = primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); @@ -293,10 +295,10 @@ private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression r ? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements) : new IncludeElementExpression(secondaryRelationship); - return new IncludeExpression(parentElement.AsArray()); + return new IncludeExpression(ImmutableArray.Create(parentElement)); } - private FilterExpression CreateFilterByIds(ICollection ids, AttrAttribute idAttribute, FilterExpression existingFilter) + private FilterExpression CreateFilterByIds(IReadOnlyCollection ids, AttrAttribute idAttribute, FilterExpression existingFilter) { var idChain = new ResourceFieldChainExpression(idAttribute); @@ -309,19 +311,11 @@ private FilterExpression CreateFilterByIds(ICollection ids, AttrAttrib } else if (ids.Count > 1) { - List constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); - filter = new EqualsAnyOfExpression(idChain, constants); + ImmutableHashSet constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToImmutableHashSet(); + filter = new AnyExpression(idChain, constants); } - // @formatter:keep_existing_linebreaks true - - return filter == null - ? existingFilter - : existingFilter == null - ? filter - : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(filter, existingFilter)); - - // @formatter:keep_existing_linebreaks restore + return filter == null ? existingFilter : existingFilter == null ? filter : new LogicalExpression(LogicalOperator.And, filter, existingFilter); } /// @@ -329,8 +323,8 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) { ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); - IncludeElementExpression[] includeElements = _targetedFields.Relationships - .Select(relationship => new IncludeElementExpression(relationship)).ToArray(); + ImmutableArray includeElements = _targetedFields.Relationships + .Select(relationship => new IncludeElementExpression(relationship)).ToImmutableArray(); AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResource); @@ -405,7 +399,7 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T return new QueryLayer(leftResourceContext) { - Include = new IncludeExpression(new IncludeElementExpression(hasManyRelationship).AsArray()), + Include = new IncludeExpression(ImmutableArray.Create(new IncludeElementExpression(hasManyRelationship))), Filter = leftFilter, Projection = new Dictionary { @@ -422,13 +416,7 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T }; } - /// - public IResourceDefinitionAccessor GetResourceDefinitionAccessor() - { - return _resourceDefinitionAccessor; - } - - protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, + protected virtual IImmutableList GetIncludeElements(IImmutableList includeElements, ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); @@ -441,8 +429,7 @@ protected virtual FilterExpression GetFilter(IReadOnlyCollection().ToArray(); - + ImmutableArray filters = expressionsInScope.OfType().ToImmutableArray(); FilterExpression filter = filters.Length > 1 ? new LogicalExpression(LogicalOperator.And, filters) : filters.FirstOrDefault(); return _resourceDefinitionAccessor.OnApplyFilter(resourceContext.ResourceType, filter); @@ -460,7 +447,8 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex if (sort == null) { AttrAttribute idAttribute = GetIdAttribute(resourceContext); - sort = new SortExpression(new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true).AsArray()); + var idAscendingSort = new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true); + sort = new SortExpression(ImmutableArray.Create(idAscendingSort)); } return sort; @@ -484,7 +472,7 @@ protected virtual IDictionary GetProjectionF { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); if (!fieldSet.Any()) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index 4d396dfbcc..2a51b561c9 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -4,7 +4,6 @@ using System.Linq.Expressions; using System.Reflection; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { @@ -61,13 +60,12 @@ private static Expression TryGetCollectionCount(Expression collectionExpression) public override Expression VisitResourceFieldChain(ResourceFieldChainExpression expression, TArgument argument) { - string[] components = expression.Fields - .Select(field => field is RelationshipAttribute relationship ? relationship.Property.Name : field.Property.Name).ToArray(); + string[] components = expression.Fields.Select(field => field.Property.Name).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } - private static MemberExpression CreatePropertyExpressionFromComponents(Expression source, IReadOnlyCollection components) + private static MemberExpression CreatePropertyExpressionFromComponents(Expression source, IEnumerable components) { MemberExpression property = null; diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index d8fb26f15a..815c96d256 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -6,7 +6,6 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; @@ -186,25 +185,6 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property Expression layerExpression = builder.ApplyQuery(layer); - // Earlier versions of EF Core 3.x failed to understand `query.ToHashSet()`, so we emit `new HashSet(query)` instead. - // Interestingly, EF Core 5 RC1 fails to understand `new HashSet(query)`, so we emit `query.ToHashSet()` instead. - // https://github.com/dotnet/efcore/issues/22902 - - if (EntityFrameworkCoreSupport.Version.Major < 5) - { - Type enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(elementType); - Type typedCollection = CollectionConverter.ToConcreteCollectionType(collectionProperty.PropertyType); - - ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(enumerableOfElementType.AsArray()); - - if (typedCollectionConstructor == null) - { - throw new InvalidOperationException($"Constructor on '{typedCollection.Name}' that accepts '{enumerableOfElementType.Name}' not found."); - } - - return Expression.New(typedCollectionConstructor, layerExpression); - } - string operationName = CollectionConverter.TypeCanContainHashSet(collectionProperty.PropertyType) ? "ToHashSet" : "ToList"; return CopyCollectionExtensionMethodCall(layerExpression, operationName, elementType); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs index d95d228c25..8009328405 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs @@ -56,7 +56,7 @@ private Expression WhereExtensionMethodCall(LambdaExpression predicate) return Expression.Call(_extensionType, "Where", LambdaScope.Parameter.Type.AsArray(), _source, predicate); } - public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, Type argument) + public override Expression VisitHas(HasExpression expression, Type argument) { Expression property = Visit(expression.TargetCollection, argument); @@ -115,7 +115,7 @@ public override Expression VisitMatchText(MatchTextExpression expression, Type a return Expression.Call(property, "Contains", null, text); } - public override Expression VisitEqualsAnyOf(EqualsAnyOfExpression expression, Type argument) + public override Expression VisitAny(AnyExpression expression, Type argument) { Expression property = Visit(expression.TargetAttribute, argument); diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index c44a0a34db..66c336d674 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -17,8 +18,8 @@ namespace JsonApiDotNetCore.Queries.Internal public sealed class SparseFieldSetCache { private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - private readonly Lazy>> _lazySourceTable; - private readonly IDictionary> _visitedTable; + private readonly Lazy>> _lazySourceTable; + private readonly IDictionary> _visitedTable; public SparseFieldSetCache(IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) { @@ -26,11 +27,12 @@ public SparseFieldSetCache(IEnumerable constraintProvi ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _resourceDefinitionAccessor = resourceDefinitionAccessor; - _lazySourceTable = new Lazy>>(() => BuildSourceTable(constraintProviders)); - _visitedTable = new Dictionary>(); + _lazySourceTable = new Lazy>>(() => BuildSourceTable(constraintProviders)); + _visitedTable = new Dictionary>(); } - private static IDictionary> BuildSourceTable(IEnumerable constraintProviders) + private static IDictionary> BuildSourceTable( + IEnumerable constraintProviders) { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true @@ -47,30 +49,31 @@ private static IDictionary> Bui // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - var mergedTable = new Dictionary>(); + var mergedTable = new Dictionary.Builder>(); foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in sparseFieldTables) { if (!mergedTable.ContainsKey(resourceContext)) { - mergedTable[resourceContext] = new HashSet(); + mergedTable[resourceContext] = ImmutableHashSet.CreateBuilder(); } AddSparseFieldsToSet(sparseFieldSet.Fields, mergedTable[resourceContext]); } - return mergedTable; + return mergedTable.ToDictionary(pair => pair.Key, pair => (IImmutableSet)pair.Value.ToImmutable()); } - private static void AddSparseFieldsToSet(IReadOnlyCollection sparseFieldsToAdd, HashSet sparseFieldSet) + private static void AddSparseFieldsToSet(IImmutableSet sparseFieldsToAdd, + ImmutableHashSet.Builder sparseFieldSetBuilder) { foreach (ResourceFieldAttribute field in sparseFieldsToAdd) { - sparseFieldSet.Add(field); + sparseFieldSetBuilder.Add(field); } } - public IReadOnlyCollection GetSparseFieldSetForQuery(ResourceContext resourceContext) + public IImmutableSet GetSparseFieldSetForQuery(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); @@ -82,9 +85,9 @@ public IReadOnlyCollection GetSparseFieldSetForQuery(Res SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); - HashSet outputFields = outputExpression == null - ? new HashSet() - : outputExpression.Fields.ToHashSet(); + IImmutableSet outputFields = outputExpression == null + ? ImmutableHashSet.Empty + : outputExpression.Fields; _visitedTable[resourceContext] = outputFields; } @@ -92,48 +95,39 @@ public IReadOnlyCollection GetSparseFieldSetForQuery(Res return _visitedTable[resourceContext]; } - public IReadOnlyCollection GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext) + public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); - var inputExpression = new SparseFieldSetExpression(idAttribute.AsArray()); + var inputExpression = new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); // Intentionally not cached, as we are fetching ID only (ignoring any sparse fieldset that came from query string). SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); - HashSet outputAttributes = outputExpression == null - ? new HashSet() - : outputExpression.Fields.OfType().ToHashSet(); + ImmutableHashSet outputAttributes = outputExpression == null + ? ImmutableHashSet.Empty + : outputExpression.Fields.OfType().ToImmutableHashSet(); - outputAttributes.Add(idAttribute); + outputAttributes = outputAttributes.Add(idAttribute); return outputAttributes; } - public IReadOnlyCollection GetSparseFieldSetForSerializer(ResourceContext resourceContext) + public IImmutableSet GetSparseFieldSetForSerializer(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); if (!_visitedTable.ContainsKey(resourceContext)) { - HashSet inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) + IImmutableSet inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) ? _lazySourceTable.Value[resourceContext] : GetResourceFields(resourceContext); var inputExpression = new SparseFieldSetExpression(inputFields); SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); - HashSet outputFields; - - if (outputExpression == null) - { - outputFields = GetResourceFields(resourceContext); - } - else - { - outputFields = new HashSet(inputFields); - outputFields.IntersectWith(outputExpression.Fields); - } + IImmutableSet outputFields = + outputExpression == null ? GetResourceFields(resourceContext) : inputFields.Intersect(outputExpression.Fields); _visitedTable[resourceContext] = outputFields; } @@ -141,25 +135,23 @@ public IReadOnlyCollection GetSparseFieldSetForSerialize return _visitedTable[resourceContext]; } -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type - private HashSet GetResourceFields(ResourceContext resourceContext) -#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type + private IImmutableSet GetResourceFields(ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - var fieldSet = new HashSet(); + ImmutableHashSet.Builder fieldSetBuilder = ImmutableHashSet.CreateBuilder(); foreach (AttrAttribute attribute in resourceContext.Attributes.Where(attr => attr.Capabilities.HasFlag(AttrCapabilities.AllowView))) { - fieldSet.Add(attribute); + fieldSetBuilder.Add(attribute); } foreach (RelationshipAttribute relationship in resourceContext.Relationships) { - fieldSet.Add(relationship); + fieldSetBuilder.Add(relationship); } - return fieldSet; + return fieldSetBuilder.ToImmutable(); } public void Reset() diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs index 0faa4d6ff4..a58a16fdd3 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs @@ -30,7 +30,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); + !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Defaults); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index a7732f108a..899363178b 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -23,8 +24,8 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil private readonly IJsonApiOptions _options; private readonly QueryStringParameterScopeParser _scopeParser; private readonly FilterParser _filterParser; - private readonly List _filtersInGlobalScope = new(); - private readonly Dictionary> _filtersPerScope = new(); + private readonly ImmutableArray.Builder _filtersInGlobalScope = ImmutableArray.CreateBuilder(); + private readonly Dictionary.Builder> _filtersPerScope = new(); private string _lastParameterName; @@ -53,7 +54,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Filter); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Filter); } /// @@ -142,7 +143,7 @@ private void StoreFilterInScope(FilterExpression filter, ResourceFieldChainExpre { if (!_filtersPerScope.ContainsKey(scope)) { - _filtersPerScope[scope] = new List(); + _filtersPerScope[scope] = ImmutableArray.CreateBuilder(); } _filtersPerScope[scope].Add(filter); @@ -159,18 +160,18 @@ private IEnumerable EnumerateFiltersInScopes() { if (_filtersInGlobalScope.Any()) { - FilterExpression filter = MergeFilters(_filtersInGlobalScope); + FilterExpression filter = MergeFilters(_filtersInGlobalScope.ToImmutable()); yield return new ExpressionInScope(null, filter); } - foreach ((ResourceFieldChainExpression scope, List filters) in _filtersPerScope) + foreach ((ResourceFieldChainExpression scope, ImmutableArray.Builder filtersBuilder) in _filtersPerScope) { - FilterExpression filter = MergeFilters(filters); + FilterExpression filter = MergeFilters(filtersBuilder.ToImmutable()); yield return new ExpressionInScope(scope, filter); } } - private static FilterExpression MergeFilters(IReadOnlyCollection filters) + private static FilterExpression MergeFilters(IImmutableList filters) { return filters.Count > 1 ? new LogicalExpression(LogicalOperator.Or, filters) : filters.First(); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index c0fbb4a60d..afdabbfbe9 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -46,7 +46,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Include); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Include); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs index 3f20c6ba2a..aa40afbf25 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs @@ -30,7 +30,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerNullValueHandling && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); + !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Nulls); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index c8d61fc7ba..ae6d8c4e67 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -39,7 +39,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Page); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Page); } /// @@ -123,7 +123,7 @@ public virtual IReadOnlyCollection GetConstraints() var context = new PaginationContext(); foreach (PaginationElementQueryStringValueExpression element in _pageSizeConstraint?.Elements ?? - Array.Empty()) + ImmutableArray.Empty) { MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); entry.PageSize = element.Value == 0 ? null : new PageSize(element.Value); @@ -131,7 +131,7 @@ public virtual IReadOnlyCollection GetConstraints() } foreach (PaginationElementQueryStringValueExpression element in _pageNumberConstraint?.Elements ?? - Array.Empty()) + ImmutableArray.Empty) { MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); entry.PageNumber = new PageNumber(element.Value); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index 79c698627e..e6fb1226da 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; @@ -34,7 +33,7 @@ protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpressio return RequestResource; } - ResourceFieldAttribute lastField = scope.Fields.Last(); + ResourceFieldAttribute lastField = scope.Fields[^1]; Type type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; return _resourceContextProvider.GetResourceContext(type); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index e06b8431e1..d96efbb4f5 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -42,7 +42,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Sort); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Sort); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index 1355689bcd..08b8fa902f 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -20,7 +21,10 @@ public class SparseFieldSetQueryStringParameterReader : QueryStringParameterRead { private readonly SparseFieldTypeParser _sparseFieldTypeParser; private readonly SparseFieldSetParser _sparseFieldSetParser; - private readonly Dictionary _sparseFieldTable = new(); + + private readonly ImmutableDictionary.Builder _sparseFieldTableBuilder = + ImmutableDictionary.CreateBuilder(); + private string _lastParameterName; /// @@ -47,7 +51,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Fields); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Fields); } /// @@ -68,7 +72,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) ResourceContext targetResource = GetSparseFieldType(parameterName); SparseFieldSetExpression sparseFieldSet = GetSparseFieldSet(parameterValue, targetResource); - _sparseFieldTable[targetResource] = sparseFieldSet; + _sparseFieldTableBuilder[targetResource] = sparseFieldSet; } catch (QueryParseException exception) { @@ -89,7 +93,7 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour { // We add ID on an incoming empty fieldset, so that callers can distinguish between no fieldset and an empty one. AttrAttribute idAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Identifiable.Id)); - return new SparseFieldSetExpression(ArrayFactory.Create(idAttribute)); + return new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); } return sparseFieldSet; @@ -98,8 +102,8 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour /// public virtual IReadOnlyCollection GetConstraints() { - return _sparseFieldTable.Any() - ? new ExpressionInScope(null, new SparseFieldTableExpression(_sparseFieldTable)).AsArray() + return _sparseFieldTableBuilder.Any() + ? new ExpressionInScope(null, new SparseFieldTableExpression(_sparseFieldTableBuilder.ToImmutable())).AsArray() : Array.Empty(); } } diff --git a/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs similarity index 91% rename from src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs rename to src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs index 521a9af37b..95406014ef 100644 --- a/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.QueryStrings /// Lists query string parameters used by . /// [Flags] - public enum StandardQueryStringParameters + public enum JsonApiQueryStringParameters { None = 0, Filter = 1, diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 927665c965..2daa011d82 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -34,32 +34,31 @@ public class EntityFrameworkCoreRepository : IResourceRepository private readonly IResourceGraph _resourceGraph; private readonly IResourceFactory _resourceFactory; private readonly IEnumerable _constraintProviders; - private readonly TraceLogWriter> _traceWriter; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly TraceLogWriter> _traceWriter; /// public virtual string TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString(); public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) { - ArgumentGuard.NotNull(contextResolver, nameof(contextResolver)); - ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); + ArgumentGuard.NotNull(contextResolver, nameof(contextResolver)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); + ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _targetedFields = targetedFields; + _dbContext = contextResolver.GetContext(); _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; _constraintProviders = constraintProviders; - _dbContext = contextResolver.GetContext(); + _resourceDefinitionAccessor = resourceDefinitionAccessor; _traceWriter = new TraceLogWriter>(loggerFactory); - -#pragma warning disable 612 // Method is obsolete - _resourceDefinitionAccessor = resourceFactory.GetResourceDefinitionAccessor(); -#pragma warning restore 612 } /// @@ -104,14 +103,6 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) ArgumentGuard.NotNull(layer, nameof(layer)); - QueryLayer rewrittenLayer = layer; - - if (EntityFrameworkCoreSupport.Version.Major < 5) - { - var writer = new MemoryLeakDetectionBugRewriter(); - rewrittenLayer = writer.Rewrite(layer); - } - IQueryable source = GetAll(); // @formatter:wrap_chained_method_calls chop_always @@ -137,7 +128,7 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, _dbContext.Model); - Expression expression = builder.ApplyQuery(rewrittenLayer); + Expression expression = builder.ApplyQuery(layer); return source.Provider.CreateQuery(expression); } @@ -171,12 +162,12 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { - object rightResources = relationship.GetValue(resourceFromRequest); + object rightValue = relationship.GetValue(resourceFromRequest); - object rightResourcesEdited = await VisitSetRelationshipAsync(resourceForDatabase, relationship, rightResources, OperationKind.CreateResource, + object rightValueEvaluated = await VisitSetRelationshipAsync(resourceForDatabase, relationship, rightValue, WriteOperationKind.CreateResource, cancellationToken); - await UpdateRelationshipAsync(relationship, resourceForDatabase, rightResourcesEdited, collector, cancellationToken); + await UpdateRelationshipAsync(relationship, resourceForDatabase, rightValueEvaluated, collector, cancellationToken); } foreach (AttrAttribute attribute in _targetedFields.Attributes) @@ -184,36 +175,36 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest)); } - await _resourceDefinitionAccessor.OnWritingAsync(resourceForDatabase, OperationKind.CreateResource, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); DbSet dbSet = _dbContext.Set(); await dbSet.AddAsync(resourceForDatabase, cancellationToken); await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceForDatabase, OperationKind.CreateResource, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); } - private async Task VisitSetRelationshipAsync(TResource leftResource, RelationshipAttribute relationship, object rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + private async Task VisitSetRelationshipAsync(TResource leftResource, RelationshipAttribute relationship, object rightValue, + WriteOperationKind writeOperation, CancellationToken cancellationToken) { if (relationship is HasOneAttribute hasOneRelationship) { - return await _resourceDefinitionAccessor.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, (IIdentifiable)rightResourceIds, - operationKind, cancellationToken); + return await _resourceDefinitionAccessor.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, (IIdentifiable)rightValue, + writeOperation, cancellationToken); } if (relationship is HasManyAttribute hasManyRelationship) { - HashSet rightResourceIdSet = _collectionConverter.ExtractResources(rightResourceIds).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIdSet = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); - await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIdSet, operationKind, + await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIdSet, writeOperation, cancellationToken); return rightResourceIdSet; } - return rightResourceIds; + return rightValue; } /// @@ -239,14 +230,14 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { - object rightResources = relationship.GetValue(resourceFromRequest); + object rightValue = relationship.GetValue(resourceFromRequest); - object rightResourcesEdited = await VisitSetRelationshipAsync(resourceFromDatabase, relationship, rightResources, OperationKind.UpdateResource, + object rightValueEvaluated = await VisitSetRelationshipAsync(resourceFromDatabase, relationship, rightValue, WriteOperationKind.UpdateResource, cancellationToken); - AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightResourcesEdited); + AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightValueEvaluated); - await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightResourcesEdited, collector, cancellationToken); + await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightValueEvaluated, collector, cancellationToken); } foreach (AttrAttribute attribute in _targetedFields.Attributes) @@ -254,11 +245,11 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r attribute.SetValue(resourceFromDatabase, attribute.GetValue(resourceFromRequest)); } - await _resourceDefinitionAccessor.OnWritingAsync(resourceFromDatabase, OperationKind.UpdateResource, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(resourceFromDatabase, WriteOperationKind.UpdateResource, cancellationToken); await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceFromDatabase, OperationKind.UpdateResource, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceFromDatabase, WriteOperationKind.UpdateResource, cancellationToken); } protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute relationship, TResource leftResource, object rightValue) @@ -309,7 +300,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke var emptyResource = _resourceFactory.CreateInstance(); emptyResource.Id = id; - await _resourceDefinitionAccessor.OnWritingAsync(emptyResource, OperationKind.DeleteResource, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(emptyResource, WriteOperationKind.DeleteResource, cancellationToken); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); TResource resource = collector.CreateForId(id); @@ -329,7 +320,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(resource, OperationKind.DeleteResource, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(resource, WriteOperationKind.DeleteResource, cancellationToken); } private NavigationEntry GetNavigationEntry(TResource resource, RelationshipAttribute relationship) @@ -375,94 +366,96 @@ private bool HasForeignKeyAtLeftSide(RelationshipAttribute relationship, INaviga } /// - public virtual async Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + public virtual async Task SetRelationshipAsync(TResource leftResource, object rightValue, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryResource, - secondaryResourceIds + leftResource, + rightValue }); RelationshipAttribute relationship = _targetedFields.Relationships.Single(); - object secondaryResourceIdsEdited = - await VisitSetRelationshipAsync(primaryResource, relationship, secondaryResourceIds, OperationKind.SetRelationship, cancellationToken); + object rightValueEvaluated = + await VisitSetRelationshipAsync(leftResource, relationship, rightValue, WriteOperationKind.SetRelationship, cancellationToken); - AssertIsNotClearingRequiredRelationship(relationship, primaryResource, secondaryResourceIdsEdited); + AssertIsNotClearingRequiredRelationship(relationship, leftResource, rightValueEvaluated); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIdsEdited, collector, cancellationToken); + await UpdateRelationshipAsync(relationship, leftResource, rightValueEvaluated, collector, cancellationToken); - await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.SetRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.SetRelationship, cancellationToken); await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.SetRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(leftResource, WriteOperationKind.SetRelationship, cancellationToken); } /// - public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryId, - secondaryResourceIds + leftId, + rightResourceIds }); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); - await _resourceDefinitionAccessor.OnAddToRelationshipAsync(primaryId, relationship, secondaryResourceIds, cancellationToken); + await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftId, relationship, rightResourceIds, cancellationToken); - if (secondaryResourceIds.Any()) + if (rightResourceIds.Any()) { using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - TResource primaryResource = collector.CreateForId(primaryId); + TResource leftResource = collector.CreateForId(leftId); - await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIds, collector, cancellationToken); + await UpdateRelationshipAsync(relationship, leftResource, rightResourceIds, collector, cancellationToken); - await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.AddToRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.AddToRelationship, cancellationToken); await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.AddToRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(leftResource, WriteOperationKind.AddToRelationship, cancellationToken); } } /// - public virtual async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, + public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryResource, - secondaryResourceIds + leftResource, + rightResourceIds }); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); - await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(primaryResource, relationship, secondaryResourceIds, cancellationToken); + await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken); - if (secondaryResourceIds.Any()) + if (rightResourceIds.Any()) { - object rightValue = relationship.GetValue(primaryResource); + object rightValueStored = relationship.GetValue(leftResource); + + HashSet rightResourceIdsToStore = + _collectionConverter.ExtractResources(rightValueStored).ToHashSet(IdentifiableComparer.Instance); - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); - rightResourceIds.ExceptWith(secondaryResourceIds); + rightResourceIdsToStore.ExceptWith(rightResourceIds); - AssertIsNotClearingRequiredRelationship(relationship, primaryResource, rightResourceIds); + AssertIsNotClearingRequiredRelationship(relationship, leftResource, rightResourceIdsToStore); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - await UpdateRelationshipAsync(relationship, primaryResource, rightResourceIds, collector, cancellationToken); + await UpdateRelationshipAsync(relationship, leftResource, rightResourceIdsToStore, collector, cancellationToken); - await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.RemoveFromRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken); await SaveChangesAsync(cancellationToken); - await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.RemoveFromRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnWriteSucceededAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken); } } @@ -533,8 +526,9 @@ public class EntityFrameworkCoreRepository : EntityFrameworkCoreRepos where TResource : class, IIdentifiable { public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs deleted file mode 100644 index 042c4613ce..0000000000 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; - -#pragma warning disable AV1008 // Class should not be static - -namespace JsonApiDotNetCore.Repositories -{ - internal static class EntityFrameworkCoreSupport - { - public static Version Version { get; } = typeof(DbContext).Assembly.GetName().Version; - } -} diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs index 607512c242..4925697112 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs @@ -63,20 +63,19 @@ Task DeleteAsync(TId id, CancellationToken cancellationToken) /// /// Invokes . /// - Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + Task SetRelationshipAsync(TResource leftResource, object rightValue, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// /// Invokes for the specified resource type. /// - Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// /// Invokes . /// - Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, - CancellationToken cancellationToken) + Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs index 049e02ce8c..9cdfee02fd 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs @@ -57,16 +57,16 @@ public interface IResourceWriteRepository /// /// Performs a complete replacement of the relationship in the underlying data store. /// - Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken); + Task SetRelationshipAsync(TResource leftResource, object rightValue, CancellationToken cancellationToken); /// /// Adds resources to a to-many relationship in the underlying data store. /// - Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken); + Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken); /// /// Removes resources from a to-many relationship in the underlying data store. /// - Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken); + Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs b/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs deleted file mode 100644 index 557f330791..0000000000 --- a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Repositories -{ - /// - /// Removes projections from a when its resource type uses injected parameters, as a workaround for EF Core bug - /// https://github.com/dotnet/efcore/issues/20502, which exists in versions below v5. - /// - /// - /// Note that by using this workaround, nested filtering, paging and sorting all remain broken in EF Core 3.1 when using injected parameters in - /// resources. But at least it enables simple top-level queries to succeed without an exception. - /// - [PublicAPI] - public sealed class MemoryLeakDetectionBugRewriter - { - public QueryLayer Rewrite(QueryLayer queryLayer) - { - ArgumentGuard.NotNull(queryLayer, nameof(queryLayer)); - - return RewriteLayer(queryLayer); - } - - private QueryLayer RewriteLayer(QueryLayer queryLayer) - { - if (queryLayer != null) - { - queryLayer.Projection = RewriteProjection(queryLayer.Projection, queryLayer.ResourceContext); - } - - return queryLayer; - } - - private IDictionary RewriteProjection(IDictionary projection, - ResourceContext resourceContext) - { - if (projection.IsNullOrEmpty()) - { - return projection; - } - - var newProjection = new Dictionary(); - - foreach ((ResourceFieldAttribute field, QueryLayer layer) in projection) - { - QueryLayer newLayer = RewriteLayer(layer); - newProjection.Add(field, newLayer); - } - - if (!ResourceFactory.HasSingleConstructorWithoutParameters(resourceContext.ResourceType)) - { - return null; - } - - return newProjection; - } - } -} diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index 25de9a3409..c23ca04fd1 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -98,29 +98,28 @@ public async Task DeleteAsync(TId id, CancellationToken cancella } /// - public async Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + public async Task SetRelationshipAsync(TResource leftResource, object rightValue, CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - await repository.SetRelationshipAsync(primaryResource, secondaryResourceIds, cancellationToken); + await repository.SetRelationshipAsync(leftResource, rightValue, cancellationToken); } /// - public async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, - CancellationToken cancellationToken) + public async Task AddToToManyRelationshipAsync(TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - await repository.AddToToManyRelationshipAsync(primaryId, secondaryResourceIds, cancellationToken); + await repository.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken); } /// - public async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, + public async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); - await repository.RemoveFromToManyRelationshipAsync(primaryResource, secondaryResourceIds, cancellationToken); + await repository.RemoveFromToManyRelationshipAsync(leftResource, rightResourceIds, cancellationToken); } protected virtual object ResolveReadRepository(Type resourceType) @@ -148,7 +147,7 @@ private object GetWriteRepository(Type resourceType) if (_request.TransactionId != null) { - if (!(writeRepository is IRepositorySupportsTransaction repository)) + if (writeRepository is not IRepositorySupportsTransaction repository) { ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); throw new MissingTransactionSupportException(resourceContext.PublicName); diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs index 17a84eda4d..eaeb10360d 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -36,13 +36,13 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute public PropertyInfo InverseNavigationProperty { get; set; } /// - /// The parent resource type. This is the type of the class in which this attribute was used. + /// The containing type in which this relationship is declared. /// public Type LeftType { get; internal set; } /// - /// The child resource type. This does not necessarily match the navigation property type. In the case of a relationship, - /// this value will be the collection element type. + /// The type this relationship points to. This does not necessarily match the relationship property type. In the case of a + /// relationship, this value will be the collection element type. /// /// /// : IResourceDefinition [PublicAPI] - // ReSharper disable once TypeParameterCanBeVariant -- Justification: making TId contravariant is a breaking change. - public interface IResourceDefinition + public interface IResourceDefinition where TResource : class, IIdentifiable { /// @@ -44,7 +44,7 @@ public interface IResourceDefinition /// /// The new set of includes. Return an empty collection to remove all inclusions (never return null). /// - IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes); + IImmutableList OnApplyIncludes(IImmutableList existingIncludes); /// /// Enables to extend, replace or remove a filter that is being applied on a set of this resource type. @@ -147,16 +147,16 @@ public interface IResourceDefinition /// /// The original resource retrieved from the underlying data store, or a freshly instantiated resource in case of a POST resource request. /// - /// - /// Identifies from which endpoint this method was called. Possible values: , - /// , and . - /// Note this intentionally excludes and , because for those - /// endpoints no resource is retrieved upfront. + /// + /// Identifies the logical write operation for which this method was called. Possible values: , + /// , and + /// . Note this intentionally excludes and + /// , because for those endpoints no resource is retrieved upfront. /// /// /// Propagates notification that request handling should be canceled. /// - Task OnPrepareWriteAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken); + Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken); /// /// Executes before setting (or clearing) the resource at the right side of a to-one relationship. @@ -174,9 +174,9 @@ public interface IResourceDefinition /// /// The new resource identifier (or null to clear the relationship), coming from the request. /// - /// - /// Identifies from which endpoint this method was called. Possible values: , - /// and . + /// + /// Identifies the logical write operation for which this method was called. Possible values: , + /// and . /// /// /// Propagates notification that request handling should be canceled. @@ -185,7 +185,7 @@ public interface IResourceDefinition /// The replacement resource identifier, or null to clear the relationship. Returns by default. /// Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, IIdentifiable rightResourceId, - OperationKind operationKind, CancellationToken cancellationToken); + WriteOperationKind writeOperation, CancellationToken cancellationToken); /// /// Executes before setting the resources at the right side of a to-many relationship. This replaces on existing set. @@ -203,15 +203,15 @@ Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAt /// /// The set of resource identifiers to replace any existing set with, coming from the request. /// - /// - /// Identifies from which endpoint this method was called. Possible values: , - /// and . + /// + /// Identifies the logical write operation for which this method was called. Possible values: , + /// and . /// /// /// Propagates notification that request handling should be canceled. /// Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken); + WriteOperationKind writeOperation, CancellationToken cancellationToken); /// /// Executes before adding resources to the right side of a to-many relationship, as part of a POST relationship request. @@ -273,19 +273,19 @@ Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasM /// /// /// The original resource retrieved from the underlying data store (or a freshly instantiated resource in case of a POST resource request), updated with - /// the changes from the incoming request. Exception: In case is or - /// , this is an empty object with only the property set, because for - /// those endpoints no resource is retrieved upfront. + /// the changes from the incoming request. Exception: In case is or + /// , this is an empty object with only the property set, because + /// for those endpoints no resource is retrieved upfront. /// - /// - /// Identifies from which endpoint this method was called. Possible values: , - /// , , , - /// and . + /// + /// Identifies the logical write operation for which this method was called. Possible values: , + /// , , + /// , and . /// /// /// Propagates notification that request handling should be canceled. /// - Task OnWritingAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken); + Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken); /// /// Executes after successfully writing the changed resource to the underlying data store, as part of a write request. @@ -296,15 +296,15 @@ Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasM /// /// The resource as written to the underlying data store. /// - /// - /// Identifies from which endpoint this method was called. Possible values: , - /// , , , - /// and . + /// + /// Identifies the logical write operation for which this method was called. Possible values: , + /// , , + /// , and . /// /// /// Propagates notification that request handling should be canceled. /// - Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken); + Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken); /// /// Executes after a resource has been deserialized from an incoming request body. diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index 042a5553c4..8c804d3602 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,7 +18,7 @@ public interface IResourceDefinitionAccessor /// /// Invokes for the specified resource type. /// - IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes); + IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes); /// /// Invokes for the specified resource type. @@ -53,21 +54,21 @@ public interface IResourceDefinitionAccessor /// /// Invokes for the specified resource. /// - Task OnPrepareWriteAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// /// Invokes for the specified resource. /// public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable rightResourceId, OperationKind operationKind, CancellationToken cancellationToken) + IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// /// Invokes for the specified resource. /// public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// @@ -87,13 +88,13 @@ public Task OnRemoveFromRelationshipAsync(TResource leftResource, Has /// /// Invokes for the specified resource. /// - Task OnWritingAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// /// Invokes for the specified resource. /// - Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// diff --git a/src/JsonApiDotNetCore/Resources/IResourceFactory.cs b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs index cd520b4f6f..38a25ad996 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs @@ -1,6 +1,5 @@ using System; using System.Linq.Expressions; -using JsonApiDotNetCore.Repositories; namespace JsonApiDotNetCore.Resources { @@ -24,12 +23,5 @@ public TResource CreateInstance() /// Returns an expression tree that represents creating a new resource object instance. /// public NewExpression CreateNewExpression(Type resourceType); - - /// - /// Provides access to the request-scoped instance. This method has been added solely to prevent introducing a - /// breaking change in the constructor and will be removed in the next major version. - /// - [Obsolete] - IResourceDefinitionAccessor GetResourceDefinitionAccessor(); } } diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 3498d74a7a..755ce781cb 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; @@ -51,7 +52,7 @@ public JsonApiResourceDefinition(IResourceGraph resourceGraph) } /// - public virtual IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + public virtual IImmutableList OnApplyIncludes(IImmutableList existingIncludes) { return existingIncludes; } @@ -84,7 +85,7 @@ protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySel { ArgumentGuard.NotNull(keySelectors, nameof(keySelectors)); - var sortElements = new List(); + ImmutableArray.Builder elementsBuilder = ImmutableArray.CreateBuilder(keySelectors.Count); foreach ((Expression> keySelector, ListSortDirection sortDirection) in keySelectors) { @@ -92,10 +93,10 @@ protected SortExpression CreateSortExpressionFromLambda(PropertySortOrder keySel AttrAttribute attribute = ResourceGraph.GetAttributes(keySelector).Single(); var sortElement = new SortElementExpression(new ResourceFieldChainExpression(attribute), isAscending); - sortElements.Add(sortElement); + elementsBuilder.Add(sortElement); } - return new SortExpression(sortElements); + return new SortExpression(elementsBuilder.ToImmutable()); } /// @@ -123,21 +124,21 @@ public virtual IDictionary GetMeta(TResource resource) } /// - public virtual Task OnPrepareWriteAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public virtual Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { return Task.CompletedTask; } /// public virtual Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable rightResourceId, OperationKind operationKind, CancellationToken cancellationToken) + IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) { return Task.FromResult(rightResourceId); } /// public virtual Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { return Task.CompletedTask; } @@ -157,13 +158,13 @@ public virtual Task OnRemoveFromRelationshipAsync(TResource leftResource, HasMan } /// - public virtual Task OnWritingAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public virtual Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { return Task.CompletedTask; } /// - public virtual Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public virtual Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index 122556d816..11e6636ccf 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -13,12 +13,12 @@ public sealed class OperationContainer { private static readonly CollectionConverter CollectionConverter = new(); - public OperationKind Kind { get; } + public WriteOperationKind Kind { get; } public IIdentifiable Resource { get; } public ITargetedFields TargetedFields { get; } public IJsonApiRequest Request { get; } - public OperationContainer(OperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) + public OperationContainer(WriteOperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index b6ed7774ca..271f14ad5b 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -28,7 +29,7 @@ public ResourceDefinitionAccessor(IResourceContextProvider resourceContextProvid } /// - public IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes) + public IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); @@ -94,30 +95,30 @@ public IDictionary GetMeta(Type resourceType, IIdentifiable reso } /// - public async Task OnPrepareWriteAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public async Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(resource, nameof(resource)); dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnPrepareWriteAsync(resource, operationKind, cancellationToken); + await resourceDefinition.OnPrepareWriteAsync(resource, writeOperation, cancellationToken); } /// public async Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable rightResourceId, OperationKind operationKind, CancellationToken cancellationToken) + IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(leftResource, nameof(leftResource)); ArgumentGuard.NotNull(hasOneRelationship, nameof(hasOneRelationship)); dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - return await resourceDefinition.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, rightResourceId, operationKind, cancellationToken); + return await resourceDefinition.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, rightResourceId, writeOperation, cancellationToken); } /// public async Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, - ISet rightResourceIds, OperationKind operationKind, CancellationToken cancellationToken) + ISet rightResourceIds, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(leftResource, nameof(leftResource)); @@ -125,7 +126,7 @@ public async Task OnSetToManyRelationshipAsync(TResource leftResource ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, operationKind, cancellationToken); + await resourceDefinition.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); } /// @@ -154,23 +155,23 @@ public async Task OnRemoveFromRelationshipAsync(TResource leftResourc } /// - public async Task OnWritingAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public async Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(resource, nameof(resource)); dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnWritingAsync(resource, operationKind, cancellationToken); + await resourceDefinition.OnWritingAsync(resource, writeOperation, cancellationToken); } /// - public async Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public async Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { ArgumentGuard.NotNull(resource, nameof(resource)); dynamic resourceDefinition = ResolveResourceDefinition(typeof(TResource)); - await resourceDefinition.OnWriteSucceededAsync(resource, operationKind, cancellationToken); + await resourceDefinition.OnWriteSucceededAsync(resource, writeOperation, cancellationToken); } /// diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 721022d6f5..6ab75ded5b 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -19,12 +19,6 @@ public ResourceFactory(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - /// - public IResourceDefinitionAccessor GetResourceDefinitionAccessor() - { - return _serviceProvider.GetRequiredService(); - } - /// public IIdentifiable CreateInstance(Type resourceType) { @@ -111,7 +105,7 @@ private static Expression CreateTupleAccessExpressionForConstant(object value, T return Expression.Property(tupleCreateCall, "Item1"); } - internal static bool HasSingleConstructorWithoutParameters(Type type) + private static bool HasSingleConstructorWithoutParameters(Type type) { ConstructorInfo[] constructors = type.GetConstructors().Where(constructor => !constructor.IsStatic).ToArray(); diff --git a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs index 17a492f9b2..86462aee54 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs @@ -15,13 +15,13 @@ public interface ILinkBuilder TopLevelLinks GetTopLevelLinks(); /// - /// Builds the links object for resources in the primary data. + /// Builds the links object for a returned resource (primary or included). /// ResourceLinks GetResourceLinks(string resourceName, string id); /// - /// Builds the links object that is included in the values of the . + /// Builds the links object for a relationship inside a returned resource. /// - RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent); + RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource); } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 170eb2d97c..451a3f0f7e 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -92,7 +93,7 @@ private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); - IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index 542923368c..3ffcedbdfe 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -141,27 +142,20 @@ private string CalculatePageSizeValue(PageSize topPageSize, ResourceContext requ private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPageSize, ResourceContext requestContext) { - IList elements = ParsePageSizeExpression(pageSizeParameterValue, requestContext); + IImmutableList elements = ParsePageSizeExpression(pageSizeParameterValue, requestContext); int elementInTopScopeIndex = elements.FindIndex(expression => expression.Scope == null); if (topPageSize != null) { var topPageSizeElement = new PaginationElementQueryStringValueExpression(null, topPageSize.Value); - if (elementInTopScopeIndex != -1) - { - elements[elementInTopScopeIndex] = topPageSizeElement; - } - else - { - elements.Insert(0, topPageSizeElement); - } + elements = elementInTopScopeIndex != -1 ? elements.SetItem(elementInTopScopeIndex, topPageSizeElement) : elements.Insert(0, topPageSizeElement); } else { if (elementInTopScopeIndex != -1) { - elements.RemoveAt(elementInTopScopeIndex); + elements = elements.RemoveAt(elementInTopScopeIndex); } } @@ -171,17 +165,18 @@ private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPage return parameterValue == string.Empty ? null : parameterValue; } - private IList ParsePageSizeExpression(string pageSizeParameterValue, ResourceContext requestResource) + private IImmutableList ParsePageSizeExpression(string pageSizeParameterValue, + ResourceContext requestResource) { if (pageSizeParameterValue == null) { - return new List(); + return ImmutableArray.Empty; } var parser = new PaginationParser(_resourceContextProvider); PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); - return paginationExpression.Elements.ToList(); + return paginationExpression.Elements; } private string GetLinkForPagination(int pageOffset, string pageSizeValue) @@ -269,39 +264,39 @@ private string GetLinkForResourceSelf(ResourceContext resourceContext, string re } /// - public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent) + public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); - ArgumentGuard.NotNull(parent, nameof(parent)); + ArgumentGuard.NotNull(leftResource, nameof(leftResource)); var links = new RelationshipLinks(); - ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(parent.GetType()); + ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(leftResource.GetType()); if (ShouldIncludeRelationshipLink(LinkTypes.Self, relationship, leftResourceContext)) { - links.Self = GetLinkForRelationshipSelf(parent.StringId, relationship); + links.Self = GetLinkForRelationshipSelf(leftResource.StringId, relationship); } if (ShouldIncludeRelationshipLink(LinkTypes.Related, relationship, leftResourceContext)) { - links.Related = GetLinkForRelationshipRelated(parent.StringId, relationship); + links.Related = GetLinkForRelationshipRelated(leftResource.StringId, relationship); } return links.HasValue() ? links : null; } - private string GetLinkForRelationshipSelf(string primaryId, RelationshipAttribute relationship) + private string GetLinkForRelationshipSelf(string leftId, RelationshipAttribute relationship) { string controllerName = _controllerResourceMapping.GetControllerNameForResourceType(relationship.LeftType); - IDictionary routeValues = GetRouteValues(primaryId, relationship.PublicName); + IDictionary routeValues = GetRouteValues(leftId, relationship.PublicName); return RenderLinkForAction(controllerName, GetRelationshipControllerActionName, routeValues); } - private string GetLinkForRelationshipRelated(string primaryId, RelationshipAttribute relationship) + private string GetLinkForRelationshipRelated(string leftId, RelationshipAttribute relationship) { string controllerName = _controllerResourceMapping.GetControllerNameForResourceType(relationship.LeftType); - IDictionary routeValues = GetRouteValues(primaryId, relationship.PublicName); + IDictionary routeValues = GetRouteValues(leftId, relationship.PublicName); return RenderLinkForAction(controllerName, GetSecondaryControllerActionName, routeValues); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index d6633910ba..787ce2a53b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -133,11 +133,11 @@ private IList GetRelatedResourceLinkageForHasMany(HasM /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - string resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; + string publicName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; return new ResourceIdentifierObject { - Type = resourceName, + Type = publicName, Id = resource.StringId }; } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index 02aebcdc2e..e23eef6a82 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -113,7 +114,7 @@ private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); - IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); } @@ -129,7 +130,7 @@ private IReadOnlyCollection> GetInclu foreach (ResourceFieldChainExpression chain in chains) { - if (chain.Fields.First().Equals(relationship)) + if (chain.Fields[0].Equals(relationship)) { inclusionChains.Add(chain.Fields.Cast().ToArray()); } diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index 763ee59b6c..d4ace8450d 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -44,9 +45,20 @@ public IReadOnlyCollection GetAttributes(Type resourceType) } ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); - IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); - return fieldSet.OfType().ToArray(); + return SortAttributesInDeclarationOrder(fieldSet, resourceContext).ToArray(); + } + + private IEnumerable SortAttributesInDeclarationOrder(IImmutableSet fieldSet, ResourceContext resourceContext) + { + foreach (AttrAttribute attribute in resourceContext.Attributes) + { + if (fieldSet.Contains(attribute)) + { + yield return attribute; + } + } } /// diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 1841da6987..d0dac14349 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -50,7 +50,7 @@ public async Task ReadAsync(InputFormatterContext context) string body = await GetRequestBodyAsync(context.HttpContext.Request.Body); string url = context.HttpContext.Request.GetEncodedUrl(); - _traceWriter.LogMessage(() => $"Received request at '{url}' with body: <<{body}>>"); + _traceWriter.LogMessage(() => $"Received {context.HttpContext.Request.Method} request at '{url}' with body: <<{body}>>"); object model = null; @@ -258,14 +258,14 @@ private void ValidateForRelationshipType(string requestMethod, object model, str throw new ToManyRelationshipRequiredException(_request.Relationship.PublicName); } - if (model != null && !(model is IIdentifiable)) + if (model is { } and not IIdentifiable) { throw new InvalidRequestBodyException("Expected single data element for to-one relationship.", $"Expected single data element for '{_request.Relationship.PublicName}' relationship.", body); } } - if (_request.Relationship is HasManyAttribute && !(model is IEnumerable)) + if (_request.Relationship is HasManyAttribute && model is not IEnumerable) { throw new InvalidRequestBodyException("Expected data[] element for to-many relationship.", $"Expected data[] element for '{_request.Relationship.PublicName}' relationship.", body); diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index d7ea3aabc1..ab372fea3f 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -29,22 +29,20 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, - IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options) + IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor) : base(resourceContextProvider, resourceFactory) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(options, nameof(options)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _targetedFields = targetedFields; _httpContextAccessor = httpContextAccessor; _request = request; _options = options; - -#pragma warning disable 612 // Method is obsolete - _resourceDefinitionAccessor = resourceFactory.GetResourceDefinitionAccessor(); -#pragma warning restore 612 + _resourceDefinitionAccessor = resourceDefinitionAccessor; } /// @@ -111,24 +109,25 @@ private OperationContainer DeserializeOperation(AtomicOperationObject operation) AssertHasNoHref(operation); - OperationKind kind = GetOperationKind(operation); + WriteOperationKind writeOperation = GetWriteOperationKind(operation); - switch (kind) + switch (writeOperation) { - case OperationKind.CreateResource: - case OperationKind.UpdateResource: + case WriteOperationKind.CreateResource: + case WriteOperationKind.UpdateResource: { - return ParseForCreateOrUpdateResourceOperation(operation, kind); + return ParseForCreateOrUpdateResourceOperation(operation, writeOperation); } - case OperationKind.DeleteResource: + case WriteOperationKind.DeleteResource: { - return ParseForDeleteResourceOperation(operation, kind); + return ParseForDeleteResourceOperation(operation, writeOperation); } } - bool requireToManyRelationship = kind == OperationKind.AddToRelationship || kind == OperationKind.RemoveFromRelationship; + bool requireToManyRelationship = + writeOperation == WriteOperationKind.AddToRelationship || writeOperation == WriteOperationKind.RemoveFromRelationship; - return ParseForRelationshipOperation(operation, kind, requireToManyRelationship); + return ParseForRelationshipOperation(operation, writeOperation, requireToManyRelationship); } [AssertionMethod] @@ -140,7 +139,7 @@ private void AssertHasNoHref(AtomicOperationObject operation) } } - private OperationKind GetOperationKind(AtomicOperationObject operation) + private WriteOperationKind GetWriteOperationKind(AtomicOperationObject operation) { switch (operation.Code) { @@ -152,11 +151,11 @@ private OperationKind GetOperationKind(AtomicOperationObject operation) atomicOperationIndex: AtomicOperationIndex); } - return operation.Ref == null ? OperationKind.CreateResource : OperationKind.AddToRelationship; + return operation.Ref == null ? WriteOperationKind.CreateResource : WriteOperationKind.AddToRelationship; } case AtomicOperationCode.Update: { - return operation.Ref?.Relationship != null ? OperationKind.SetRelationship : OperationKind.UpdateResource; + return operation.Ref?.Relationship != null ? WriteOperationKind.SetRelationship : WriteOperationKind.UpdateResource; } case AtomicOperationCode.Remove: { @@ -165,19 +164,19 @@ private OperationKind GetOperationKind(AtomicOperationObject operation) throw new JsonApiSerializationException("The 'ref' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } - return operation.Ref.Relationship != null ? OperationKind.RemoveFromRelationship : OperationKind.DeleteResource; + return operation.Ref.Relationship != null ? WriteOperationKind.RemoveFromRelationship : WriteOperationKind.DeleteResource; } } throw new NotSupportedException($"Unknown operation code '{operation.Code}'."); } - private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperationObject operation, OperationKind kind) + private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperationObject operation, WriteOperationKind writeOperation) { ResourceObject resourceObject = GetRequiredSingleDataForResourceOperation(operation); AssertElementHasType(resourceObject, "data"); - AssertElementHasIdOrLid(resourceObject, "data", kind != OperationKind.CreateResource); + AssertElementHasIdOrLid(resourceObject, "data", writeOperation != WriteOperationKind.CreateResource); ResourceContext primaryResourceContext = GetExistingResourceContext(resourceObject.Type); @@ -192,7 +191,7 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati ResourceContext resourceContextInRef = GetExistingResourceContext(operation.Ref.Type); - if (resourceContextInRef != primaryResourceContext) + if (!primaryResourceContext.Equals(resourceContextInRef)) { throw new JsonApiSerializationException("Resource type mismatch between 'ref.type' and 'data.type' element.", $"Expected resource of type '{resourceContextInRef.PublicName}' in 'data.type', instead of '{primaryResourceContext.PublicName}'.", @@ -205,11 +204,8 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati var request = new JsonApiRequest { Kind = EndpointKind.AtomicOperations, -#pragma warning disable CS0618 // Type or member is obsolete - BasePath = _request.BasePath, -#pragma warning restore CS0618 // Type or member is obsolete PrimaryResource = primaryResourceContext, - OperationKind = kind + WriteOperation = writeOperation }; _request.CopyFrom(request); @@ -229,7 +225,7 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati AssertResourceIdIsNotTargeted(targetedFields); - return new OperationContainer(kind, primaryResource, targetedFields, request); + return new OperationContainer(writeOperation, primaryResource, targetedFields, request); } private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperationObject operation) @@ -315,7 +311,7 @@ private void AssertSameIdentityInRefData(AtomicOperationObject operation, Resour } } - private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject operation, OperationKind kind) + private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject operation, WriteOperationKind writeOperation) { AssertElementHasType(operation.Ref, "ref"); AssertElementHasIdOrLid(operation.Ref, "ref", true); @@ -331,18 +327,15 @@ private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject var request = new JsonApiRequest { Kind = EndpointKind.AtomicOperations, -#pragma warning disable CS0618 // Type or member is obsolete - BasePath = _request.BasePath, -#pragma warning restore CS0618 // Type or member is obsolete PrimaryId = primaryResource.StringId, PrimaryResource = primaryResourceContext, - OperationKind = kind + WriteOperation = writeOperation }; - return new OperationContainer(kind, primaryResource, new TargetedFields(), request); + return new OperationContainer(writeOperation, primaryResource, new TargetedFields(), request); } - private OperationContainer ParseForRelationshipOperation(AtomicOperationObject operation, OperationKind kind, bool requireToMany) + private OperationContainer ParseForRelationshipOperation(AtomicOperationObject operation, WriteOperationKind writeOperation, bool requireToMany) { AssertElementHasType(operation.Ref, "ref"); AssertElementHasIdOrLid(operation.Ref, "ref", true); @@ -368,15 +361,12 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o var request = new JsonApiRequest { Kind = EndpointKind.AtomicOperations, -#pragma warning disable CS0618 // Type or member is obsolete - BasePath = _request.BasePath, -#pragma warning restore CS0618 // Type or member is obsolete PrimaryId = primaryResource.StringId, PrimaryResource = primaryResourceContext, SecondaryResource = secondaryResourceContext, Relationship = relationship, IsCollection = relationship is HasManyAttribute, - OperationKind = kind + WriteOperation = writeOperation }; _request.CopyFrom(request); @@ -391,7 +381,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o Relationships = _targetedFields.Relationships.ToHashSet() }; - return new OperationContainer(kind, primaryResource, targetedFields, request); + return new OperationContainer(writeOperation, primaryResource, targetedFields, request); } private RelationshipAttribute GetExistingRelationship(AtomicReference reference, ResourceContext resourceContext) @@ -522,14 +512,14 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA private bool IsCreatingResource() { return _request.Kind == EndpointKind.AtomicOperations - ? _request.OperationKind == OperationKind.CreateResource + ? _request.WriteOperation == WriteOperationKind.CreateResource : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext!.Request.Method == HttpMethod.Post.Method; } private bool IsUpdatingResource() { return _request.Kind == EndpointKind.AtomicOperations - ? _request.OperationKind == OperationKind.UpdateResource + ? _request.WriteOperation == WriteOperationKind.UpdateResource : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext!.Request.Method == HttpMethod.Patch.Method; } } diff --git a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs index 5bbc74ae71..2fb6f82e8a 100644 --- a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs @@ -22,19 +22,18 @@ public interface IAddToRelationshipService /// /// Handles a JSON:API request to add resources to a to-many relationship. /// - /// - /// The identifier of the primary resource. + /// + /// Identifies the left side of the relationship. /// /// /// The relationship to add resources to. /// - /// + /// /// The set of resources to add to the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, - CancellationToken cancellationToken); + Task AddToToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs index c322732cc7..12d0889ce1 100644 --- a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs @@ -20,19 +20,18 @@ public interface IRemoveFromRelationshipService /// /// Handles a JSON:API request to remove resources from a to-many relationship. /// - /// - /// The identifier of the primary resource. + /// + /// Identifies the left side of the relationship. /// /// /// The relationship to remove resources from. /// - /// + /// /// The set of resources to remove from the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, - CancellationToken cancellationToken); + Task RemoveFromToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs index 28904f1a28..2afc8a175a 100644 --- a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs @@ -19,18 +19,18 @@ public interface ISetRelationshipService /// /// Handles a JSON:API request to perform a complete replacement of a relationship on an existing resource. /// - /// - /// The identifier of the primary resource. + /// + /// Identifies the left side of the relationship. /// /// /// The relationship for which to perform a complete replacement. /// - /// + /// /// The resource or set of resources to assign to the relationship. /// /// /// Propagates notification that request handling should be canceled. /// - Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken); + Task SetRelationshipAsync(TId leftId, string relationshipName, object rightValue, CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 92064666dc..8ae06cd907 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -35,7 +35,7 @@ public class JsonApiResourceService : IResourceService resourceChangeTracker) + IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) { ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor)); ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer)); @@ -44,6 +44,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker)); + ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); _repositoryAccessor = repositoryAccessor; _queryLayerComposer = queryLayerComposer; @@ -51,11 +52,8 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ _options = options; _request = request; _resourceChangeTracker = resourceChangeTracker; + _resourceDefinitionAccessor = resourceDefinitionAccessor; _traceWriter = new TraceLogWriter>(loggerFactory); - -#pragma warning disable 612 // Method is obsolete - _resourceDefinitionAccessor = queryLayerComposer.GetResourceDefinitionAccessor(); -#pragma warning restore 612 } /// @@ -115,14 +113,14 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN TResource primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); - object secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); + object rightValue = _request.Relationship.GetValue(primaryResource); - if (secondaryResourceOrResources is ICollection secondaryResources && secondaryLayer.Pagination?.PageSize?.Value == secondaryResources.Count) + if (rightValue is ICollection rightResources && secondaryLayer.Pagination?.PageSize?.Value == rightResources.Count) { _paginationContext.IsPageFull = true; } - return secondaryResourceOrResources; + return rightValue; } /// @@ -199,7 +197,7 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio protected virtual async Task InitializeResourceAsync(TResource resourceForDatabase, CancellationToken cancellationToken) { - await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceForDatabase, OperationKind.CreateResource, cancellationToken); + await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); } protected async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource primaryResource, CancellationToken cancellationToken) @@ -243,65 +241,64 @@ private async IAsyncEnumerable GetMissingRightRes } /// - public virtual async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + public virtual async Task AddToToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryId, - secondaryResourceIds + leftId, + rightResourceIds }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); AssertHasRelationship(_request.Relationship, relationshipName); - if (secondaryResourceIds.Any() && _request.Relationship is HasManyAttribute { IsManyToMany: true } manyToManyRelationship) + if (rightResourceIds.Any() && _request.Relationship is HasManyAttribute { IsManyToMany: true } manyToManyRelationship) { // In the case of a many-to-many relationship, creating a duplicate entry in the join table results in a // unique constraint violation. We avoid that by excluding already-existing entries from the set in advance. - await RemoveExistingIdsFromSecondarySetAsync(primaryId, secondaryResourceIds, manyToManyRelationship, cancellationToken); + await RemoveExistingIdsFromRelationshipRightSideAsync(manyToManyRelationship, leftId, rightResourceIds, cancellationToken); } try { - await _repositoryAccessor.AddToToManyRelationshipAsync(primaryId, secondaryResourceIds, cancellationToken); + await _repositoryAccessor.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken); } catch (DataStoreUpdateException) { - _ = await GetPrimaryResourceByIdAsync(primaryId, TopFieldSelection.OnlyIdAttribute, cancellationToken); - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + _ = await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); + await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); throw; } } - private async Task RemoveExistingIdsFromSecondarySetAsync(TId primaryId, ISet secondaryResourceIds, HasManyAttribute hasManyRelationship, - CancellationToken cancellationToken) + private async Task RemoveExistingIdsFromRelationshipRightSideAsync(HasManyAttribute hasManyRelationship, TId leftId, + ISet rightResourceIds, CancellationToken cancellationToken) { - QueryLayer queryLayer = _queryLayerComposer.ComposeForHasMany(hasManyRelationship, primaryId, secondaryResourceIds); - IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); + QueryLayer queryLayer = _queryLayerComposer.ComposeForHasMany(hasManyRelationship, leftId, rightResourceIds); + IReadOnlyCollection leftResources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); - TResource primaryResource = primaryResources.FirstOrDefault(); - AssertPrimaryResourceExists(primaryResource); + TResource leftResource = leftResources.FirstOrDefault(); + AssertPrimaryResourceExists(leftResource); - object rightValue = _request.Relationship.GetValue(primaryResource); + object rightValue = _request.Relationship.GetValue(leftResource); ICollection existingRightResourceIds = _collectionConverter.ExtractResources(rightValue); - secondaryResourceIds.ExceptWith(existingRightResourceIds); + rightResourceIds.ExceptWith(existingRightResourceIds); } - protected async Task AssertRightResourcesExistAsync(object rightResourceIds, CancellationToken cancellationToken) + protected async Task AssertRightResourcesExistAsync(object rightValue, CancellationToken cancellationToken) { - ICollection secondaryResourceIds = _collectionConverter.ExtractResources(rightResourceIds); + ICollection rightResourceIds = _collectionConverter.ExtractResources(rightValue); - if (secondaryResourceIds.Any()) + if (rightResourceIds.Any()) { - QueryLayer queryLayer = _queryLayerComposer.ComposeForGetRelationshipRightIds(_request.Relationship, secondaryResourceIds); + QueryLayer queryLayer = _queryLayerComposer.ComposeForGetRelationshipRightIds(_request.Relationship, rightResourceIds); List missingResources = - await GetMissingRightResourcesAsync(queryLayer, _request.Relationship, secondaryResourceIds, cancellationToken) - .ToListAsync(cancellationToken); + await GetMissingRightResourcesAsync(queryLayer, _request.Relationship, rightResourceIds, cancellationToken).ToListAsync(cancellationToken); if (missingResources.Any()) { @@ -328,7 +325,7 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can _resourceChangeTracker.SetInitiallyStoredAttributeValues(resourceFromDatabase); - await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, OperationKind.UpdateResource, cancellationToken); + await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, WriteOperationKind.UpdateResource, cancellationToken); try { @@ -349,30 +346,30 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can } /// - public virtual async Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) + public virtual async Task SetRelationshipAsync(TId leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryId, + leftId, relationshipName, - secondaryResourceIds + rightValue }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); AssertHasRelationship(_request.Relationship, relationshipName); - TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(primaryId, cancellationToken); + TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); - await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, OperationKind.SetRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, WriteOperationKind.SetRelationship, cancellationToken); try { - await _repositoryAccessor.SetRelationshipAsync(resourceFromDatabase, secondaryResourceIds, cancellationToken); + await _repositoryAccessor.SetRelationshipAsync(resourceFromDatabase, rightValue, cancellationToken); } catch (DataStoreUpdateException) { - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(rightValue, cancellationToken); throw; } } @@ -397,28 +394,28 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke } /// - public virtual async Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + public virtual async Task RemoveFromToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { _traceWriter.LogMethodStart(new { - primaryId, + leftId, relationshipName, - secondaryResourceIds + rightResourceIds }); ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); - ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); + ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); AssertHasRelationship(_request.Relationship, relationshipName); - TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(primaryId, cancellationToken); + TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); - await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, OperationKind.RemoveFromRelationship, cancellationToken); + await _resourceDefinitionAccessor.OnPrepareWriteAsync(resourceFromDatabase, WriteOperationKind.RemoveFromRelationship, cancellationToken); - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); - await _repositoryAccessor.RemoveFromToManyRelationshipAsync(resourceFromDatabase, secondaryResourceIds, cancellationToken); + await _repositoryAccessor.RemoveFromToManyRelationshipAsync(resourceFromDatabase, rightResourceIds, cancellationToken); } protected async Task GetPrimaryResourceByIdAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken) @@ -477,8 +474,9 @@ public class JsonApiResourceService : JsonApiResourceService resourceChangeTracker) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 98615ced4b..8392b92baa 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -39,6 +39,7 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); + _services.AddScoped(_ => new Mock().Object); _resourceGraphBuilder = new ResourceGraphBuilder(_options, LoggerFactory); } diff --git a/test/DiscoveryTests/TestResourceRepository.cs b/test/DiscoveryTests/TestResourceRepository.cs index b0c60f8b1b..096da8abd9 100644 --- a/test/DiscoveryTests/TestResourceRepository.cs +++ b/test/DiscoveryTests/TestResourceRepository.cs @@ -12,8 +12,9 @@ namespace DiscoveryTests public sealed class TestResourceRepository : EntityFrameworkCoreRepository { public TestResourceRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/test/DiscoveryTests/TestResourceService.cs b/test/DiscoveryTests/TestResourceService.cs index d5e6e53d0f..f2d565ba4d 100644 --- a/test/DiscoveryTests/TestResourceService.cs +++ b/test/DiscoveryTests/TestResourceService.cs @@ -13,8 +13,10 @@ namespace DiscoveryTests public sealed class TestResourceService : JsonApiResourceService { public TestResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, - IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs index 191bca53dc..4e1b02cb3f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; @@ -45,14 +44,11 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter)) { AttrAttribute archivedAtAttribute = ResourceContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt)); - var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute); FilterExpression isUnarchived = new ComparisonExpression(ComparisonOperator.Equals, archivedAtChain, new NullConstantExpression()); - return existingFilter == null - ? isUnarchived - : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(existingFilter, isUnarchived)); + return existingFilter == null ? isUnarchived : new LogicalExpression(LogicalOperator.And, existingFilter, isUnarchived); } } @@ -68,7 +64,7 @@ private bool IsRequestingCollectionOfTelevisionBroadcasts() { if (_request.IsCollection) { - if (_request.PrimaryResource == ResourceContext || _request.SecondaryResource == ResourceContext) + if (ResourceContext.Equals(_request.PrimaryResource) || ResourceContext.Equals(_request.SecondaryResource)) { return true; } @@ -116,27 +112,27 @@ private bool HasFilterOnArchivedAt(FilterExpression existingFilter) return walker.HasFilterOnArchivedAt; } - public override Task OnPrepareWriteAsync(TelevisionBroadcast broadcast, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnPrepareWriteAsync(TelevisionBroadcast broadcast, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.UpdateResource) + if (writeOperation == WriteOperationKind.UpdateResource) { _storedArchivedAt = broadcast.ArchivedAt; } - return base.OnPrepareWriteAsync(broadcast, operationKind, cancellationToken); + return base.OnPrepareWriteAsync(broadcast, writeOperation, cancellationToken); } - public override async Task OnWritingAsync(TelevisionBroadcast broadcast, OperationKind operationKind, CancellationToken cancellationToken) + public override async Task OnWritingAsync(TelevisionBroadcast broadcast, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { AssertIsNotArchived(broadcast); } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { AssertIsNotShiftingArchiveDate(broadcast); } - else if (operationKind == OperationKind.DeleteResource) + else if (writeOperation == WriteOperationKind.DeleteResource) { TelevisionBroadcast broadcastToDelete = await _dbContext.Broadcasts.FirstOrDefaultAsync(resource => resource.Id == broadcast.Id, cancellationToken); @@ -147,7 +143,7 @@ public override async Task OnWritingAsync(TelevisionBroadcast broadcast, Operati } } - await base.OnWritingAsync(broadcast, operationKind, cancellationToken); + await base.OnWritingAsync(broadcast, writeOperation, cancellationToken); } [AssertionMethod] @@ -192,7 +188,7 @@ private sealed class FilterWalker : QueryExpressionRewriter public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object argument) { - if (expression.Fields.First().Property.Name == nameof(TelevisionBroadcast.ArchivedAt)) + if (expression.Fields[0].Property.Name == nameof(TelevisionBroadcast.ArchivedAt)) { HasFilterOnArchivedAt = true; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index 21b4fd4ac5..55268c88c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -38,7 +38,7 @@ private static void AssertOnlyCreatingMusicTracks(IEnumerable public override string TransactionId => _extraDbContext.Database.CurrentTransaction.TransactionId.ToString(); public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { _extraDbContext = extraDbContext; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs index 044cfa0a14..aabf45bb25 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs @@ -15,8 +15,9 @@ public sealed class MusicTrackRepository : EntityFrameworkCoreRepository null; public MusicTrackRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs index 555c9ba955..6a5f092e28 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs @@ -48,17 +48,17 @@ public Task DeleteAsync(int id, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task SetRelationshipAsync(Performer primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(Performer leftResource, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(int primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(int leftId, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(Performer primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task RemoveFromToManyRelationshipAsync(Performer leftResource, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index f25b890546..68fcb52cb7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using JsonApiDotNetCore; @@ -27,7 +28,6 @@ public CarExpressionRewriter(IResourceContextProvider resourceContextProvider) ResourceContext carResourceContext = resourceContextProvider.GetResourceContext(); _regionIdAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.RegionId)); - _licensePlateAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.LicensePlate)); } @@ -35,7 +35,7 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, { if (expression.Left is ResourceFieldChainExpression leftChain && expression.Right is LiteralConstantExpression rightConstant) { - PropertyInfo leftProperty = leftChain.Fields.Last().Property; + PropertyInfo leftProperty = leftChain.Fields[^1].Property; if (IsCarId(leftProperty)) { @@ -51,9 +51,9 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, return base.VisitComparison(expression, argument); } - public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expression, object argument) + public override QueryExpression VisitAny(AnyExpression expression, object argument) { - PropertyInfo property = expression.TargetAttribute.Fields.Last().Property; + PropertyInfo property = expression.TargetAttribute.Fields[^1].Property; if (IsCarId(property)) { @@ -61,12 +61,12 @@ public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expressio return RewriteFilterOnCarStringIds(expression.TargetAttribute, carStringIds); } - return base.VisitEqualsAnyOf(expression, argument); + return base.VisitAny(expression, argument); } public override QueryExpression VisitMatchText(MatchTextExpression expression, object argument) { - PropertyInfo property = expression.TargetAttribute.Fields.Last().Property; + PropertyInfo property = expression.TargetAttribute.Fields[^1].Property; if (IsCarId(property)) { @@ -83,7 +83,7 @@ private static bool IsCarId(PropertyInfo property) private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression existingCarIdChain, IEnumerable carStringIds) { - var outerTerms = new List(); + ImmutableArray.Builder outerTermsBuilder = ImmutableArray.CreateBuilder(); foreach (string carStringId in carStringIds) { @@ -93,10 +93,10 @@ private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression }; FilterExpression keyComparison = CreateEqualityComparisonOnCompositeKey(existingCarIdChain, tempCar.RegionId, tempCar.LicensePlate); - outerTerms.Add(keyComparison); + outerTermsBuilder.Add(keyComparison); } - return outerTerms.Count == 1 ? outerTerms[0] : new LogicalExpression(LogicalOperator.Or, outerTerms); + return outerTermsBuilder.Count == 1 ? outerTermsBuilder[0] : new LogicalExpression(LogicalOperator.Or, outerTermsBuilder.ToImmutable()); } private FilterExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldChainExpression existingCarIdChain, long regionIdValue, @@ -112,41 +112,37 @@ private FilterExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldCha var licensePlateComparison = new ComparisonExpression(ComparisonOperator.Equals, licensePlateChain, new LiteralConstantExpression(licensePlateValue)); - return new LogicalExpression(LogicalOperator.And, new[] - { - regionIdComparison, - licensePlateComparison - }); + return new LogicalExpression(LogicalOperator.And, regionIdComparison, licensePlateComparison); } public override QueryExpression VisitSort(SortExpression expression, object argument) { - var newSortElements = new List(); + ImmutableArray.Builder elementsBuilder = ImmutableArray.CreateBuilder(expression.Elements.Count); foreach (SortElementExpression sortElement in expression.Elements) { if (IsSortOnCarId(sortElement)) { ResourceFieldChainExpression regionIdSort = ReplaceLastAttributeInChain(sortElement.TargetAttribute, _regionIdAttribute); - newSortElements.Add(new SortElementExpression(regionIdSort, sortElement.IsAscending)); + elementsBuilder.Add(new SortElementExpression(regionIdSort, sortElement.IsAscending)); ResourceFieldChainExpression licensePlateSort = ReplaceLastAttributeInChain(sortElement.TargetAttribute, _licensePlateAttribute); - newSortElements.Add(new SortElementExpression(licensePlateSort, sortElement.IsAscending)); + elementsBuilder.Add(new SortElementExpression(licensePlateSort, sortElement.IsAscending)); } else { - newSortElements.Add(sortElement); + elementsBuilder.Add(sortElement); } } - return new SortExpression(newSortElements); + return new SortExpression(elementsBuilder.ToImmutable()); } private static bool IsSortOnCarId(SortElementExpression sortElement) { if (sortElement.TargetAttribute != null) { - PropertyInfo property = sortElement.TargetAttribute.Fields.Last().Property; + PropertyInfo property = sortElement.TargetAttribute.Fields[^1].Property; if (IsCarId(property)) { @@ -159,8 +155,7 @@ private static bool IsSortOnCarId(SortElementExpression sortElement) private static ResourceFieldChainExpression ReplaceLastAttributeInChain(ResourceFieldChainExpression resourceFieldChain, AttrAttribute attribute) { - List fields = resourceFieldChain.Fields.ToList(); - fields[^1] = attribute; + IImmutableList fields = resourceFieldChain.Fields.SetItem(resourceFieldChain.Fields.Count - 1, attribute); return new ResourceFieldChainExpression(fields); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs index 8142d8d378..af9690cdf4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs @@ -13,13 +13,13 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class CarRepository : EntityFrameworkCoreRepository { - private readonly IResourceGraph _resourceGraph; + private readonly CarExpressionRewriter _writer; public CarRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IEnumerable constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { - _resourceGraph = resourceGraph; + _writer = new CarExpressionRewriter(resourceGraph); } protected override IQueryable ApplyQueryLayer(QueryLayer layer) @@ -33,14 +33,12 @@ private void RecursiveRewriteFilterInLayer(QueryLayer queryLayer) { if (queryLayer.Filter != null) { - var writer = new CarExpressionRewriter(_resourceGraph); - queryLayer.Filter = (FilterExpression)writer.Visit(queryLayer.Filter, null); + queryLayer.Filter = (FilterExpression)_writer.Visit(queryLayer.Filter, null); } if (queryLayer.Sort != null) { - var writer = new CarExpressionRewriter(_resourceGraph); - queryLayer.Sort = (SortExpression)writer.Visit(queryLayer.Sort, null); + queryLayer.Sort = (SortExpression)_writer.Visit(queryLayer.Sort, null); } if (queryLayer.Projection != null) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs index 8bf5086e06..839fe71fab 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs @@ -14,8 +14,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading public sealed class BuildingRepository : EntityFrameworkCoreRepository { public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, + IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs index fcb6af0d6b..d0b52daed3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs @@ -20,8 +20,9 @@ public sealed class ConsumerArticleService : JsonApiResourceService resourceChangeTracker) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs index d1e44519f9..b2d89011cf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs @@ -54,11 +54,11 @@ public override Task PostAsync([FromBody] TResource resource, Can } [HttpPost("{id}/relationships/{relationshipName}")] - public Task PostRelationshipAsync(string id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public Task PostRelationshipAsync(string id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { int idValue = _codec.Decode(id); - return base.PostRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); + return base.PostRelationshipAsync(idValue, relationshipName, rightResourceIds, cancellationToken); } [HttpPatch("{id}")] @@ -69,11 +69,10 @@ public Task PatchAsync(string id, [FromBody] TResource resource, } [HttpPatch("{id}/relationships/{relationshipName}")] - public Task PatchRelationshipAsync(string id, string relationshipName, [FromBody] object secondaryResourceIds, - CancellationToken cancellationToken) + public Task PatchRelationshipAsync(string id, string relationshipName, [FromBody] object rightValue, CancellationToken cancellationToken) { int idValue = _codec.Decode(id); - return base.PatchRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); + return base.PatchRelationshipAsync(idValue, relationshipName, rightValue, cancellationToken); } [HttpDelete("{id}")] @@ -84,11 +83,11 @@ public Task DeleteAsync(string id, CancellationToken cancellation } [HttpDelete("{id}/relationships/{relationshipName}")] - public Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] ISet secondaryResourceIds, + public Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] ISet rightResourceIds, CancellationToken cancellationToken) { int idValue = _codec.Decode(id); - return base.DeleteRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); + return base.DeleteRelationshipAsync(idValue, relationshipName, rightResourceIds, cancellationToken); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs index 7139b92865..5c768fb4d2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs @@ -42,9 +42,9 @@ public WorkflowDefinition(IResourceGraph resourceGraph) { } - public override Task OnPrepareWriteAsync(Workflow resource, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnPrepareWriteAsync(Workflow resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.UpdateResource) + if (writeOperation == WriteOperationKind.UpdateResource) { _previousStage = resource.Stage; } @@ -52,13 +52,13 @@ public override Task OnPrepareWriteAsync(Workflow resource, OperationKind operat return Task.CompletedTask; } - public override Task OnWritingAsync(Workflow resource, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWritingAsync(Workflow resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { AssertHasValidInitialStage(resource); } - else if (operationKind == OperationKind.UpdateResource && resource.Stage != _previousStage) + else if (writeOperation == WriteOperationKind.UpdateResource && resource.Stage != _previousStage) { AssertCanTransitionToStage(_previousStage, resource.Stage); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs index c4c176a7c5..f234be59fc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs @@ -77,7 +77,7 @@ public async Task Logs_request_body_at_Trace_level() loggerFactory.Logger.Messages.Should().NotBeEmpty(); loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace && - message.Text.StartsWith("Received request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal)); + message.Text.StartsWith("Received POST request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal)); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs index 803afbf723..51437cdba5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetGroupDefinition.cs @@ -23,21 +23,21 @@ public FireForgetGroupDefinition(IResourceGraph resourceGraph, FireForgetDbConte _hitCounter = hitCounter; } - public override async Task OnWritingAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) + public override async Task OnWritingAsync(DomainGroup group, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); - if (operationKind == OperationKind.DeleteResource) + if (writeOperation == WriteOperationKind.DeleteResource) { _groupToDelete = await base.GetGroupToDeleteAsync(group.Id, cancellationToken); } } - public override Task OnWriteSucceededAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWriteSucceededAsync(DomainGroup group, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync); - return FinishWriteAsync(group, operationKind, cancellationToken); + return FinishWriteAsync(group, writeOperation, cancellationToken); } protected override Task FlushMessageAsync(OutgoingMessage message, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs index 72df77a2e2..34fd1504b5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetUserDefinition.cs @@ -23,21 +23,21 @@ public FireForgetUserDefinition(IResourceGraph resourceGraph, FireForgetDbContex _hitCounter = hitCounter; } - public override async Task OnWritingAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) + public override async Task OnWritingAsync(DomainUser user, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); - if (operationKind == OperationKind.DeleteResource) + if (writeOperation == WriteOperationKind.DeleteResource) { _userToDelete = await base.GetUserToDeleteAsync(user.Id, cancellationToken); } } - public override Task OnWriteSucceededAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWriteSucceededAsync(DomainUser user, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWriteSucceededAsync); - return FinishWriteAsync(user, operationKind, cancellationToken); + return FinishWriteAsync(user, writeOperation, cancellationToken); } protected override Task FlushMessageAsync(OutgoingMessage message, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs index bac42f8d6f..f08d66ae5f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingGroupDefinition.cs @@ -30,15 +30,15 @@ protected MessagingGroupDefinition(IResourceGraph resourceGraph, DbSet(ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync); - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { group.Id = Guid.NewGuid(); } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { _beforeGroupName = group.Name; } @@ -47,7 +47,7 @@ public override Task OnPrepareWriteAsync(DomainGroup group, OperationKind operat } public override async Task OnSetToManyRelationshipAsync(DomainGroup group, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToManyRelationshipAsync); @@ -168,9 +168,9 @@ public override Task OnRemoveFromRelationshipAsync(DomainGroup group, HasManyAtt return Task.CompletedTask; } - protected async Task FinishWriteAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) + protected async Task FinishWriteAsync(DomainGroup group, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { var message = OutgoingMessage.CreateFromContent(new GroupCreatedContent { @@ -180,7 +180,7 @@ protected async Task FinishWriteAsync(DomainGroup group, OperationKind operation await FlushMessageAsync(message, cancellationToken); } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { if (_beforeGroupName != group.Name) { @@ -194,7 +194,7 @@ protected async Task FinishWriteAsync(DomainGroup group, OperationKind operation await FlushMessageAsync(message, cancellationToken); } } - else if (operationKind == OperationKind.DeleteResource) + else if (writeOperation == WriteOperationKind.DeleteResource) { DomainGroup groupToDelete = await GetGroupToDeleteAsync(group.Id, cancellationToken); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs index d7294f3884..f7f9ca81cf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/MessagingUserDefinition.cs @@ -27,15 +27,15 @@ protected MessagingUserDefinition(IResourceGraph resourceGraph, DbSet(ResourceDefinitionHitCounter.ExtensibilityPoint.OnPrepareWriteAsync); - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { user.Id = Guid.NewGuid(); } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { _beforeLoginName = user.LoginName; _beforeDisplayName = user.DisplayName; @@ -45,7 +45,7 @@ public override Task OnPrepareWriteAsync(DomainUser user, OperationKind operatio } public override Task OnSetToOneRelationshipAsync(DomainUser user, HasOneAttribute hasOneRelationship, IIdentifiable rightResourceId, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnSetToOneRelationshipAsync); @@ -90,9 +90,9 @@ public override Task OnSetToOneRelationshipAsync(DomainUser user, return Task.FromResult(rightResourceId); } - protected async Task FinishWriteAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) + protected async Task FinishWriteAsync(DomainUser user, WriteOperationKind writeOperation, CancellationToken cancellationToken) { - if (operationKind == OperationKind.CreateResource) + if (writeOperation == WriteOperationKind.CreateResource) { var message = OutgoingMessage.CreateFromContent(new UserCreatedContent { @@ -103,7 +103,7 @@ protected async Task FinishWriteAsync(DomainUser user, OperationKind operationKi await FlushMessageAsync(message, cancellationToken); } - else if (operationKind == OperationKind.UpdateResource) + else if (writeOperation == WriteOperationKind.UpdateResource) { if (_beforeLoginName != user.LoginName) { @@ -129,7 +129,7 @@ protected async Task FinishWriteAsync(DomainUser user, OperationKind operationKi await FlushMessageAsync(message, cancellationToken); } } - else if (operationKind == OperationKind.DeleteResource) + else if (writeOperation == WriteOperationKind.DeleteResource) { DomainUser userToDelete = await GetUserToDeleteAsync(user.Id, cancellationToken); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs index 61e685859b..480585b63e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxGroupDefinition.cs @@ -21,11 +21,11 @@ public OutboxGroupDefinition(IResourceGraph resourceGraph, OutboxDbContext dbCon _outboxMessageSet = dbContext.OutboxMessages; } - public override Task OnWritingAsync(DomainGroup group, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWritingAsync(DomainGroup group, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); - return FinishWriteAsync(group, operationKind, cancellationToken); + return FinishWriteAsync(group, writeOperation, cancellationToken); } protected override async Task FlushMessageAsync(OutgoingMessage message, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs index 88b8a2e865..a4b4731a22 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxUserDefinition.cs @@ -21,11 +21,11 @@ public OutboxUserDefinition(IResourceGraph resourceGraph, OutboxDbContext dbCont _outboxMessageSet = dbContext.OutboxMessages; } - public override Task OnWritingAsync(DomainUser user, OperationKind operationKind, CancellationToken cancellationToken) + public override Task OnWritingAsync(DomainUser user, WriteOperationKind writeOperation, CancellationToken cancellationToken) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync); - return FinishWriteAsync(user, operationKind, cancellationToken); + return FinishWriteAsync(user, writeOperation, cancellationToken); } protected override async Task FlushMessageAsync(OutgoingMessage message, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs index ba6ad3e96f..138bf96cac 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenantResourceService.cs @@ -23,8 +23,9 @@ public class MultiTenantResourceService : JsonApiResourceService public MultiTenantResourceService(ITenantProvider tenantProvider, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { _tenantProvider = tenantProvider; } @@ -59,21 +60,20 @@ public override async Task UpdateAsync(TId id, TResource resource, Ca return await base.UpdateAsync(id, resource, cancellationToken); } - public override async Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, - CancellationToken cancellationToken) + public override async Task SetRelationshipAsync(TId leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(rightValue, cancellationToken); - await base.SetRelationshipAsync(primaryId, relationshipName, secondaryResourceIds, cancellationToken); + await base.SetRelationshipAsync(leftId, relationshipName, rightValue, cancellationToken); } - public override async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + public override async Task AddToToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { - _ = await GetPrimaryResourceByIdAsync(primaryId, TopFieldSelection.OnlyIdAttribute, cancellationToken); - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + _ = await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); + await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); - await base.AddToToManyRelationshipAsync(primaryId, relationshipName, secondaryResourceIds, cancellationToken); + await base.AddToToManyRelationshipAsync(leftId, relationshipName, rightResourceIds, cancellationToken); } public override async Task DeleteAsync(TId id, CancellationToken cancellationToken) @@ -90,8 +90,9 @@ public class MultiTenantResourceService : MultiTenantResourceService< { public MultiTenantResourceService(ITenantProvider tenantProvider, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) - : base(tenantProvider, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(tenantProvider, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs index 4435f79e56..1ebbd87fd2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs @@ -21,8 +21,8 @@ public sealed class ResultCapturingRepository : EntityFrameworkCoreRe public ResultCapturingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory, - ResourceCaptureStore captureStore) - : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) + IResourceDefinitionAccessor resourceDefinitionAccessor, ResourceCaptureStore captureStore) + : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor) { _captureStore = captureStore; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index e3c17a8733..1f19221d70 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -25,7 +25,7 @@ public MoonDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clie _hitCounter = hitCounter; } - public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + public override IImmutableList OnApplyIncludes(IImmutableList existingIncludes) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); @@ -38,10 +38,7 @@ public override IReadOnlyCollection OnApplyIncludes(IR RelationshipAttribute orbitsAroundRelationship = ResourceContext.Relationships.Single(relationship => relationship.Property.Name == nameof(Moon.OrbitsAround)); - return new List(existingIncludes) - { - new(orbitsAroundRelationship) - }; + return existingIncludes.Add(new IncludeElementExpression(orbitsAroundRelationship)); } public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs index de0da36670..9b547cf117 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using JetBrains.Annotations; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; @@ -28,7 +27,7 @@ public PlanetDefinition(IResourceGraph resourceGraph, IClientSettingsProvider cl _hitCounter = hitCounter; } - public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + public override IImmutableList OnApplyIncludes(IImmutableList existingIncludes) { _hitCounter.TrackInvocation(ResourceDefinitionHitCounter.ExtensibilityPoint.OnApplyIncludes); @@ -55,9 +54,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) FilterExpression hasNoPrivateName = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(privateNameAttribute), new NullConstantExpression()); - return existingFilter == null - ? hasNoPrivateName - : new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(hasNoPrivateName, existingFilter)); + return existingFilter == null ? hasNoPrivateName : new LogicalExpression(LogicalOperator.And, hasNoPrivateName, existingFilter); } return existingFilter; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/BlockingHttpDeleteController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/BlockingHttpDeleteController.cs index 52c01ab6b8..1c0761bc83 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/BlockingHttpDeleteController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/BlockingHttpDeleteController.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.RestrictedControllers { [NoHttpDelete] - [DisableQueryString(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)] + [DisableQueryString(JsonApiQueryStringParameters.Sort | JsonApiQueryStringParameters.Page)] public sealed class BlockingHttpDeleteController : JsonApiController { public BlockingHttpDeleteController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs index 680dda6c86..03a726ab4e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs @@ -62,7 +62,7 @@ public async Task Cannot_create_resource() Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support POST requests."); + error.Detail.Should().Be("Endpoint does not support POST requests."); } [Fact] @@ -102,7 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support PATCH requests."); + error.Detail.Should().Be("Endpoint does not support PATCH requests."); } [Fact] @@ -130,7 +130,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support DELETE requests."); + error.Detail.Should().Be("Endpoint does not support DELETE requests."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs index 56a808e7da..d04a93fe6e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs @@ -116,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support DELETE requests."); + error.Detail.Should().Be("Endpoint does not support DELETE requests."); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs index 61c051e62e..33a9695c10 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs @@ -95,7 +95,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support PATCH requests."); + error.Detail.Should().Be("Endpoint does not support PATCH requests."); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs index 241c1f1770..e248087b7e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs @@ -62,7 +62,7 @@ public async Task Cannot_create_resource() Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); - error.Detail.Should().Be("Resource does not support POST requests."); + error.Detail.Should().Be("Endpoint does not support POST requests."); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs index f129f5121e..0ec6475b15 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RestrictedControllers/SkipCacheQueryStringParameterReader.cs @@ -1,4 +1,3 @@ -using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs index 002f8df2b1..b09bbff935 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionAwareResourceService.cs @@ -26,8 +26,9 @@ public class SoftDeletionAwareResourceService : JsonApiResourceS public SoftDeletionAwareResourceService(ISystemClock systemClock, ITargetedFields targetedFields, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, + resourceDefinitionAccessor) { _systemClock = systemClock; _targetedFields = targetedFields; @@ -58,31 +59,30 @@ public override async Task UpdateAsync(TId id, TResource resource, Ca return await base.UpdateAsync(id, resource, cancellationToken); } - public override async Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, - CancellationToken cancellationToken) + public override async Task SetRelationshipAsync(TId leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { if (IsSoftDeletable(_request.Relationship.RightType)) { - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(rightValue, cancellationToken); } - await base.SetRelationshipAsync(primaryId, relationshipName, secondaryResourceIds, cancellationToken); + await base.SetRelationshipAsync(leftId, relationshipName, rightValue, cancellationToken); } - public override async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + public override async Task AddToToManyRelationshipAsync(TId leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { if (IsSoftDeletable(typeof(TResource))) { - _ = await GetPrimaryResourceByIdAsync(primaryId, TopFieldSelection.OnlyIdAttribute, cancellationToken); + _ = await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); } if (IsSoftDeletable(_request.Relationship.RightType)) { - await AssertRightResourcesExistAsync(secondaryResourceIds, cancellationToken); + await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); } - await base.AddToToManyRelationshipAsync(primaryId, relationshipName, secondaryResourceIds, cancellationToken); + await base.AddToToManyRelationshipAsync(leftId, relationshipName, rightResourceIds, cancellationToken); } public override async Task DeleteAsync(TId id, CancellationToken cancellationToken) @@ -119,9 +119,9 @@ public class SoftDeletionAwareResourceService : SoftDeletionAwareReso { public SoftDeletionAwareResourceService(ISystemClock systemClock, ITargetedFields targetedFields, IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker) + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor) : base(systemClock, targetedFields, repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, - resourceChangeTracker) + resourceChangeTracker, resourceDefinitionAccessor) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs index bd00582b87..590292daa7 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/Links/LinkInclusionTests.cs @@ -56,12 +56,8 @@ public sealed class LinkInclusionTests public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInResourceContext, LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext - { - PublicName = nameof(ExampleResource), - ResourceType = typeof(ExampleResource), - TopLevelLinks = linksInResourceContext - }; + var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), + Array.Empty(), Array.Empty(), linksInResourceContext); var options = new JsonApiOptions { @@ -84,7 +80,7 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso TotalResourceCount = 10 }; - var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); @@ -157,12 +153,8 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResourceContext, LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext - { - PublicName = nameof(ExampleResource), - ResourceType = typeof(ExampleResource), - ResourceLinks = linksInResourceContext - }; + var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), + Array.Empty(), Array.Empty(), resourceLinks: linksInResourceContext); var options = new JsonApiOptions { @@ -171,7 +163,7 @@ public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResou var request = new JsonApiRequest(); var paginationContext = new PaginationContext(); - var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); @@ -323,12 +315,8 @@ public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInR LinkTypes linksInOptions, LinkTypes expected) { // Arrange - var exampleResourceContext = new ResourceContext - { - PublicName = nameof(ExampleResource), - ResourceType = typeof(ExampleResource), - RelationshipLinks = linksInResourceContext - }; + var exampleResourceContext = new ResourceContext(nameof(ExampleResource), typeof(ExampleResource), typeof(int), Array.Empty(), + Array.Empty(), Array.Empty(), relationshipLinks: linksInResourceContext); var options = new JsonApiOptions { @@ -337,7 +325,7 @@ public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInR var request = new JsonApiRequest(); var paginationContext = new PaginationContext(); - var resourceGraph = new ResourceGraph(exampleResourceContext.AsArray()); + var resourceGraph = new ResourceGraph(exampleResourceContext.AsHashSet()); var httpContextAccessor = new FakeHttpContextAccessor(); var linkGenerator = new FakeLinkGenerator(); var controllerResourceMapping = new FakeControllerResourceMapping(); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs index cc54815e60..d790bac795 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs @@ -34,15 +34,15 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Defaults, false, false)] - [InlineData(StandardQueryStringParameters.Defaults, true, false)] - [InlineData(StandardQueryStringParameters.All, false, false)] - [InlineData(StandardQueryStringParameters.All, true, false)] - [InlineData(StandardQueryStringParameters.None, false, false)] - [InlineData(StandardQueryStringParameters.None, true, true)] - [InlineData(StandardQueryStringParameters.Filter, false, false)] - [InlineData(StandardQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Defaults, false, false)] + [InlineData(JsonApiQueryStringParameters.Defaults, true, false)] + [InlineData(JsonApiQueryStringParameters.All, false, false)] + [InlineData(JsonApiQueryStringParameters.All, true, false)] + [InlineData(JsonApiQueryStringParameters.None, false, false)] + [InlineData(JsonApiQueryStringParameters.None, true, true)] + [InlineData(JsonApiQueryStringParameters.Filter, false, false)] + [InlineData(JsonApiQueryStringParameters.Filter, true, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) { // Arrange var options = new JsonApiOptions diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/FilterParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/FilterParseTests.cs index 2ebd5a05fd..b4319447f1 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/FilterParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/FilterParseTests.cs @@ -43,11 +43,11 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Filter, false)] - [InlineData(StandardQueryStringParameters.All, false)] - [InlineData(StandardQueryStringParameters.None, true)] - [InlineData(StandardQueryStringParameters.Page, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Filter, false)] + [InlineData(JsonApiQueryStringParameters.All, false)] + [InlineData(JsonApiQueryStringParameters.None, true)] + [InlineData(JsonApiQueryStringParameters.Page, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act bool isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); @@ -131,7 +131,7 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin [InlineData("filter", "contains(title,'this')", null, "contains(title,'this')")] [InlineData("filter", "startsWith(title,'this')", null, "startsWith(title,'this')")] [InlineData("filter", "endsWith(title,'this')", null, "endsWith(title,'this')")] - [InlineData("filter", "any(title,'this','that','there')", null, "any(title,'this','that','there')")] + [InlineData("filter", "any(title,'this','that','there')", null, "any(title,'that','there','this')")] [InlineData("filter", "and(contains(title,'sales'),contains(title,'marketing'),contains(title,'advertising'))", null, "and(contains(title,'sales'),contains(title,'marketing'),contains(title,'advertising'))")] [InlineData("filter[posts]", "or(and(not(equals(author.userName,null)),not(equals(author.displayName,null))),not(has(comments,startsWith(text,'A'))))", diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/IncludeParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/IncludeParseTests.cs index 1836fbaaee..dcb1ce2557 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/IncludeParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/IncludeParseTests.cs @@ -37,11 +37,11 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Include, false)] - [InlineData(StandardQueryStringParameters.All, false)] - [InlineData(StandardQueryStringParameters.None, true)] - [InlineData(StandardQueryStringParameters.Filter, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Include, false)] + [InlineData(JsonApiQueryStringParameters.All, false)] + [InlineData(JsonApiQueryStringParameters.None, true)] + [InlineData(JsonApiQueryStringParameters.Filter, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act bool isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/NullsParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/NullsParseTests.cs index 90a8739a91..63a7d5ed77 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/NullsParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/NullsParseTests.cs @@ -34,15 +34,15 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Nulls, false, false)] - [InlineData(StandardQueryStringParameters.Nulls, true, false)] - [InlineData(StandardQueryStringParameters.All, false, false)] - [InlineData(StandardQueryStringParameters.All, true, false)] - [InlineData(StandardQueryStringParameters.None, false, false)] - [InlineData(StandardQueryStringParameters.None, true, true)] - [InlineData(StandardQueryStringParameters.Filter, false, false)] - [InlineData(StandardQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Nulls, false, false)] + [InlineData(JsonApiQueryStringParameters.Nulls, true, false)] + [InlineData(JsonApiQueryStringParameters.All, false, false)] + [InlineData(JsonApiQueryStringParameters.All, true, false)] + [InlineData(JsonApiQueryStringParameters.None, false, false)] + [InlineData(JsonApiQueryStringParameters.None, true, true)] + [InlineData(JsonApiQueryStringParameters.Filter, false, false)] + [InlineData(JsonApiQueryStringParameters.Filter, true, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) { // Arrange var options = new JsonApiOptions diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/PaginationParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/PaginationParseTests.cs index f3501b4609..f188a00ace 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/PaginationParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/PaginationParseTests.cs @@ -40,11 +40,11 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Page, false)] - [InlineData(StandardQueryStringParameters.All, false)] - [InlineData(StandardQueryStringParameters.None, true)] - [InlineData(StandardQueryStringParameters.Sort, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Page, false)] + [InlineData(JsonApiQueryStringParameters.All, false)] + [InlineData(JsonApiQueryStringParameters.None, true)] + [InlineData(JsonApiQueryStringParameters.Sort, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act bool isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SortParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SortParseTests.cs index 0513fb5ca9..27d2262123 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SortParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SortParseTests.cs @@ -39,11 +39,11 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Sort, false)] - [InlineData(StandardQueryStringParameters.All, false)] - [InlineData(StandardQueryStringParameters.None, true)] - [InlineData(StandardQueryStringParameters.Filter, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Sort, false)] + [InlineData(JsonApiQueryStringParameters.All, false)] + [InlineData(JsonApiQueryStringParameters.None, true)] + [InlineData(JsonApiQueryStringParameters.Filter, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act bool isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); diff --git a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs index 42e63a5a84..41b7672d08 100644 --- a/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs @@ -39,11 +39,11 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP } [Theory] - [InlineData(StandardQueryStringParameters.Fields, false)] - [InlineData(StandardQueryStringParameters.All, false)] - [InlineData(StandardQueryStringParameters.None, true)] - [InlineData(StandardQueryStringParameters.Filter, true)] - public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) + [InlineData(JsonApiQueryStringParameters.Fields, false)] + [InlineData(JsonApiQueryStringParameters.All, false)] + [InlineData(JsonApiQueryStringParameters.None, true)] + [InlineData(JsonApiQueryStringParameters.Filter, true)] + public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act bool isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); @@ -83,7 +83,7 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin } [Theory] - [InlineData("fields[blogPosts]", "caption,url,author", "blogPosts(caption,url,author)")] + [InlineData("fields[blogPosts]", "caption,url,author", "blogPosts(author,caption,url)")] [InlineData("fields[blogPosts]", "author,comments,labels", "blogPosts(author,comments,labels)")] [InlineData("fields[blogs]", "id", "blogs(id)")] [InlineData("fields[blogs]", "", "blogs(id)")] diff --git a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs index 0c64dcc897..d17c2ac432 100644 --- a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs @@ -78,7 +78,7 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( // Assert ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); Assert.Equal("relatedResource", resource.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); - Assert.Equal("relatedResources", resource.Relationships.Single(relationship => !(relationship is HasOneAttribute)).PublicName); + Assert.Equal("relatedResources", resource.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); } private sealed class NonDbResource : Identifiable diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index bd94ee6c7c..5cb26ff134 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -219,7 +220,7 @@ public Task CreateAsync(IntResource resource, CancellationToken can throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, + public Task AddToToManyRelationshipAsync(int leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -230,7 +231,7 @@ public Task UpdateAsync(int id, IntResource resource, CancellationT throw new NotImplementedException(); } - public Task SetRelationshipAsync(int primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(int leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -240,7 +241,7 @@ public Task DeleteAsync(int id, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, + public Task RemoveFromToManyRelationshipAsync(int leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -275,7 +276,7 @@ public Task CreateAsync(GuidResource resource, CancellationToken c throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(Guid primaryId, string relationshipName, ISet secondaryResourceIds, + public Task AddToToManyRelationshipAsync(Guid leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -286,7 +287,7 @@ public Task UpdateAsync(Guid id, GuidResource resource, Cancellati throw new NotImplementedException(); } - public Task SetRelationshipAsync(Guid primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(Guid leftId, string relationshipName, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -296,7 +297,7 @@ public Task DeleteAsync(Guid id, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(Guid primaryId, string relationshipName, ISet secondaryResourceIds, + public Task RemoveFromToManyRelationshipAsync(Guid leftId, string relationshipName, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -341,18 +342,17 @@ public Task DeleteAsync(int id, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task SetRelationshipAsync(IntResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(IntResource leftResource, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(int primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(int leftId, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(IntResource primaryResource, ISet secondaryResourceIds, - CancellationToken cancellationToken) + public Task RemoveFromToManyRelationshipAsync(IntResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -396,18 +396,17 @@ public Task DeleteAsync(Guid id, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task SetRelationshipAsync(GuidResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) + public Task SetRelationshipAsync(GuidResource leftResource, object rightValue, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task AddToToManyRelationshipAsync(Guid primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(Guid leftId, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(GuidResource primaryResource, ISet secondaryResourceIds, - CancellationToken cancellationToken) + public Task RemoveFromToManyRelationshipAsync(GuidResource leftResource, ISet rightResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -416,7 +415,7 @@ public Task RemoveFromToManyRelationshipAsync(GuidResource primaryResource, ISet [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class IntResourceDefinition : IResourceDefinition { - public IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + public IImmutableList OnApplyIncludes(IImmutableList existingIncludes) { throw new NotImplementedException(); } @@ -451,19 +450,19 @@ public IDictionary GetMeta(IntResource resource) throw new NotImplementedException(); } - public Task OnPrepareWriteAsync(IntResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnPrepareWriteAsync(IntResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } public Task OnSetToOneRelationshipAsync(IntResource leftResource, HasOneAttribute hasOneRelationship, IIdentifiable rightResourceId, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } public Task OnSetToManyRelationshipAsync(IntResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -480,12 +479,12 @@ public Task OnRemoveFromRelationshipAsync(IntResource leftResource, HasManyAttri throw new NotImplementedException(); } - public Task OnWritingAsync(IntResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWritingAsync(IntResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task OnWriteSucceededAsync(IntResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWriteSucceededAsync(IntResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -504,7 +503,7 @@ public void OnSerialize(IntResource resource) [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] private sealed class GuidResourceDefinition : IResourceDefinition { - public IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + public IImmutableList OnApplyIncludes(IImmutableList existingIncludes) { throw new NotImplementedException(); } @@ -539,19 +538,19 @@ public IDictionary GetMeta(GuidResource resource) throw new NotImplementedException(); } - public Task OnPrepareWriteAsync(GuidResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnPrepareWriteAsync(GuidResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } public Task OnSetToOneRelationshipAsync(GuidResource leftResource, HasOneAttribute hasOneRelationship, IIdentifiable rightResourceId, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } public Task OnSetToManyRelationshipAsync(GuidResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -568,12 +567,12 @@ public Task OnRemoveFromRelationshipAsync(GuidResource leftResource, HasManyAttr throw new NotImplementedException(); } - public Task OnWritingAsync(GuidResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWritingAsync(GuidResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task OnWriteSucceededAsync(GuidResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWriteSucceededAsync(GuidResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 8014907020..f789b8ba0f 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -167,24 +167,18 @@ private Mock CreateMockResourceGraph(string resourceName, bool i { var mockGraph = new Mock(); - var resourceContext = new ResourceContext - { - PublicName = resourceName, - IdentityType = typeof(string) - }; + var resourceContext = new ResourceContext(resourceName, typeof(object), typeof(string), Array.Empty(), + Array.Empty(), Array.Empty()); ISetupSequentialResult seq = mockGraph.SetupSequence(resourceGraph => resourceGraph.GetResourceContext(It.IsAny())) .Returns(resourceContext); if (includeRelationship) { - var relResourceContext = new ResourceContext - { - PublicName = "todoItems", - IdentityType = typeof(string) - }; + var relatedContext = new ResourceContext("todoItems", typeof(object), typeof(string), Array.Empty(), + Array.Empty(), Array.Empty()); - seq.Returns(relResourceContext); + seq.Returns(relatedContext); } return mockGraph; diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index c64a2a0434..7950291a9d 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -18,27 +18,27 @@ namespace UnitTests.Middleware public sealed class JsonApiRequestTests { [Theory] - [InlineData("HEAD", "/todoItems", true, EndpointKind.Primary, true)] - [InlineData("HEAD", "/todoItems/1", false, EndpointKind.Primary, true)] - [InlineData("HEAD", "/todoItems/1/owner", false, EndpointKind.Secondary, true)] - [InlineData("HEAD", "/todoItems/1/tags", true, EndpointKind.Secondary, true)] - [InlineData("HEAD", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, true)] - [InlineData("HEAD", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, true)] - [InlineData("GET", "/todoItems", true, EndpointKind.Primary, true)] - [InlineData("GET", "/todoItems/1", false, EndpointKind.Primary, true)] - [InlineData("GET", "/todoItems/1/owner", false, EndpointKind.Secondary, true)] - [InlineData("GET", "/todoItems/1/tags", true, EndpointKind.Secondary, true)] - [InlineData("GET", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, true)] - [InlineData("GET", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, true)] - [InlineData("POST", "/todoItems", false, EndpointKind.Primary, false)] - [InlineData("POST", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, false)] - [InlineData("PATCH", "/todoItems/1", false, EndpointKind.Primary, false)] - [InlineData("PATCH", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, false)] - [InlineData("PATCH", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, false)] - [InlineData("DELETE", "/todoItems/1", false, EndpointKind.Primary, false)] - [InlineData("DELETE", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, false)] + [InlineData("HEAD", "/todoItems", true, EndpointKind.Primary, null, true)] + [InlineData("HEAD", "/todoItems/1", false, EndpointKind.Primary, null, true)] + [InlineData("HEAD", "/todoItems/1/owner", false, EndpointKind.Secondary, null, true)] + [InlineData("HEAD", "/todoItems/1/tags", true, EndpointKind.Secondary, null, true)] + [InlineData("HEAD", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, null, true)] + [InlineData("HEAD", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, null, true)] + [InlineData("GET", "/todoItems", true, EndpointKind.Primary, null, true)] + [InlineData("GET", "/todoItems/1", false, EndpointKind.Primary, null, true)] + [InlineData("GET", "/todoItems/1/owner", false, EndpointKind.Secondary, null, true)] + [InlineData("GET", "/todoItems/1/tags", true, EndpointKind.Secondary, null, true)] + [InlineData("GET", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, null, true)] + [InlineData("GET", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, null, true)] + [InlineData("POST", "/todoItems", false, EndpointKind.Primary, WriteOperationKind.CreateResource, false)] + [InlineData("POST", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, WriteOperationKind.AddToRelationship, false)] + [InlineData("PATCH", "/todoItems/1", false, EndpointKind.Primary, WriteOperationKind.UpdateResource, false)] + [InlineData("PATCH", "/todoItems/1/relationships/owner", false, EndpointKind.Relationship, WriteOperationKind.SetRelationship, false)] + [InlineData("PATCH", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, WriteOperationKind.SetRelationship, false)] + [InlineData("DELETE", "/todoItems/1", false, EndpointKind.Primary, WriteOperationKind.DeleteResource, false)] + [InlineData("DELETE", "/todoItems/1/relationships/tags", true, EndpointKind.Relationship, WriteOperationKind.RemoveFromRelationship, false)] public async Task Sets_request_properties_correctly(string requestMethod, string requestPath, bool expectIsCollection, EndpointKind expectKind, - bool expectIsReadOnly) + WriteOperationKind? expectWriteOperation, bool expectIsReadOnly) { // Arrange var options = new JsonApiOptions @@ -69,8 +69,8 @@ public async Task Sets_request_properties_correctly(string requestMethod, string // Assert request.IsCollection.Should().Be(expectIsCollection); request.Kind.Should().Be(expectKind); + request.WriteOperation.Should().Be(expectWriteOperation); request.IsReadOnly.Should().Be(expectIsReadOnly); - request.BasePath.Should().BeEmpty(); request.PrimaryResource.Should().NotBeNull(); request.PrimaryResource.PublicName.Should().Be("todoItems"); } @@ -96,7 +96,7 @@ private static void SetupRoutes(HttpContext httpContext, string requestMethod, s if (pathSegments.Length >= 3) { - feature.RouteValues["relationshipName"] = pathSegments.Last(); + feature.RouteValues["relationshipName"] = pathSegments[^1]; } } diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index f31d140029..82a152dd41 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -16,6 +16,7 @@ public sealed class ResourceConstructionTests { private readonly Mock _requestMock; private readonly Mock _mockHttpContextAccessor; + private readonly Mock _resourceDefinitionAccessorMock = new(); public ResourceConstructionTests() { @@ -37,7 +38,7 @@ public void When_resource_has_default_constructor_it_must_succeed() serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options); + _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -70,7 +71,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options); + _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -105,7 +106,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options); + _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { diff --git a/test/UnitTests/NeverResourceDefinitionAccessor.cs b/test/UnitTests/NeverResourceDefinitionAccessor.cs index ddc89a95ba..f4d0f308bf 100644 --- a/test/UnitTests/NeverResourceDefinitionAccessor.cs +++ b/test/UnitTests/NeverResourceDefinitionAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Middleware; @@ -11,7 +12,7 @@ namespace UnitTests { internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor { - public IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes) + public IImmutableList OnApplyIncludes(Type resourceType, IImmutableList existingIncludes) { return existingIncludes; } @@ -46,21 +47,21 @@ public IDictionary GetMeta(Type resourceType, IIdentifiable reso return null; } - public Task OnPrepareWriteAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { return Task.CompletedTask; } public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable rightResourceId, OperationKind operationKind, CancellationToken cancellationToken) + IIdentifiable rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { return Task.FromResult(rightResourceId); } public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - OperationKind operationKind, CancellationToken cancellationToken) + WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { return Task.CompletedTask; @@ -80,13 +81,13 @@ public Task OnRemoveFromRelationshipAsync(TResource leftResource, Has return Task.CompletedTask; } - public Task OnWritingAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { return Task.CompletedTask; } - public Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken) + public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) where TResource : class, IIdentifiable { return Task.CompletedTask; diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 302668e09a..08ff89ad21 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; @@ -140,8 +141,8 @@ private IEnumerable GetIncludeConstraints(IEnumerable< if (inclusionChains != null) { - List chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships.ToArray())) - .ToList(); + List chains = inclusionChains.Select(relationships => + new ResourceFieldChainExpression(relationships.Cast().ToImmutableArray())).ToList(); IncludeExpression includeExpression = IncludeChainConverter.FromRelationshipChains(chains); expressionsInScope.Add(new ExpressionInScope(null, includeExpression)); @@ -161,8 +162,8 @@ private IEvaluatedIncludeCache GetEvaluatedIncludeCache(IEnumerable chains = inclusionChains.Select(relationships => new ResourceFieldChainExpression(relationships.ToArray())) - .ToList(); + List chains = inclusionChains.Select(relationships => + new ResourceFieldChainExpression(relationships.Cast().ToImmutableArray())).ToList(); IncludeExpression includeExpression = IncludeChainConverter.FromRelationshipChains(chains); diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index 3aeadc4654..e683e67c02 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -16,11 +16,12 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup private readonly RequestDeserializer _deserializer; private readonly Mock _fieldsManagerMock = new(); private readonly Mock _requestMock = new(); + private readonly Mock _resourceDefinitionAccessorMock = new(); public RequestDeserializerTests() { _deserializer = new RequestDeserializer(ResourceGraph, new TestResourceFactory(), _fieldsManagerMock.Object, MockHttpContextAccessor.Object, - _requestMock.Object, new JsonApiOptions()); + _requestMock.Object, new JsonApiOptions(), _resourceDefinitionAccessorMock.Object); } [Fact] diff --git a/test/UnitTests/TestResourceFactory.cs b/test/UnitTests/TestResourceFactory.cs index b27e334650..8df6c2e4b1 100644 --- a/test/UnitTests/TestResourceFactory.cs +++ b/test/UnitTests/TestResourceFactory.cs @@ -21,10 +21,5 @@ public NewExpression CreateNewExpression(Type resourceType) { return Expression.New(resourceType); } - - public IResourceDefinitionAccessor GetResourceDefinitionAccessor() - { - return new NeverResourceDefinitionAccessor(); - } } }