Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.Queries
{
Expand All @@ -12,10 +14,22 @@ public interface IQueryLayerComposer
/// Builds a top-level filter from constraints, used to determine total resource count.
/// </summary>
FilterExpression GetTopFilter();

/// <summary>
/// Collects constraints and builds a <see cref="QueryLayer"/> out of them, used to retrieve the actual resources.
/// </summary>
QueryLayer Compose(ResourceContext requestResource);

/// <summary>
/// Wraps a layer for a secondary endpoint into a primary layer, rewriting top-level includes.
/// </summary>
QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, ResourceContext primaryResourceContext,
TId primaryId, RelationshipAttribute secondaryRelationship);

/// <summary>
/// Gets the secondary projection for a relationship endpoint.
/// </summary>
IDictionary<ResourceFieldAttribute, QueryLayer> GetSecondaryProjectionForRelationshipEndpoint(
ResourceContext secondaryResourceContext);
}
}
49 changes: 49 additions & 0 deletions src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,55 @@ private static IReadOnlyCollection<IncludeElementExpression> ApplyIncludeElement
return newIncludeElements;
}

/// <inheritdoc />
public QueryLayer WrapLayerForSecondaryEndpoint<TId>(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<ResourceFieldAttribute, QueryLayer>();
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>(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<ResourceFieldAttribute, QueryLayer> 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<ResourceFieldAttribute, QueryLayer>();
secondaryProjection[secondaryIdAttribute] = null;

return secondaryProjection;
}

protected virtual IReadOnlyCollection<IncludeElementExpression> GetIncludeElements(IReadOnlyCollection<IncludeElementExpression> includeElements, ResourceContext resourceContext)
{
if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext));
Expand Down
22 changes: 21 additions & 1 deletion src/JsonApiDotNetCore/Resources/ResourceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -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();
Expand Down
49 changes: 7 additions & 42 deletions src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,18 @@ public virtual async Task<TResource> GetRelationshipAsync(TId id, string relatio
_hookExecutor?.BeforeRead<TResource>(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<ResourceFieldAttribute, QueryLayer>
{
[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();
}
Expand All @@ -233,7 +227,7 @@ public virtual async Task<object> GetSecondaryAsync(TId id, string relationshipN
_hookExecutor?.BeforeRead<TResource>(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);

Expand All @@ -249,35 +243,6 @@ public virtual async Task<object> 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<ResourceFieldAttribute, QueryLayer>
{
[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});
}

/// <inheritdoc />
public virtual async Task<TResource> UpdateAsync(TId id, TResource requestResource)
{
Expand Down Expand Up @@ -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);
Expand Down