I'm trying to figure out a way to structure my data so that it is model bindable. My Issue is that I have to create a query filter which can represent multiple expressions in data.
For example:
x => (x.someProperty == true && x.someOtherProperty == false) || x.UserId == 2
x => (x.someProperty && x.anotherProperty) || (x.userId == 3 && x.userIsActive)
I've created this structure which represents all of the expressions fine my Issue is how can I make this so it's property Model Bindable
public enum FilterCondition
{
Equals,
}
public enum ExpressionCombine
{
And = 0,
Or
}
public interface IFilterResolver<T>
{
Expression<Func<T, bool>> ResolveExpression();
}
public class QueryTreeNode<T> : IFilterResolver<T>
{
public string PropertyName { get; set; }
public FilterCondition FilterCondition { get; set; }
public string Value { get; set; }
public bool isNegated { get; set; }
public Expression<Func<T, bool>> ResolveExpression()
{
return this.BuildSimpleFilter();
}
}
//TODO: rename this class
public class QueryTreeBranch<T> : IFilterResolver<T>
{
public QueryTreeBranch(IFilterResolver<T> left, IFilterResolver<T> right, ExpressionCombine combinor)
{
this.Left = left;
this.Right = right;
this.Combinor = combinor;
}
public IFilterResolver<T> Left { get; set; }
public IFilterResolver<T> Right { get; set; }
public ExpressionCombine Combinor { get; set; }
public Expression<Func<T, bool>> ResolveExpression()
{
var leftExpression = Left.ResolveExpression();
var rightExpression = Right.ResolveExpression();
return leftExpression.Combine(rightExpression, Combinor);
}
}
My left an right members just need to be able to be resolved to an IResolvable, but the model binder only binds to concrete types. I know I can write a custom model binder but I'd prefer to just have a structure that works.
I know I can pass json as a solutions but as a requirement I can't
Is there a way I can refine this structure so that it can still represent all simple expression while being Model Bindable? or is there an easy way I can apply this structure so that it works with the model binder?
EDIT Just in case anyone is wondering, my expression builder has a whitelist of member expressions that it it filters on. The dynamic filtering work I just looking for a way to bind this structure naturally so that my Controller can take in a QueryTreeBranch or take in a structure which accurately represent the same data.
public class FilterController
{
[HttpGet]
[ReadRoute("")]
public Entity[] GetList(QueryTreeBranch<Entity> queryRoot)
{
//queryRoot no bind :/
}
}
Currently the IFilterResolver has 2 implementations which need to be chosen dynamically based on the data passed
I'm looking for a solution closest to out of the box WebApi / MVC framework. Preferable one that does NOT require me to adapt the input to another structure in order generate my expression
At first glance, you can split filtering logic on DTO, which contains an expression tree independent on entity type, and a type-dependent generator of Expression<Func<T, bool>>
. Thus we can avoid making DTO generic and polymorphic, which causes the difficulties.
One can notice, that you used polymorphism (2 implementations) for IFilterResolver<T>
because you want to say, that every node of the filtering tree is either a leaf or a branch (this is also called disjoint union).
Model
Ok, if this certain implementation causes proplems, let's try another one:
public class QueryTreeNode
{
public NodeType Type { get; set; }
public QueryTreeBranch Branch { get; set; }
public QueryTreeLeaf Leaf { get; set; }
}
public enum NodeType
{
Branch, Leaf
}
Of course, you will need validation for such model.
So the node is either a branch or a leaf (I slightly simplified the leaf here):
public class QueryTreeBranch
{
public QueryTreeNode Left { get; set; }
public QueryTreeNode Right { get; set; }
public ExpressionCombine Combinor { get; set; }
}
public class QueryTreeLeaf
{
public string PropertyName { get; set; }
public string Value { get; set; }
}
public enum ExpressionCombine
{
And = 0, Or
}
DTOs above are not so convenient to create from code, so one can use following class to generate those objects:
public static class QueryTreeHelper
{
public static QueryTreeNode Leaf(string property, int value)
{
return new QueryTreeNode
{
Type = NodeType.Leaf,
Leaf = new QueryTreeLeaf
{
PropertyName = property,
Value = value.ToString()
}
};
}
public static QueryTreeNode Branch(QueryTreeNode left, QueryTreeNode right)
{
return new QueryTreeNode
{
Type = NodeType.Branch,
Branch = new QueryTreeBranch
{
Left = left,
Right = right
}
};
}
}
View
There should be no problems with binding such a model (ASP.Net MVC is okay with recursive models, see this question). E.g. following dummy views (place them in \Views\Shared\EditorTemplates
folder).
For branch:
@model WebApplication1.Models.QueryTreeBranch
<h4>Branch</h4>
<div style="border-left-style: dotted">
@{
<div>@Html.EditorFor(x => x.Left)</div>
<div>@Html.EditorFor(x => x.Right)</div>
}
</div>
For leaf:
@model WebApplication1.Models.QueryTreeLeaf
<div>
@{
<div>@Html.LabelFor(x => x.PropertyName)</div>
<div>@Html.EditorFor(x => x.PropertyName)</div>
<div>@Html.LabelFor(x => x.Value)</div>
<div>@Html.EditorFor(x => x.Value)</div>
}
</div>
For node:
@model WebApplication1.Models.QueryTreeNode
<div style="margin-left: 15px">
@{
if (Model.Type == WebApplication1.Models.NodeType.Branch)
{
<div>@Html.EditorFor(x => x.Branch)</div>
}
else
{
<div>@Html.EditorFor(x => x.Leaf)</div>
}
}
</div>
Sample usage:
@using (Html.BeginForm("Post"))
{
<div>@Html.EditorForModel()</div>
}
Controller
Finally, you can implement an expression generator taking filtering DTO and a type of T
, e.g. from string:
public class SomeRepository
{
public TEntity[] GetAllEntities<TEntity>()
{
// Somehow select a collection of entities of given type TEntity
}
public TEntity[] GetEntities<TEntity>(QueryTreeNode queryRoot)
{
return GetAllEntities<TEntity>()
.Where(BuildExpression<TEntity>(queryRoot));
}
Expression<Func<TEntity, bool>> BuildExpression<TEntity>(QueryTreeNode queryRoot)
{
// Expression building logic
}
}
Then you call it from controller:
using static WebApplication1.Models.QueryTreeHelper;
public class FilterController
{
[HttpGet]
[ReadRoute("")]
public Entity[] GetList(QueryTreeNode queryRoot, string entityType)
{
var type = Assembly.GetExecutingAssembly().GetType(entityType);
var entities = someRepository.GetType()
.GetMethod("GetEntities")
.MakeGenericMethod(type)
.Invoke(dbContext, queryRoot);
}
// A sample tree to test the view
[HttpGet]
public ActionResult Sample()
{
return View(
Branch(
Branch(
Leaf("a", 1),
Branch(
Leaf("d", 4),
Leaf("b", 2))),
Leaf("c", 3)));
}
}
UPDATE:
As discussed in comments, it's better to have a single model class:
public class QueryTreeNode
{
// Branch data (should be null for leaf)
public QueryTreeNode LeftBranch { get; set; }
public QueryTreeNode RightBranch { get; set; }
// Leaf data (should be null for branch)
public string PropertyName { get; set; }
public string Value { get; set; }
}
...and a single editor template:
@model WebApplication1.Models.QueryTreeNode
<div style="margin-left: 15px">
@{
if (Model.PropertyName == null)
{
<h4>Branch</h4>
<div style="border-left-style: dotted">
<div>@Html.EditorFor(x => x.LeftBranch)</div>
<div>@Html.EditorFor(x => x.RightBranch)</div>
</div>
}
else
{
<div>
<div>@Html.LabelFor(x => x.PropertyName)</div>
<div>@Html.EditorFor(x => x.PropertyName)</div>
<div>@Html.LabelFor(x => x.Value)</div>
<div>@Html.EditorFor(x => x.Value)</div>
</div>
}
}
</div>
Again this way requires a lot of validation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With