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