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
1 change: 1 addition & 0 deletions example/Dto/UserDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record UserDto
public string Firstname { get; set; } = string.Empty;
public string Lastname { get; set; } = string.Empty;
public int Age { get; set; }
public Gender Gender { get; set; }
public bool IsEmailVerified { get; set; }
public double Test { get; set; }
public int? NullableInt { get; set; }
Expand Down
11 changes: 11 additions & 0 deletions example/Entities/User.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;

public record User
{
public Guid Id { get; set; }
public string Firstname { get; set; } = string.Empty;
public string Lastname { get; set; } = string.Empty;
public int Age { get; set; }
public Gender Gender { get; set; }
public bool IsDeleted { get; set; }
public bool IsEmailVerified { get; set; }
public double Test { get; set; }
Expand Down Expand Up @@ -41,4 +43,13 @@ public record Company
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Gender
{
Male,
Female,
[JsonStringEnumMemberName("Alternative")]
Other
}
1 change: 1 addition & 0 deletions example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
.RuleFor(x => x.Firstname, f => f.Person.FirstName)
.RuleFor(x => x.Lastname, f => f.Person.LastName)
.RuleFor(x => x.Age, f => f.Random.Int(0, 100))
.RuleFor(x => x.Gender, f => f.PickRandom<Gender>())
.RuleFor(x => x.IsDeleted, f => f.Random.Bool())
.RuleFor(x => x.Test, f => f.Random.Double())
.RuleFor(x => x.NullableInt, f => f.Random.Bool() ? f.Random.Int(1, 100) : null)
Expand Down
92 changes: 74 additions & 18 deletions src/GoatQuery/src/Evaluator/FilterEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json.Serialization;
using FluentResults;

public static class FilterEvaluator
Expand Down Expand Up @@ -127,11 +128,6 @@ private static Result<MemberExpression> ResolvePropertyPathForCollection(
return (MemberExpression)current;
}

private static bool IsNullableReferenceType(Type type)
{
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
}

private static bool IsPrimitiveType(Type type)
{
return type.IsPrimitive || type == typeof(string) || type == typeof(decimal) ||
Expand Down Expand Up @@ -226,13 +222,13 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
{
return literal switch
{
IntegerLiteral intLit => CreateIntegerConstant(intLit.Value, expression),
IntegerLiteral intLit => CreateIntegerOrEnumConstant(intLit.Value, expression.Type),
DateLiteral dateLit => Result.Ok(CreateDateConstant(dateLit, expression)),
GuidLiteral guidLit => Result.Ok(Expression.Constant(guidLit.Value, expression.Type)),
DecimalLiteral decLit => Result.Ok(Expression.Constant(decLit.Value, expression.Type)),
FloatLiteral floatLit => Result.Ok(Expression.Constant(floatLit.Value, expression.Type)),
DoubleLiteral dblLit => Result.Ok(Expression.Constant(dblLit.Value, expression.Type)),
StringLiteral strLit => Result.Ok(Expression.Constant(strLit.Value, expression.Type)),
StringLiteral strLit => CreateStringOrEnumConstant(strLit.Value, expression.Type),
DateTimeLiteral dtLit => Result.Ok(Expression.Constant(dtLit.Value, expression.Type)),
BooleanLiteral boolLit => Result.Ok(Expression.Constant(boolLit.Value, expression.Type)),
NullLiteral _ => Result.Ok(Expression.Constant(null, expression.Type)),
Expand All @@ -248,11 +244,6 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
return Result.Ok((constantResult.Value, property));
}

private static Result<ConstantExpression> CreateIntegerConstant(int value, Expression expression)
{
return GetIntegerExpressionConstant(value, expression.Type);
}

private static ConstantExpression CreateDateConstant(DateLiteral dateLiteral, Expression expression)
{
if (expression.Type == typeof(DateTime?))
Expand Down Expand Up @@ -563,9 +554,7 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
{
try
{
// Fetch the underlying type if it's nullable.
var underlyingType = Nullable.GetUnderlyingType(targetType);
var type = underlyingType ?? targetType;
var type = GetNonNullableType(targetType);

object convertedValue = type switch
{
Expand All @@ -586,9 +575,76 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
{
return Result.Fail($"Value {value} is too large for type {targetType.Name}");
}
catch (Exception ex)
catch (Exception)
{
return Result.Fail($"Error converting {value} to {targetType.Name}");
}
}

private static Result<ConstantExpression> CreateIntegerOrEnumConstant(int value, Type targetType)
{
var actualType = GetNonNullableType(targetType);

if (actualType.IsEnum)
{
return ConvertIntegerToEnum(value, actualType, targetType);
}

return GetIntegerExpressionConstant(value, targetType);
}

private static Result<ConstantExpression> ConvertIntegerToEnum(int value, Type actualType, Type targetType)
{
try
{
var enumValue = Enum.ToObject(actualType, value);

return Result.Ok(Expression.Constant(enumValue, targetType));
}
catch (Exception)
{
return Result.Fail($"Error converting {value} to {targetType.Name}: {ex.Message}");
return Result.Fail($"Error converting {value} to enum type {targetType.Name}");
}
}
}

private static Result<ConstantExpression> CreateStringOrEnumConstant(string value, Type targetType)
{
var actualType = GetNonNullableType(targetType);

if (actualType.IsEnum)
{
return ConvertStringToEnum(value, actualType, targetType);
}

return Result.Ok(Expression.Constant(value, targetType));
}

private static Result<ConstantExpression> ConvertStringToEnum(string value, Type actualType, Type targetType)
{
try
{
var enumValue = Enum.Parse(actualType, value, true);

return Result.Ok(Expression.Constant(enumValue, targetType));
}
catch (Exception)
{
foreach (var field in actualType.GetFields(BindingFlags.Public | BindingFlags.Static))
{
var memberNameAttribute = field.GetCustomAttribute<JsonStringEnumMemberNameAttribute>();

if (memberNameAttribute != null && memberNameAttribute.Name.Equals(value, StringComparison.Ordinal))
{
return Result.Ok(Expression.Constant(field.GetValue(null), targetType));
}
}

return Result.Fail($"Value '{value}' is not a valid member of enum {actualType.Name}");
}
}

private static Type GetNonNullableType(Type type)
{
return Nullable.GetUnderlyingType(type) ?? type;
}
}
1 change: 0 additions & 1 deletion src/GoatQuery/src/Utilities/PropertyMappingTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ private static bool IsPrimitiveType(Type type)
if (type.IsPrimitive || PrimitiveTypes.Contains(type))
return true;

// Handle nullable types
var underlyingType = Nullable.GetUnderlyingType(type);
return underlyingType != null && (underlyingType.IsPrimitive || PrimitiveTypes.Contains(underlyingType));
}
Expand Down
103 changes: 103 additions & 0 deletions src/GoatQuery/tests/Filter/FilterLexerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,109 @@ public static IEnumerable<object[]> Parameters()
new (TokenType.RPAREN, ")"),
}
};

yield return new object[]
{
"status eq 0",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "status"),
new (TokenType.IDENT, "eq"),
new (TokenType.INT, "0"),
}
};

yield return new object[]
{
"status eq 1",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "status"),
new (TokenType.IDENT, "eq"),
new (TokenType.INT, "1"),
}
};

yield return new object[]
{
"status ne 0",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "status"),
new (TokenType.IDENT, "ne"),
new (TokenType.INT, "0"),
}
};

yield return new object[]
{
"gender eq 'Male'",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "eq"),
new (TokenType.STRING, "Male"),
}
};

yield return new object[]
{
"gender eq 'Female'",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "eq"),
new (TokenType.STRING, "Female"),
}
};

yield return new object[]
{
"gender eq 'Alternative'",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "eq"),
new (TokenType.STRING, "Alternative"),
}
};

yield return new object[]
{
"gender ne 'Male'",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "ne"),
new (TokenType.STRING, "Male"),
}
};

yield return new object[]
{
"gender eq null",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "eq"),
new (TokenType.NULL, "null"),
}
};

yield return new object[]
{
"gender eq 'Male' and status eq 0",
new KeyValuePair<TokenType, string>[]
{
new (TokenType.IDENT, "gender"),
new (TokenType.IDENT, "eq"),
new (TokenType.STRING, "Male"),
new (TokenType.IDENT, "and"),
new (TokenType.IDENT, "status"),
new (TokenType.IDENT, "eq"),
new (TokenType.INT, "0"),
}
};
}

[Theory]
Expand Down
Loading