Skip to content

Commit f600299

Browse files
committed
Experiments to fix #1671
1 parent c42c4b6 commit f600299

File tree

7 files changed

+162
-15
lines changed

7 files changed

+162
-15
lines changed

src/Examples/GettingStarted/Data/SampleDbContext.cs

+3
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ public class SampleDbContext(DbContextOptions<SampleDbContext> options)
99
: DbContext(options)
1010
{
1111
public DbSet<Book> Books => Set<Book>();
12+
public DbSet<House> Houses => Set<House>();
13+
public DbSet<TinyHouse> TinyHouses => Set<TinyHouse>();
14+
public DbSet<BigHouse> BigHouses => Set<BigHouse>();
1215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using GettingStarted.Models;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Queries.Expressions;
5+
using JsonApiDotNetCore.Queries.Parsing;
6+
using JsonApiDotNetCore.Resources;
7+
8+
namespace GettingStarted.Definitions;
9+
10+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
11+
internal sealed class PersonDefinition(IResourceGraph resourceGraph, IResourceFactory resourceFactory)
12+
: JsonApiResourceDefinition<Person, int>(resourceGraph)
13+
{
14+
private readonly IResourceFactory _resourceFactory = resourceFactory;
15+
16+
public override FilterExpression OnApplyFilter(FilterExpression? existingFilter)
17+
{
18+
var parser = new FilterParser(_resourceFactory);
19+
FilterExpression isNotDeleted = parser.Parse("equals(isDeleted,'false')", ResourceType);
20+
FilterExpression hasBooksWithName = parser.Parse("has(books,equals(author.name,'Mary Shelley'))", ResourceType);
21+
FilterExpression isTypeWithCond = parser.Parse("isType(house,bigHouses,equals(floorCount,'3'))", ResourceType);
22+
23+
return LogicalExpression.Compose(LogicalOperator.And, isNotDeleted, hasBooksWithName, isTypeWithCond,existingFilter)!;
24+
}
25+
}

src/Examples/GettingStarted/Models/Person.cs

+19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ public sealed class Person : Identifiable<int>
1111
[Attr]
1212
public string Name { get; set; } = null!;
1313

14+
[Attr]
15+
public bool IsDeleted { get; set; }
16+
1417
[HasMany]
1518
public ICollection<Book> Books { get; set; } = new List<Book>();
19+
20+
[HasOne]
21+
public House? House { get; set; }
22+
}
23+
24+
[Resource]
25+
public abstract class House : Identifiable<int>;
26+
27+
[Resource]
28+
public sealed class TinyHouse : House;
29+
30+
[Resource]
31+
public sealed class BigHouse : House
32+
{
33+
[Attr]
34+
public int? FloorCount { get; set; }
1635
}

src/Examples/GettingStarted/Program.cs

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using GettingStarted.Data;
3+
using GettingStarted.Definitions;
34
using GettingStarted.Models;
45
using JsonApiDotNetCore.Configuration;
56
using Microsoft.EntityFrameworkCore;
@@ -28,6 +29,8 @@
2829
#endif
2930
});
3031

32+
builder.Services.AddResourceDefinition<PersonDefinition>();
33+
3134
WebApplication app = builder.Build();
3235

3336
// Configure the HTTP request pipeline.
@@ -69,23 +72,32 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext)
6972
PublishYear = 1818,
7073
Author = new Person
7174
{
72-
Name = "Mary Shelley"
75+
Name = "Mary Shelley",
76+
House = new BigHouse
77+
{
78+
FloorCount = 3
79+
}
7380
}
7481
}, new Book
7582
{
7683
Title = "Robinson Crusoe",
7784
PublishYear = 1719,
7885
Author = new Person
7986
{
80-
Name = "Daniel Defoe"
87+
Name = "Daniel Defoe",
88+
House = new TinyHouse()
8189
}
8290
}, new Book
8391
{
8492
Title = "Gulliver's Travels",
8593
PublishYear = 1726,
8694
Author = new Person
8795
{
88-
Name = "Jonathan Swift"
96+
Name = "Jonathan Swift",
97+
House = new BigHouse
98+
{
99+
FloorCount = 4
100+
}
89101
}
90102
});
91103

src/Examples/GettingStarted/Properties/launchSettings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
"IIS Express": {
1212
"commandName": "IISExpress",
1313
"launchBrowser": true,
14-
"launchUrl": "api/people?include=books",
14+
"launchUrl": "api/people/1/books",
1515
"environmentVariables": {
1616
"ASPNETCORE_ENVIRONMENT": "Development"
1717
}
1818
},
1919
"Kestrel": {
2020
"commandName": "Project",
2121
"launchBrowser": true,
22-
"launchUrl": "api/people?include=books",
22+
"launchUrl": "api/people/1/books",
2323
"applicationUrl": "http://localhost:14141",
2424
"environmentVariables": {
2525
"ASPNETCORE_ENVIRONMENT": "Development"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Collections.Immutable;
2+
using JsonApiDotNetCore.Queries.Expressions;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCore.Queries;
6+
7+
internal sealed class ChainInsertionFilterRewriter : QueryExpressionRewriter<object?>
8+
{
9+
private readonly ResourceFieldAttribute _fieldToInsert;
10+
private bool _isInNestedScope;
11+
12+
public ChainInsertionFilterRewriter(ResourceFieldAttribute fieldToInsert)
13+
{
14+
ArgumentNullException.ThrowIfNull(fieldToInsert);
15+
16+
_fieldToInsert = fieldToInsert;
17+
}
18+
19+
public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object? argument)
20+
{
21+
if (_isInNestedScope)
22+
{
23+
return expression;
24+
}
25+
26+
IImmutableList<ResourceFieldAttribute> newFields = expression.Fields.Insert(0, _fieldToInsert);
27+
return new ResourceFieldChainExpression(newFields);
28+
}
29+
30+
public override QueryExpression? VisitHas(HasExpression expression, object? argument)
31+
{
32+
if (Visit(expression.TargetCollection, argument) is ResourceFieldChainExpression newTargetCollection)
33+
{
34+
var backupIsInNestedScope = _isInNestedScope;
35+
_isInNestedScope = true;
36+
FilterExpression? newFilter = expression.Filter != null ? Visit(expression.Filter, argument) as FilterExpression : null;
37+
_isInNestedScope = backupIsInNestedScope;
38+
39+
var newExpression = new HasExpression(newTargetCollection, newFilter);
40+
return newExpression.Equals(expression) ? expression : newExpression;
41+
}
42+
43+
return null;
44+
}
45+
46+
public override QueryExpression VisitIsType(IsTypeExpression expression, object? argument)
47+
{
48+
ResourceFieldChainExpression? newTargetToOneRelationship = expression.TargetToOneRelationship != null
49+
? Visit(expression.TargetToOneRelationship, argument) as ResourceFieldChainExpression
50+
: null;
51+
52+
var backupIsInNestedScope = _isInNestedScope;
53+
_isInNestedScope = true;
54+
FilterExpression? newChild = expression.Child != null ? Visit(expression.Child, argument) as FilterExpression : null;
55+
_isInNestedScope = backupIsInNestedScope;
56+
57+
var newExpression = new IsTypeExpression(newTargetToOneRelationship, expression.DerivedType, newChild);
58+
return newExpression.Equals(expression) ? expression : newExpression;
59+
}
60+
}

src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs

+38-10
Original file line numberDiff line numberDiff line change
@@ -98,36 +98,64 @@ public QueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintProvid
9898
FilterExpression? primaryFilter = GetFilter(Array.Empty<QueryExpression>(), hasManyRelationship.LeftType);
9999
FilterExpression? secondaryFilter = GetFilter(filtersInSecondaryScope, hasManyRelationship.RightType);
100100

101-
FilterExpression inverseFilter = GetInverseRelationshipFilter(primaryId, hasManyRelationship, inverseRelationship);
101+
if (primaryFilter != null)
102+
{
103+
// This would hide total resource count on secondary to-one endpoint, but at least not crash anymore.
104+
// return null;
105+
}
106+
107+
FilterExpression inverseFilter = GetInverseRelationshipFilter(primaryId, hasManyRelationship, inverseRelationship, primaryFilter);
102108

103-
return LogicalExpression.Compose(LogicalOperator.And, inverseFilter, primaryFilter, secondaryFilter);
109+
return LogicalExpression.Compose(LogicalOperator.And, inverseFilter, secondaryFilter);
104110
}
105111

106112
private static FilterExpression GetInverseRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
107-
RelationshipAttribute inverseRelationship)
113+
RelationshipAttribute inverseRelationship, FilterExpression? primaryFilter)
108114
{
109115
return inverseRelationship is HasManyAttribute hasManyInverseRelationship
110-
? GetInverseHasManyRelationshipFilter(primaryId, relationship, hasManyInverseRelationship)
111-
: GetInverseHasOneRelationshipFilter(primaryId, relationship, (HasOneAttribute)inverseRelationship);
116+
? GetInverseHasManyRelationshipFilter(primaryId, relationship, hasManyInverseRelationship, primaryFilter)
117+
: GetInverseHasOneRelationshipFilter(primaryId, relationship, (HasOneAttribute)inverseRelationship, primaryFilter);
112118
}
113119

114-
private static ComparisonExpression GetInverseHasOneRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
115-
HasOneAttribute inverseRelationship)
120+
private static FilterExpression GetInverseHasOneRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
121+
HasOneAttribute inverseRelationship, FilterExpression? primaryFilter)
116122
{
117123
AttrAttribute idAttribute = GetIdAttribute(relationship.LeftType);
118124
var idChain = new ResourceFieldChainExpression(ImmutableArray.Create<ResourceFieldAttribute>(inverseRelationship, idAttribute));
125+
var idComparison = new ComparisonExpression(ComparisonOperator.Equals, idChain, new LiteralConstantExpression(primaryId));
126+
127+
FilterExpression? newPrimaryFilter = null;
119128

120-
return new ComparisonExpression(ComparisonOperator.Equals, idChain, new LiteralConstantExpression(primaryId));
129+
if (primaryFilter != null)
130+
{
131+
// many-to-one. This is the hard part. We can special-case for built-in has() and isType() usage, however third-party filters can't participate.
132+
// Because there is no way of indicating in an expression "this chain belongs to something related"; the parsers only know that.
133+
// For example, see the third-party SumExpression.Selector with SumFilterParser in test project.
134+
135+
// For example:
136+
// input: and(equals(isDeleted,'false'), has(books ,equals(author.name,'Mary Shelley')),isType( house,bigHouses,equals(floorCount,'3')))
137+
// output: and(equals(author.isDeleted,'false'),has(author.books,equals(author.name,'Mary Shelley')),isType(author.house,bigHouses,equals(floorCount,'3')))
138+
// ^ ^ ^! ^ ^!
139+
// Note how some chains are updated, while others (expressions on related types) are intentionally not.
140+
141+
var rewriter = new ChainInsertionFilterRewriter(inverseRelationship);
142+
newPrimaryFilter = (FilterExpression?)rewriter.Visit(primaryFilter, null);
143+
}
144+
145+
return LogicalExpression.Compose(LogicalOperator.And, idComparison, newPrimaryFilter)!;
121146
}
122147

123148
private static HasExpression GetInverseHasManyRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
124-
HasManyAttribute inverseRelationship)
149+
HasManyAttribute inverseRelationship, FilterExpression? primaryFilter)
125150
{
151+
// many-to-many. This one is easy, we can just push into the sub-condition of has().
152+
126153
AttrAttribute idAttribute = GetIdAttribute(relationship.LeftType);
127154
var idChain = new ResourceFieldChainExpression(ImmutableArray.Create<ResourceFieldAttribute>(idAttribute));
128155
var idComparison = new ComparisonExpression(ComparisonOperator.Equals, idChain, new LiteralConstantExpression(primaryId));
129156

130-
return new HasExpression(new ResourceFieldChainExpression(inverseRelationship), idComparison);
157+
FilterExpression filter = LogicalExpression.Compose(LogicalOperator.And, idComparison, primaryFilter)!;
158+
return new HasExpression(new ResourceFieldChainExpression(inverseRelationship), filter);
131159
}
132160

133161
/// <inheritdoc />

0 commit comments

Comments
 (0)