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
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Modified by Projekt202</Description>
<DebugType>embedded</DebugType>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<AssemblyName>Projekt202.JsonApiDotNetCore</AssemblyName>
<Version>4.1.2.1</Version>
<Version>4.1.2.2</Version>
<RepositoryUrl>https://github.com/projekt202/JsonApiDotNetCore</RepositoryUrl>
<AssemblyVersion>4.1.2.1</AssemblyVersion>
<FileVersion>4.1.2.1</FileVersion>
<AssemblyVersion>4.1.2.2</AssemblyVersion>
<FileVersion>4.1.2.2</FileVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
using System;
using System.Text;
using JetBrains.Annotations;
using JsonApiDotNetCore.Queries.Internal.Parsing;

namespace JsonApiDotNetCore.Queries.Expressions
{
/// <summary>
/// Represents the "has" filter function, resulting from text such as: has(articles)
/// Represents the "has" filter function, resulting from text such as: has(articles) or has(articles,equals(isHidden,'false'))
/// </summary>
[PublicAPI]
public class CollectionNotEmptyExpression : FilterExpression
{
public ResourceFieldChainExpression TargetCollection { get; }

public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection)
public FilterExpression Filter { get; }

public CollectionNotEmptyExpression(ResourceFieldChainExpression targetCollection, FilterExpression filter)
{
ArgumentGuard.NotNull(targetCollection, nameof(targetCollection));

TargetCollection = targetCollection;
Filter = filter;
}

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

public override string ToString()
{
return $"{Keywords.Has}({TargetCollection})";
var builder = new StringBuilder();
builder.Append(Keywords.Has);
builder.Append('(');
builder.Append(TargetCollection);

if (Filter != null)
{
builder.Append(',');
builder.Append(Filter);
}

builder.Append(')');

return builder.ToString();
}

public override bool Equals(object obj)
Expand All @@ -42,12 +60,12 @@ public override bool Equals(object obj)

var other = (CollectionNotEmptyExpression)obj;

return TargetCollection.Equals(other.TargetCollection);
return TargetCollection.Equals(other.TargetCollection) && Equals(Filter, other.Filter);
}

public override int GetHashCode()
{
return TargetCollection.GetHashCode();
return HashCode.Combine(TargetCollection, Filter);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace JsonApiDotNetCore.Queries.Expressions
{
/// <summary>
/// Represents the base type for filter functions.
/// Represents the base type for filter functions that return a boolean value.
/// </summary>
public abstract class FilterExpression : FunctionExpression
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace JsonApiDotNetCore.Queries.Expressions
{
/// <summary>
/// Represents the base type for functions.
/// Represents the base type for functions that return a value.
/// </summary>
public abstract class FunctionExpression : QueryExpression
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ namespace JsonApiDotNetCore.Queries.Expressions
public class LogicalExpression : FilterExpression
{
public LogicalOperator Operator { get; }
public IReadOnlyCollection<QueryExpression> Terms { get; }
public IReadOnlyCollection<FilterExpression> Terms { get; }

public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection<QueryExpression> terms)
public LogicalExpression(LogicalOperator @operator, IReadOnlyCollection<FilterExpression> terms)
{
ArgumentGuard.NotNull(terms, nameof(terms));

Expand Down
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace JsonApiDotNetCore.Queries.Expressions
[PublicAPI]
public class NotExpression : FilterExpression
{
public QueryExpression Child { get; }
public FilterExpression Child { get; }

public NotExpression(QueryExpression child)
public NotExpression(FilterExpression child)
{
ArgumentGuard.NotNull(child, nameof(child));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public override QueryExpression VisitLogical(LogicalExpression expression, TArgu
{
if (expression != null)
{
IReadOnlyCollection<QueryExpression> newTerms = VisitSequence(expression.Terms, argument);
IReadOnlyCollection<FilterExpression> newTerms = VisitSequence(expression.Terms, argument);

if (newTerms.Count == 1)
{
Expand All @@ -75,9 +75,7 @@ public override QueryExpression VisitNot(NotExpression expression, TArgument arg
{
if (expression != null)
{
QueryExpression newChild = Visit(expression.Child, argument);

if (newChild != null)
if (Visit(expression.Child, argument) is FilterExpression newChild)
{
var newExpression = new NotExpression(newChild);
return newExpression.Equals(expression) ? expression : newExpression;
Expand All @@ -93,7 +91,8 @@ public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpres
{
if (Visit(expression.TargetCollection, argument) is ResourceFieldChainExpression newTargetCollection)
{
var newExpression = new CollectionNotEmptyExpression(newTargetCollection);
FilterExpression newFilter = expression.Filter != null ? Visit(expression.Filter, argument) as FilterExpression : null;
var newExpression = new CollectionNotEmptyExpression(newTargetCollection, newFilter);
return newExpression.Equals(expression) ? expression : newExpression;
}
}
Expand Down
28 changes: 26 additions & 2 deletions src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing
[PublicAPI]
public class FilterParser : QueryExpressionParser
{
private readonly IResourceContextProvider _resourceContextProvider;
private readonly IResourceFactory _resourceFactory;
private readonly Action<ResourceFieldAttribute, ResourceContext, string> _validateSingleFieldCallback;
private ResourceContext _resourceContextInScope;
Expand All @@ -22,8 +23,10 @@ public FilterParser(IResourceContextProvider resourceContextProvider, IResourceF
Action<ResourceFieldAttribute, ResourceContext, string> validateSingleFieldCallback = null)
: base(resourceContextProvider)
{
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory));

_resourceContextProvider = resourceContextProvider;
_resourceFactory = resourceFactory;
_validateSingleFieldCallback = validateSingleFieldCallback;
}
Expand Down Expand Up @@ -103,7 +106,7 @@ protected LogicalExpression ParseLogical(string operatorName)
EatText(operatorName);
EatSingleCharacterToken(TokenKind.OpenParen);

var terms = new List<QueryExpression>();
var terms = new List<FilterExpression>();

FilterExpression term = ParseFilter();
terms.Add(term);
Expand Down Expand Up @@ -234,10 +237,31 @@ protected CollectionNotEmptyExpression ParseHas()
EatSingleCharacterToken(TokenKind.OpenParen);

ResourceFieldChainExpression targetCollection = ParseFieldChain(FieldChainRequirements.EndsInToMany, null);
FilterExpression filter = null;

if (TokenStack.TryPeek(out Token nextToken) && nextToken.Kind == TokenKind.Comma)
{
EatSingleCharacterToken(TokenKind.Comma);

filter = ParseFilterInHas((HasManyAttribute)targetCollection.Fields.Last());
}

EatSingleCharacterToken(TokenKind.CloseParen);

return new CollectionNotEmptyExpression(targetCollection);
return new CollectionNotEmptyExpression(targetCollection, filter);
}

private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship)
{
ResourceContext outerScopeBackup = _resourceContextInScope;

Type innerResourceType = hasManyRelationship.RightType;
_resourceContextInScope = _resourceContextProvider.GetResourceContext(innerResourceType);

FilterExpression filter = ParseFilter();

_resourceContextInScope = outerScopeBackup;
return filter;
}

protected QueryExpression ParseCountOrField(FieldChainRequirements chainRequirements)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ protected virtual Expression ApplyFilter(Expression source, FilterExpression fil
{
using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType);

var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType);
var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType, _nameFactory);
return builder.ApplyWhere(filter);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,35 @@ public class WhereClauseBuilder : QueryClauseBuilder<Type>

private readonly Expression _source;
private readonly Type _extensionType;
private readonly LambdaParameterNameFactory _nameFactory;

public WhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType)
public WhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType, LambdaParameterNameFactory nameFactory)
: base(lambdaScope)
{
ArgumentGuard.NotNull(source, nameof(source));
ArgumentGuard.NotNull(extensionType, nameof(extensionType));
ArgumentGuard.NotNull(nameFactory, nameof(nameFactory));

_source = source;
_extensionType = extensionType;
_nameFactory = nameFactory;
}

public Expression ApplyWhere(FilterExpression filter)
{
ArgumentGuard.NotNull(filter, nameof(filter));

Expression body = Visit(filter, null);
LambdaExpression lambda = Expression.Lambda(body, LambdaScope.Parameter);
LambdaExpression lambda = GetPredicateLambda(filter);

return WhereExtensionMethodCall(lambda);
}

private LambdaExpression GetPredicateLambda(FilterExpression filter)
{
Expression body = Visit(filter, null);
return Expression.Lambda(body, LambdaScope.Parameter);
}

private Expression WhereExtensionMethodCall(LambdaExpression predicate)
{
return Expression.Call(_extensionType, "Where", LambdaScope.Parameter.Type.AsArray(), _source, predicate);
Expand All @@ -60,7 +68,29 @@ public override Expression VisitCollectionNotEmpty(CollectionNotEmptyExpression
throw new InvalidOperationException("Expression must be a collection.");
}

return AnyExtensionMethodCall(elementType, property);
Expression predicate = null;

if (expression.Filter != null)
{
var hasManyThrough = expression.TargetCollection.Fields.Last() as HasManyThroughAttribute;
var lambdaScopeFactory = new LambdaScopeFactory(_nameFactory, hasManyThrough);
using LambdaScope lambdaScope = lambdaScopeFactory.CreateScope(elementType);

var builder = new WhereClauseBuilder(property, lambdaScope, typeof(Enumerable), _nameFactory);
predicate = builder.GetPredicateLambda(expression.Filter);
}

return AnyExtensionMethodCall(elementType, property, predicate);
}

private static MethodCallExpression AnyExtensionMethodCall(Type elementType, Expression source, Expression predicate)
{
if (predicate != null)
{
return Expression.Call(typeof(Enumerable), "Any", elementType.AsArray(), source, predicate);
}

return Expression.Call(typeof(Enumerable), "Any", elementType.AsArray(), source);
}

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

private static string GetPropertyName(ResourceFieldAttribute field)
{
// 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.
{
return field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private static bool IsCarId(PropertyInfo property)

private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression existingCarIdChain, IEnumerable<string> carStringIds)
{
var outerTerms = new List<QueryExpression>();
var outerTerms = new List<FilterExpression>();

foreach (string carStringId in carStringIds)
{
Expand All @@ -92,15 +92,15 @@ private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression
StringId = carStringId
};

QueryExpression keyComparison = CreateEqualityComparisonOnCompositeKey(existingCarIdChain, tempCar.RegionId, tempCar.LicensePlate);
FilterExpression keyComparison = CreateEqualityComparisonOnCompositeKey(existingCarIdChain, tempCar.RegionId, tempCar.LicensePlate);
outerTerms.Add(keyComparison);
}

return outerTerms.Count == 1 ? outerTerms[0] : new LogicalExpression(LogicalOperator.Or, outerTerms);
}

private QueryExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldChainExpression existingCarIdChain, long regionIdValue,
string licensePlateValue)
private FilterExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldChainExpression existingCarIdChain, long regionIdValue,
string licensePlateValue)
{
ResourceFieldChainExpression regionIdChain = ReplaceLastAttributeInChain(existingCarIdChain, _regionIdAttribute);

Expand Down
Loading