I'm trying to add filtering functionality to my web api. I have two classes as base class
Global one is:
public abstract class GlobalDto<TKey, TCultureDtoKey, TCultureDto> :
Dto<TKey>,
IGlobalDto<TKey, TCultureDtoKey, TCultureDto>
where TCultureDto : ICultureDto<TCultureDtoKey, TKey>, new()
{
public virtual IList<TCultureDto> Globals { get; set; }
}
and the cultured one is:
public abstract class CultureDto<TKey, TMasterDtoKey> :
SubDto<TKey, TMasterDtoKey>,
ICultureDto<TKey, TMasterDtoKey>
{
public int CultureId { get; set; }
}
also SubDto class is:
public abstract class SubDto<TKey, TMasterDtoKey> : Dto<TKey>, ISubDto<TKey, TMasterDtoKey>
{
public TMasterDtoKey MasterId { get; set; }
}
the scenario I'm trying is filtering the IQueryable GlobalDto dynamically and also filter by its
IList<TCultureDto> Globals { get; set; }
eg:
public class CategoryDto : GlobalDto<int, int, CategoryCultureDto>, IDtoWithSelfReference<int>
{
public int? TopId { get; set; }
[StringLength(20)]
public string Code { get; set; }
public IList<CategoryCoverDto> Covers { get; set; }
}
public class CategoryCultureDto : CultureDto<int, int>
{
[Required]
[StringLength(100)]
public string Name { get; set; }
}
I have tried this answer here and also lot of things but I couldn't make it.
I have property name, operation type (eg: contains, startswith) and comparing value from querystring so it has to be dynamic for various propertynames and various operation types like co(contains) and infinite values like foo.
http://localhost:5000/categories?search=name co foo
after this request
IQueryable<CategoryDto> q;//query
/* Expression building process equals to q.Where(p=>p.Globals.Any(c=>c.Name.Contains("foo")))*/
return q.Where(predicate);//filtered query
But I couldnt make it for globals
Edit: Code I used for doing this.
[HttpGet("/[controller]/Test")]
public IActionResult Test()
{
var propName = "Name";
var expressionProvider = new GlobalStringSearchExpressionProvider();
var value = "foo";
var op = "co";
var propertyInfo = ExpressionHelper
.GetPropertyInfo<CategoryCultureDto>(propName);
var obj = ExpressionHelper.Parameter<CategoryCultureDto>();
// Build up the LINQ expression backwards:
// query = query.Where(x => x.Property == "Value");
// x.Property
var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
// "Value"
var right = expressionProvider.GetValue(value);
// x.Property == "Value"
var comparisonExpression = expressionProvider
.GetComparison(left, op, right);
// x => x.Property == "Value"
var lambdaExpression = ExpressionHelper
.GetLambda<CategoryCultureDto, bool>(obj, comparisonExpression);
var q = _service.GetAll(); //this returns IQueryable<CategoryDto>
var query = q.Where(p => p.Globals.CallWhere(lambdaExpression).Any());
var list = query.ToList();
return Ok(list);
}
public class GlobalStringSearchExpressionProvider : DefaultSearchExpressionProvider
{
private const string StartsWithOperator = "sw";
private const string EndsWithOperator = "ew";
private const string ContainsOperator = "co";
private static readonly MethodInfo StartsWithMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 2);
private static readonly MethodInfo EndsWithMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "EndsWith" && m.GetParameters().Length == 2);
private static readonly MethodInfo StringEqualsMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "Equals" && m.GetParameters().Length == 2);
private static readonly MethodInfo ContainsMethod = typeof(string)
.GetMethods()
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1);
private static readonly ConstantExpression IgnoreCase
= Expression.Constant(StringComparison.OrdinalIgnoreCase);
public override IEnumerable<string> GetOperators()
=> base.GetOperators()
.Concat(new[]
{
StartsWithOperator,
ContainsOperator,
EndsWithOperator
});
public override Expression GetComparison(MemberExpression left, string op, ConstantExpression right)
{
switch (op.ToLower())
{
case StartsWithOperator:
return Expression.Call(left, StartsWithMethod, right, IgnoreCase);
// TODO: This may or may not be case-insensitive, depending
// on how your database translates Contains()
case ContainsOperator:
return Expression.Call(left, ContainsMethod, right);
// Handle the "eq" operator ourselves (with a case-insensitive compare)
case EqualsOperator:
return Expression.Call(left, StringEqualsMethod, right, IgnoreCase);
case EndsWithOperator:
return Expression.Call(left, EndsWithMethod, right);
default: return base.GetComparison(left, op, right);
}
}
}
public static class ExpressionHelper
{
private static readonly MethodInfo LambdaMethod = typeof(Expression)
.GetMethods()
.First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2);
private static readonly MethodInfo[] QueryableMethods = typeof(Queryable)
.GetMethods()
.ToArray();
private static MethodInfo GetLambdaFuncBuilder(Type source, Type dest)
{
var predicateType = typeof(Func<,>).MakeGenericType(source, dest);
return LambdaMethod.MakeGenericMethod(predicateType);
}
public static PropertyInfo GetPropertyInfo<T>(string name)
=> typeof(T).GetProperties()
.Single(p => p.Name == name);
public static ParameterExpression Parameter<T>()
=> Expression.Parameter(typeof(T));
public static ParameterExpression ParameterGlobal(Type type)
=> Expression.Parameter(type);
public static MemberExpression GetPropertyExpression(ParameterExpression obj, PropertyInfo property)
=> Expression.Property(obj, property);
public static LambdaExpression GetLambda<TSource, TDest>(ParameterExpression obj, Expression arg)
=> GetLambda(typeof(TSource), typeof(TDest), obj, arg);
public static LambdaExpression GetLambda(Type source, Type dest, ParameterExpression obj, Expression arg)
{
var lambdaBuilder = GetLambdaFuncBuilder(source, dest);
return (LambdaExpression)lambdaBuilder.Invoke(null, new object[] { arg, new[] { obj } });
}
public static IQueryable<T> CallWhere<T>(this IEnumerable<T> query, LambdaExpression predicate)
{
var whereMethodBuilder = QueryableMethods
.First(x => x.Name == "Where" && x.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T));
return (IQueryable<T>)whereMethodBuilder
.Invoke(null, new object[] { query, predicate });
}
public static IQueryable<T> CallAny<T>(this IEnumerable<T> query, LambdaExpression predicate)
{
var anyMethodBuilder = QueryableMethods
.First(x => x.Name == "Any" && x.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T));
return (IQueryable<T>) anyMethodBuilder
.Invoke(null, new object[] {query, predicate});
}
}
Exception is:
{
"message": "Could not parse expression 'p.Globals.CallWhere(Param_0 => Param_0.Name.Contains(\"stil\"))': This overload of the method 'ImjustCore.CrossCutting.Extensions.Expressions.ExpressionHelper.CallWhere' is currently not supported.",
"detail": " at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.GetNodeType(MethodCallExpression expressionToParse)\n at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseNode(Expression expression, String associatedIdentifier)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseTree(Expression expressionTree)\n at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)\n at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Visit(Expression expression)\n at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)\n at System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor)\n at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)\n at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Visit(Expression expression)\n at Remotion.Linq.Parsing.ExpressionVisitors.SubQueryFindingExpressionVisitor.Process(Expression expressionTree, INodeTypeProvider nodeTypeProvider)\n at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.ProcessArgumentExpression(Expression argumentExpression)\n at System.Linq.Enumerable.SelectListPartitionIterator`2.ToArray()\n at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)\n at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier)\n at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseTree(Expression expressionTree)\n at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot)\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, IDiagnosticsLogger`1 logger, Type contextType)\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass15_0`1.<Execute>b__0()\n at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)\n at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)\n at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)\n at Remotion.Linq.QueryableBase`1.GetEnumerator()\n at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)\n at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)\n at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)\n at ImjustCore.Presentation.Api.Controllers.CategoriesController.Test() in /Users/apple/Desktop/Development/Core/ImjustCore/ImjustCore/ImjustCore.Presentation.Api/Controllers/CategoriesController.cs:line 87\n at lambda_method(Closure , Object , Object[] )\n at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()\n--- End of stack trace from previous location where exception was thrown ---\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()\n--- End of stack trace from previous location where exception was thrown ---\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()\n--- End of stack trace from previous location where exception was thrown ---\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\n at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextExceptionFilterAsync>d__23.MoveNext()"
}
When I apply the lambda expression directly to IQueryable of CategoryDto with same extension classes above
with:
[HttpGet("/[controller]/Test")]
public IActionResult Test()
{
var propName = "Code";
var expressionProvider = new StringSearchExpressionProvider();
var value = "foo";
var op = "co";
var propertyInfo = ExpressionHelper
.GetPropertyInfo<CategoryDto>(propName);
var obj = ExpressionHelper.Parameter<CategoryCultureDto>();
// Build up the LINQ expression backwards:
// query = query.Where(x => x.Property == "Value");
// x.Property
var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
// "Value"
var right = expressionProvider.GetValue(value);
// x.Property == "Value"
var comparisonExpression = expressionProvider
.GetComparison(left, op, right);
// x => x.Property == "Value"
var lambdaExpression = ExpressionHelper
.GetLambda<CategoryDto, bool>(obj, comparisonExpression);
var q = _service.GetAll();
var query = q.CallWhere(lambdaExpression);
var list = query.ToList();
return Ok(list);
}
It works fine. because there is no filtering on child collection and results are filtering properly.
Aucun commentaire:
Enregistrer un commentaire