Skip to content

Commit 417692b

Browse files
author
Bart Koelman
authored
Fix: Moved logic from ResourceService to Composer, so it can invoke ResourceDefinition callbacks (#816)
1 parent 6f72110 commit 417692b

File tree

4 files changed

+92
-44
lines changed

4 files changed

+92
-44
lines changed

src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using System.Collections.Generic;
12
using JsonApiDotNetCore.Configuration;
23
using JsonApiDotNetCore.Queries.Expressions;
4+
using JsonApiDotNetCore.Resources.Annotations;
35

46
namespace JsonApiDotNetCore.Queries
57
{
@@ -12,10 +14,22 @@ public interface IQueryLayerComposer
1214
/// Builds a top-level filter from constraints, used to determine total resource count.
1315
/// </summary>
1416
FilterExpression GetTopFilter();
15-
17+
1618
/// <summary>
1719
/// Collects constraints and builds a <see cref="QueryLayer"/> out of them, used to retrieve the actual resources.
1820
/// </summary>
1921
QueryLayer Compose(ResourceContext requestResource);
22+
23+
/// <summary>
24+
/// Wraps a layer for a secondary endpoint into a primary layer, rewriting top-level includes.
25+
/// </summary>
26+
QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, ResourceContext primaryResourceContext,
27+
TId primaryId, RelationshipAttribute secondaryRelationship);
28+
29+
/// <summary>
30+
/// Gets the secondary projection for a relationship endpoint.
31+
/// </summary>
32+
IDictionary<ResourceFieldAttribute, QueryLayer> GetSecondaryProjectionForRelationshipEndpoint(
33+
ResourceContext secondaryResourceContext);
2034
}
2135
}

src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,55 @@ private static IReadOnlyCollection<IncludeElementExpression> ApplyIncludeElement
176176
return newIncludeElements;
177177
}
178178

179+
/// <inheritdoc />
180+
public QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, RelationshipAttribute secondaryRelationship)
181+
{
182+
var innerInclude = secondaryLayer.Include;
183+
secondaryLayer.Include = null;
184+
185+
var primaryIdAttribute = primaryResourceContext.Attributes.Single(x => x.Property.Name == nameof(Identifiable.Id));
186+
var sparseFieldSet = new SparseFieldSetExpression(new[] { primaryIdAttribute });
187+
188+
var primaryProjection = GetSparseFieldSetProjection(new[] { sparseFieldSet }, primaryResourceContext) ?? new Dictionary<ResourceFieldAttribute, QueryLayer>();
189+
primaryProjection[secondaryRelationship] = secondaryLayer;
190+
primaryProjection[primaryIdAttribute] = null;
191+
192+
return new QueryLayer(primaryResourceContext)
193+
{
194+
Include = RewriteIncludeForSecondaryEndpoint(innerInclude, secondaryRelationship),
195+
Filter = CreateFilterById(primaryId, primaryResourceContext),
196+
Projection = primaryProjection
197+
};
198+
}
199+
200+
private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude, RelationshipAttribute secondaryRelationship)
201+
{
202+
var parentElement = relativeInclude != null
203+
? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements)
204+
: new IncludeElementExpression(secondaryRelationship);
205+
206+
return new IncludeExpression(new[] {parentElement});
207+
}
208+
209+
private FilterExpression CreateFilterById<TId>(TId id, ResourceContext resourceContext)
210+
{
211+
var primaryIdAttribute = resourceContext.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id));
212+
213+
return new ComparisonExpression(ComparisonOperator.Equals,
214+
new ResourceFieldChainExpression(primaryIdAttribute), new LiteralConstantExpression(id.ToString()));
215+
}
216+
217+
public IDictionary<ResourceFieldAttribute, QueryLayer> GetSecondaryProjectionForRelationshipEndpoint(ResourceContext secondaryResourceContext)
218+
{
219+
var secondaryIdAttribute = secondaryResourceContext.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id));
220+
var sparseFieldSet = new SparseFieldSetExpression(new[] { secondaryIdAttribute });
221+
222+
var secondaryProjection = GetSparseFieldSetProjection(new[] { sparseFieldSet }, secondaryResourceContext) ?? new Dictionary<ResourceFieldAttribute, QueryLayer>();
223+
secondaryProjection[secondaryIdAttribute] = null;
224+
225+
return secondaryProjection;
226+
}
227+
179228
protected virtual IReadOnlyCollection<IncludeElementExpression> GetIncludeElements(IReadOnlyCollection<IncludeElementExpression> includeElements, ResourceContext resourceContext)
180229
{
181230
if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext));

src/JsonApiDotNetCore/Resources/ResourceFactory.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Linq;
44
using System.Linq.Expressions;
55
using System.Reflection;
6+
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
7+
using Microsoft.EntityFrameworkCore;
68
using Microsoft.Extensions.DependencyInjection;
79

810
namespace JsonApiDotNetCore.Resources
@@ -73,7 +75,12 @@ public NewExpression CreateNewExpression(Type resourceType)
7375
object constructorArgument =
7476
ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, constructorParameter.ParameterType);
7577

76-
constructorArguments.Add(Expression.Constant(constructorArgument));
78+
var argumentExpression = typeof(DbContext).Assembly.GetName().Version.Major >= 5
79+
// Workaround for https://github.com/dotnet/efcore/issues/20502 to not fail on injected DbContext in EF Core 5.
80+
? CreateTupleAccessExpressionForConstant(constructorArgument, constructorArgument.GetType())
81+
: Expression.Constant(constructorArgument);
82+
83+
constructorArguments.Add(argumentExpression);
7784
}
7885
catch (Exception exception)
7986
{
@@ -86,6 +93,19 @@ public NewExpression CreateNewExpression(Type resourceType)
8693
return Expression.New(longestConstructor, constructorArguments);
8794
}
8895

96+
private static Expression CreateTupleAccessExpressionForConstant(object value, Type type)
97+
{
98+
MethodInfo tupleCreateMethod = typeof(Tuple).GetMethods()
99+
.Single(m => m.Name == "Create" && m.IsGenericMethod && m.GetGenericArguments().Length == 1);
100+
101+
MethodInfo constructedTupleCreateMethod = tupleCreateMethod.MakeGenericMethod(type);
102+
103+
ConstantExpression constantExpression = Expression.Constant(value, type);
104+
105+
MethodCallExpression tupleCreateCall = Expression.Call(constructedTupleCreateMethod, constantExpression);
106+
return Expression.Property(tupleCreateCall, "Item1");
107+
}
108+
89109
private static bool HasSingleConstructorWithoutParameters(Type type)
90110
{
91111
ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray();

src/JsonApiDotNetCore/Services/JsonApiResourceService.cs

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -196,24 +196,18 @@ public virtual async Task<TResource> GetRelationshipAsync(TId id, string relatio
196196
_hookExecutor?.BeforeRead<TResource>(ResourcePipeline.GetRelationship, id.ToString());
197197

198198
var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource);
199-
200-
var secondaryIdAttribute = _request.SecondaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id));
201-
199+
secondaryLayer.Projection = _queryLayerComposer.GetSecondaryProjectionForRelationshipEndpoint(_request.SecondaryResource);
202200
secondaryLayer.Include = null;
203-
secondaryLayer.Projection = new Dictionary<ResourceFieldAttribute, QueryLayer>
204-
{
205-
[secondaryIdAttribute] = null
206-
};
207-
208-
var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id);
201+
202+
var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship);
209203

210204
var primaryResources = await _repository.GetAsync(primaryLayer);
211-
205+
212206
var primaryResource = primaryResources.SingleOrDefault();
213207
AssertPrimaryResourceExists(primaryResource);
214208

215209
if (_hookExecutor != null)
216-
{
210+
{
217211
_hookExecutor.AfterRead(AsList(primaryResource), ResourcePipeline.GetRelationship);
218212
primaryResource = _hookExecutor.OnReturn(AsList(primaryResource), ResourcePipeline.GetRelationship).Single();
219213
}
@@ -233,7 +227,7 @@ public virtual async Task<object> GetSecondaryAsync(TId id, string relationshipN
233227
_hookExecutor?.BeforeRead<TResource>(ResourcePipeline.GetRelationship, id.ToString());
234228

235229
var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource);
236-
var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id);
230+
var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship);
237231

238232
var primaryResources = await _repository.GetAsync(primaryLayer);
239233

@@ -249,35 +243,6 @@ public virtual async Task<object> GetSecondaryAsync(TId id, string relationshipN
249243
return _request.Relationship.GetValue(primaryResource);
250244
}
251245

252-
private QueryLayer GetPrimaryLayerForSecondaryEndpoint(QueryLayer secondaryLayer, TId primaryId)
253-
{
254-
var innerInclude = secondaryLayer.Include;
255-
secondaryLayer.Include = null;
256-
257-
var primaryIdAttribute =
258-
_request.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id));
259-
260-
return new QueryLayer(_request.PrimaryResource)
261-
{
262-
Include = RewriteIncludeForSecondaryEndpoint(innerInclude),
263-
Filter = CreateFilterById(primaryId),
264-
Projection = new Dictionary<ResourceFieldAttribute, QueryLayer>
265-
{
266-
[primaryIdAttribute] = null,
267-
[_request.Relationship] = secondaryLayer
268-
}
269-
};
270-
}
271-
272-
private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude)
273-
{
274-
var parentElement = relativeInclude != null
275-
? new IncludeElementExpression(_request.Relationship, relativeInclude.Elements)
276-
: new IncludeElementExpression(_request.Relationship);
277-
278-
return new IncludeExpression(new[] {parentElement});
279-
}
280-
281246
/// <inheritdoc />
282247
public virtual async Task<TResource> UpdateAsync(TId id, TResource requestResource)
283248
{
@@ -320,7 +285,7 @@ public virtual async Task UpdateRelationshipAsync(TId id, string relationshipNam
320285
AssertRelationshipExists(relationshipName);
321286

322287
var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource);
323-
var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id);
288+
var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship);
324289
primaryLayer.Projection = null;
325290

326291
var primaryResources = await _repository.GetAsync(primaryLayer);

0 commit comments

Comments
 (0)