diff --git a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs index f653787b25..dd2657e6a2 100644 --- a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries { @@ -12,10 +14,22 @@ public interface IQueryLayerComposer /// Builds a top-level filter from constraints, used to determine total resource count. /// FilterExpression GetTopFilter(); - + /// /// Collects constraints and builds a out of them, used to retrieve the actual resources. /// QueryLayer Compose(ResourceContext requestResource); + + /// + /// Wraps a layer for a secondary endpoint into a primary layer, rewriting top-level includes. + /// + QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, + TId primaryId, RelationshipAttribute secondaryRelationship); + + /// + /// Gets the secondary projection for a relationship endpoint. + /// + IDictionary GetSecondaryProjectionForRelationshipEndpoint( + ResourceContext secondaryResourceContext); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 95ad1dba01..a9061e9380 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -176,6 +176,55 @@ private static IReadOnlyCollection ApplyIncludeElement return newIncludeElements; } + /// + public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, RelationshipAttribute secondaryRelationship) + { + var innerInclude = secondaryLayer.Include; + secondaryLayer.Include = null; + + var primaryIdAttribute = primaryResourceContext.Attributes.Single(x => x.Property.Name == nameof(Identifiable.Id)); + var sparseFieldSet = new SparseFieldSetExpression(new[] { primaryIdAttribute }); + + var primaryProjection = GetSparseFieldSetProjection(new[] { sparseFieldSet }, primaryResourceContext) ?? new Dictionary(); + primaryProjection[secondaryRelationship] = secondaryLayer; + primaryProjection[primaryIdAttribute] = null; + + return new QueryLayer(primaryResourceContext) + { + Include = RewriteIncludeForSecondaryEndpoint(innerInclude, secondaryRelationship), + Filter = CreateFilterById(primaryId, primaryResourceContext), + Projection = primaryProjection + }; + } + + private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude, RelationshipAttribute secondaryRelationship) + { + var parentElement = relativeInclude != null + ? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements) + : new IncludeElementExpression(secondaryRelationship); + + return new IncludeExpression(new[] {parentElement}); + } + + private FilterExpression CreateFilterById(TId id, ResourceContext resourceContext) + { + var primaryIdAttribute = resourceContext.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); + + return new ComparisonExpression(ComparisonOperator.Equals, + new ResourceFieldChainExpression(primaryIdAttribute), new LiteralConstantExpression(id.ToString())); + } + + public IDictionary GetSecondaryProjectionForRelationshipEndpoint(ResourceContext secondaryResourceContext) + { + var secondaryIdAttribute = secondaryResourceContext.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); + var sparseFieldSet = new SparseFieldSetExpression(new[] { secondaryIdAttribute }); + + var secondaryProjection = GetSparseFieldSetProjection(new[] { sparseFieldSet }, secondaryResourceContext) ?? new Dictionary(); + secondaryProjection[secondaryIdAttribute] = null; + + return secondaryProjection; + } + protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, ResourceContext resourceContext) { if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 7ec213e134..5352ea4bd7 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCore.Resources @@ -73,7 +75,12 @@ public NewExpression CreateNewExpression(Type resourceType) object constructorArgument = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, constructorParameter.ParameterType); - constructorArguments.Add(Expression.Constant(constructorArgument)); + var argumentExpression = typeof(DbContext).Assembly.GetName().Version.Major >= 5 + // Workaround for https://github.com/dotnet/efcore/issues/20502 to not fail on injected DbContext in EF Core 5. + ? CreateTupleAccessExpressionForConstant(constructorArgument, constructorArgument.GetType()) + : Expression.Constant(constructorArgument); + + constructorArguments.Add(argumentExpression); } catch (Exception exception) { @@ -86,6 +93,19 @@ public NewExpression CreateNewExpression(Type resourceType) return Expression.New(longestConstructor, constructorArguments); } + private static Expression CreateTupleAccessExpressionForConstant(object value, Type type) + { + MethodInfo tupleCreateMethod = typeof(Tuple).GetMethods() + .Single(m => m.Name == "Create" && m.IsGenericMethod && m.GetGenericArguments().Length == 1); + + MethodInfo constructedTupleCreateMethod = tupleCreateMethod.MakeGenericMethod(type); + + ConstantExpression constantExpression = Expression.Constant(value, type); + + MethodCallExpression tupleCreateCall = Expression.Call(constructedTupleCreateMethod, constantExpression); + return Expression.Property(tupleCreateCall, "Item1"); + } + private static bool HasSingleConstructorWithoutParameters(Type type) { ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray(); diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 2a101f22c9..a7293799b3 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -196,24 +196,18 @@ public virtual async Task GetRelationshipAsync(TId id, string relatio _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); - - var secondaryIdAttribute = _request.SecondaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); - + secondaryLayer.Projection = _queryLayerComposer.GetSecondaryProjectionForRelationshipEndpoint(_request.SecondaryResource); secondaryLayer.Include = null; - secondaryLayer.Projection = new Dictionary - { - [secondaryIdAttribute] = null - }; - - var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id); + + var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); var primaryResources = await _repository.GetAsync(primaryLayer); - + var primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); if (_hookExecutor != null) - { + { _hookExecutor.AfterRead(AsList(primaryResource), ResourcePipeline.GetRelationship); primaryResource = _hookExecutor.OnReturn(AsList(primaryResource), ResourcePipeline.GetRelationship).Single(); } @@ -233,7 +227,7 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); - var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id); + var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); var primaryResources = await _repository.GetAsync(primaryLayer); @@ -249,35 +243,6 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN return _request.Relationship.GetValue(primaryResource); } - private QueryLayer GetPrimaryLayerForSecondaryEndpoint(QueryLayer secondaryLayer, TId primaryId) - { - var innerInclude = secondaryLayer.Include; - secondaryLayer.Include = null; - - var primaryIdAttribute = - _request.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); - - return new QueryLayer(_request.PrimaryResource) - { - Include = RewriteIncludeForSecondaryEndpoint(innerInclude), - Filter = CreateFilterById(primaryId), - Projection = new Dictionary - { - [primaryIdAttribute] = null, - [_request.Relationship] = secondaryLayer - } - }; - } - - private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude) - { - var parentElement = relativeInclude != null - ? new IncludeElementExpression(_request.Relationship, relativeInclude.Elements) - : new IncludeElementExpression(_request.Relationship); - - return new IncludeExpression(new[] {parentElement}); - } - /// public virtual async Task UpdateAsync(TId id, TResource requestResource) { @@ -320,7 +285,7 @@ public virtual async Task UpdateRelationshipAsync(TId id, string relationshipNam AssertRelationshipExists(relationshipName); var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); - var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id); + var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); primaryLayer.Projection = null; var primaryResources = await _repository.GetAsync(primaryLayer);