Skip to content

Commit a99ee6e

Browse files
authored
Merge pull request #3 from projekt202/feature/Add-Conditional-Has-Filter
feat: Added Conditional Has filter
2 parents dfee9ea + 2f82a71 commit a99ee6e

File tree

14 files changed

+221
-32
lines changed

14 files changed

+221
-32
lines changed

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ Modified by Projekt202</Description>
1818
<DebugType>embedded</DebugType>
1919
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
2020
<AssemblyName>Projekt202.JsonApiDotNetCore</AssemblyName>
21-
<Version>4.1.2.1</Version>
21+
<Version>4.1.2.2</Version>
2222
<RepositoryUrl>https://github.com/projekt202/JsonApiDotNetCore</RepositoryUrl>
23-
<AssemblyVersion>4.1.2.1</AssemblyVersion>
24-
<FileVersion>4.1.2.1</FileVersion>
23+
<AssemblyVersion>4.1.2.2</AssemblyVersion>
24+
<FileVersion>4.1.2.2</FileVersion>
2525
</PropertyGroup>
2626

2727
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1+
using System;
2+
using System.Text;
13
using JetBrains.Annotations;
24
using JsonApiDotNetCore.Queries.Internal.Parsing;
35

46
namespace JsonApiDotNetCore.Queries.Expressions
57
{
68
/// <summary>
7-
/// Represents the "has" filter function, resulting from text such as: has(articles)
9+
/// Represents the "has" filter function, resulting from text such as: has(articles) or has(articles,equals(isHidden,'false'))
810
/// </summary>
911
[PublicAPI]
1012
public class CollectionNotEmptyExpression : FilterExpression
1113
{
1214
public ResourceFieldChainExpression TargetCollection { get; }
1315

14-
public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection)
16+
public FilterExpression Filter { get; }
17+
18+
public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection, FilterExpression filter)
1519
{
1620
ArgumentGuard.NotNull(targetCollection, nameof(targetCollection));
1721

1822
TargetCollection = targetCollection;
23+
Filter = filter;
1924
}
2025

2126
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
@@ -25,7 +30,20 @@ public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgum
2530

2631
public override string ToString()
2732
{
28-
return $"{Keywords.Has}({TargetCollection})";
33+
var builder = new StringBuilder();
34+
builder.Append(Keywords.Has);
35+
builder.Append('(');
36+
builder.Append(TargetCollection);
37+
38+
if (Filter != null)
39+
{
40+
builder.Append(',');
41+
builder.Append(Filter);
42+
}
43+
44+
builder.Append(')');
45+
46+
return builder.ToString();
2947
}
3048

3149
public override bool Equals(object obj)
@@ -42,12 +60,12 @@ public override bool Equals(object obj)
4260

4361
var other = (CollectionNotEmptyExpression)obj;
4462

45-
return TargetCollection.Equals(other.TargetCollection);
63+
return TargetCollection.Equals(other.TargetCollection) && Equals(Filter, other.Filter);
4664
}
4765

4866
public override int GetHashCode()
4967
{
50-
return TargetCollection.GetHashCode();
68+
return HashCode.Combine(TargetCollection, Filter);
5169
}
5270
}
5371
}

src/JsonApiDotNetCore/Queries/Expressions/FilterExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace JsonApiDotNetCore.Queries.Expressions
22
{
33
/// <summary>
4-
/// Represents the base type for filter functions.
4+
/// Represents the base type for filter functions that return a boolean value.
55
/// </summary>
66
public abstract class FilterExpression : FunctionExpression
77
{

src/JsonApiDotNetCore/Queries/Expressions/FunctionExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace JsonApiDotNetCore.Queries.Expressions
22
{
33
/// <summary>
4-
/// Represents the base type for functions.
4+
/// Represents the base type for functions that return a value.
55
/// </summary>
66
public abstract class FunctionExpression : QueryExpression
77
{

src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ namespace JsonApiDotNetCore.Queries.Expressions
1414
public class LogicalExpression : FilterExpression
1515
{
1616
public LogicalOperator Operator { get; }
17-
public IReadOnlyCollection<QueryExpression> Terms { get; }
17+
public IReadOnlyCollection<FilterExpression> Terms { get; }
1818

19-
public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection<QueryExpression> terms)
19+
public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection<FilterExpression> terms)
2020
{
2121
ArgumentGuard.NotNull(terms, nameof(terms));
2222

src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ namespace JsonApiDotNetCore.Queries.Expressions
99
[PublicAPI]
1010
public class NotExpression : FilterExpression
1111
{
12-
public QueryExpression Child { get; }
12+
public FilterExpression Child { get; }
1313

14-
public NotExpression(QueryExpression child)
14+
public NotExpression(FilterExpression child)
1515
{
1616
ArgumentGuard.NotNull(child, nameof(child));
1717

src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public override QueryExpression VisitLogical(LogicalExpression expression, TArgu
5454
{
5555
if (expression != null)
5656
{
57-
IReadOnlyCollection<QueryExpression> newTerms = VisitSequence(expression.Terms, argument);
57+
IReadOnlyCollection<FilterExpression> newTerms = VisitSequence(expression.Terms, argument);
5858

5959
if (newTerms.Count == 1)
6060
{
@@ -75,9 +75,7 @@ public override QueryExpression VisitNot(NotExpression expression, TArgument arg
7575
{
7676
if (expression != null)
7777
{
78-
QueryExpression newChild = Visit(expression.Child, argument);
79-
80-
if (newChild != null)
78+
if (Visit(expression.Child, argument) is FilterExpression newChild)
8179
{
8280
var newExpression = new NotExpression(newChild);
8381
return newExpression.Equals(expression) ? expression : newExpression;
@@ -93,7 +91,8 @@ public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpres
9391
{
9492
if (Visit(expression.TargetCollection, argument) is ResourceFieldChainExpression newTargetCollection)
9593
{
96-
var newExpression = new CollectionNotEmptyExpression(newTargetCollection);
94+
FilterExpression newFilter = expression.Filter != null ? Visit(expression.Filter, argument) as FilterExpression : null;
95+
var newExpression = new CollectionNotEmptyExpression(newTargetCollection, newFilter);
9796
return newExpression.Equals(expression) ? expression : newExpression;
9897
}
9998
}

src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing
1414
[PublicAPI]
1515
public class FilterParser : QueryExpressionParser
1616
{
17+
private readonly IResourceContextProvider _resourceContextProvider;
1718
private readonly IResourceFactory _resourceFactory;
1819
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
1920
private ResourceContext _resourceContextInScope;
@@ -22,8 +23,10 @@ public FilterParser(IResourceContextProvider resourceContextProvider, IResourceF
2223
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
2324
: base(resourceContextProvider)
2425
{
26+
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
2527
ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory));
2628

29+
_resourceContextProvider = resourceContextProvider;
2730
_resourceFactory = resourceFactory;
2831
_validateSingleFieldCallback = validateSingleFieldCallback;
2932
}
@@ -103,7 +106,7 @@ protected LogicalExpression ParseLogical(string operatorName)
103106
EatText(operatorName);
104107
EatSingleCharacterToken(TokenKind.OpenParen);
105108

106-
var terms = new List<QueryExpression>();
109+
var terms = new List<FilterExpression>();
107110

108111
FilterExpression term = ParseFilter();
109112
terms.Add(term);
@@ -234,10 +237,31 @@ protected CollectionNotEmptyExpression ParseHas()
234237
EatSingleCharacterToken(TokenKind.OpenParen);
235238

236239
ResourceFieldChainExpression targetCollection = ParseFieldChain(FieldChainRequirements.EndsInToMany, null);
240+
FilterExpression filter = null;
241+
242+
if (TokenStack.TryPeek(out Token nextToken) && nextToken.Kind == TokenKind.Comma)
243+
{
244+
EatSingleCharacterToken(TokenKind.Comma);
245+
246+
filter = ParseFilterInHas((HasManyAttribute)targetCollection.Fields.Last());
247+
}
237248

238249
EatSingleCharacterToken(TokenKind.CloseParen);
239250

240-
return new CollectionNotEmptyExpression(targetCollection);
251+
return new CollectionNotEmptyExpression(targetCollection, filter);
252+
}
253+
254+
private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship)
255+
{
256+
ResourceContext outerScopeBackup = _resourceContextInScope;
257+
258+
Type innerResourceType = hasManyRelationship.RightType;
259+
_resourceContextInScope = _resourceContextProvider.GetResourceContext(innerResourceType);
260+
261+
FilterExpression filter = ParseFilter();
262+
263+
_resourceContextInScope = outerScopeBackup;
264+
return filter;
241265
}
242266

243267
protected QueryExpression ParseCountOrField(FieldChainRequirements chainRequirements)

src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ protected virtual Expression ApplyFilter(Expression source, FilterExpression fil
9393
{
9494
using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType);
9595

96-
var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType);
96+
var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType, _nameFactory);
9797
return builder.ApplyWhere(filter);
9898
}
9999

src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,35 @@ public class WhereClauseBuilder : QueryClauseBuilder<Type>
2323

2424
private readonly Expression _source;
2525
private readonly Type _extensionType;
26+
private readonly LambdaParameterNameFactory _nameFactory;
2627

27-
public WhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType)
28+
public WhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType, LambdaParameterNameFactory nameFactory)
2829
: base(lambdaScope)
2930
{
3031
ArgumentGuard.NotNull(source, nameof(source));
3132
ArgumentGuard.NotNull(extensionType, nameof(extensionType));
33+
ArgumentGuard.NotNull(nameFactory, nameof(nameFactory));
3234

3335
_source = source;
3436
_extensionType = extensionType;
37+
_nameFactory = nameFactory;
3538
}
3639

3740
public Expression ApplyWhere(FilterExpression filter)
3841
{
3942
ArgumentGuard.NotNull(filter, nameof(filter));
4043

41-
Expression body = Visit(filter, null);
42-
LambdaExpression lambda = Expression.Lambda(body, LambdaScope.Parameter);
44+
LambdaExpression lambda = GetPredicateLambda(filter);
4345

4446
return WhereExtensionMethodCall(lambda);
4547
}
4648

49+
private LambdaExpression GetPredicateLambda(FilterExpression filter)
50+
{
51+
Expression body = Visit(filter, null);
52+
return Expression.Lambda(body, LambdaScope.Parameter);
53+
}
54+
4755
private Expression WhereExtensionMethodCall(LambdaExpression predicate)
4856
{
4957
return Expression.Call(_extensionType, "Where", LambdaScope.Parameter.Type.AsArray(), _source, predicate);
@@ -60,7 +68,29 @@ public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression
6068
throw new InvalidOperationException("Expression must be a collection.");
6169
}
6270

63-
return AnyExtensionMethodCall(elementType, property);
71+
Expression predicate = null;
72+
73+
if (expression.Filter != null)
74+
{
75+
var hasManyThrough = expression.TargetCollection.Fields.Last() as HasManyThroughAttribute;
76+
var lambdaScopeFactory = new LambdaScopeFactory(_nameFactory, hasManyThrough);
77+
using LambdaScope lambdaScope = lambdaScopeFactory.CreateScope(elementType);
78+
79+
var builder = new WhereClauseBuilder(property, lambdaScope, typeof(Enumerable), _nameFactory);
80+
predicate = builder.GetPredicateLambda(expression.Filter);
81+
}
82+
83+
return AnyExtensionMethodCall(elementType, property, predicate);
84+
}
85+
86+
private static MethodCallExpression AnyExtensionMethodCall(Type elementType, Expression source, Expression predicate)
87+
{
88+
if (predicate != null)
89+
{
90+
return Expression.Call(typeof(Enumerable), "Any", elementType.AsArray(), source, predicate);
91+
}
92+
93+
return Expression.Call(typeof(Enumerable), "Any", elementType.AsArray(), source);
6494
}
6595

6696
private static MethodCallExpression AnyExtensionMethodCall(Type elementType, Expression source)
@@ -275,8 +305,7 @@ protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadO
275305
}
276306

277307
private static string GetPropertyName(ResourceFieldAttribute field)
278-
{
279-
// In case of a HasManyThrough access (from count() or has() function), we only need to look at the number of entries in the join table.
308+
{
280309
return field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name;
281310
}
282311
}

0 commit comments

Comments
 (0)