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.
You should use a custom data binder for your generic class.
See this previous question that had a similar need in a previous version using web forms and the Microsoft documentation.
You're other option is to pass a serialized version of the class.
I've created an interface binder that works off of the standard ComplexTypeModelBinder
//Redefine IModelBinder so that when the ModelBinderProvider Casts it to an
//IModelBinder it uses our new BindModelAsync
public class InterfaceBinder : ComplexTypeModelBinder, IModelBinder
{
protected TypeResolverOptions _options;
//protected Dictionary<Type, ModelMetadata> _modelMetadataMap;
protected IDictionary<ModelMetadata, IModelBinder> _propertyMap;
protected ModelBinderProviderContext _binderProviderContext;
protected InterfaceBinder(TypeResolverOptions options, ModelBinderProviderContext binderProviderContext, IDictionary<ModelMetadata, IModelBinder> propertyMap) : base(propertyMap)
{
this._options = options;
//this._modelMetadataMap = modelMetadataMap;
this._propertyMap = propertyMap;
this._binderProviderContext = binderProviderContext;
}
public InterfaceBinder(TypeResolverOptions options, ModelBinderProviderContext binderProviderContext) :
this(options, binderProviderContext, new Dictionary<ModelMetadata, IModelBinder>())
{
}
public new Task BindModelAsync(ModelBindingContext bindingContext)
{
var propertyNames = bindingContext.HttpContext.Request.Query
.Select(x => x.Key.Trim());
var modelName = bindingContext.ModelName;
if (false == string.IsNullOrEmpty(modelName))
{
modelName = modelName + ".";
propertyNames = propertyNames
.Where(x => x.StartsWith(modelName, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Remove(0, modelName.Length));
}
//split always returns original object if empty
propertyNames = propertyNames.Select(p => p.Split('.')[0]);
var type = ResolveTypeFromCommonProperties(propertyNames, bindingContext.ModelType);
ModelBindingResult result;
ModelStateDictionary modelState;
object model;
using (var scope = CreateNestedBindingScope(bindingContext, type))
{
base.BindModelAsync(bindingContext);
result = bindingContext.Result;
modelState = bindingContext.ModelState;
model = bindingContext.Model;
}
bindingContext.ModelState = modelState;
bindingContext.Result = result;
bindingContext.Model = model;
return Task.FromResult(0);
}
protected override object CreateModel(ModelBindingContext bindingContext)
{
return Activator.CreateInstance(bindingContext.ModelType);
}
protected NestedScope CreateNestedBindingScope(ModelBindingContext bindingContext, Type type)
{
var modelMetadata = this._binderProviderContext.MetadataProvider.GetMetadataForType(type);
//TODO: don't create this everytime this should be cached
this._propertyMap.Clear();
for (var i = 0; i < modelMetadata.Properties.Count; i++)
{
var property = modelMetadata.Properties[i];
var binder = this._binderProviderContext.CreateBinder(property);
this._propertyMap.Add(property, binder);
}
return bindingContext.EnterNestedScope(modelMetadata, bindingContext.ModelName, bindingContext.ModelName, null);
}
protected Type ResolveTypeFromCommonProperties(IEnumerable<string> propertyNames, Type interfaceType)
{
var types = this.ConcreteTypesFromInterface(interfaceType);
//Find the type with the most matching properties, with the least unassigned properties
var expectedType = types.OrderByDescending(x => x.GetProperties().Select(p => p.Name).Intersect(propertyNames).Count())
.ThenBy(x => x.GetProperties().Length).FirstOrDefault();
expectedType = interfaceType.CopyGenericParameters(expectedType);
if (null == expectedType)
{
throw new Exception("No suitable type found for models");
}
return expectedType;
}
public List<Type> ConcreteTypesFromInterface(Type interfaceType)
{
var interfaceTypeInfo = interfaceType.GetTypeInfo();
if (interfaceTypeInfo.IsGenericType && (false == interfaceTypeInfo.IsGenericTypeDefinition))
{
interfaceType = interfaceTypeInfo.GetGenericTypeDefinition();
}
this._options.TypeResolverMap.TryGetValue(interfaceType, out var types);
return types ?? new List<Type>();
}
}
Then you need a Model Binding Provider:
public class InterfaceBinderProvider : IModelBinderProvider
{
TypeResolverOptions _options;
public InterfaceBinderProvider(TypeResolverOptions options)
{
this._options = options;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (!context.Metadata.IsCollectionType &&
(context.Metadata.ModelType.GetTypeInfo().IsInterface ||
context.Metadata.ModelType.GetTypeInfo().IsAbstract) &&
(context.BindingInfo.BindingSource == null ||
!context.BindingInfo.BindingSource
.CanAcceptDataFrom(BindingSource.Services)))
{
return new InterfaceBinder(this._options, context);
}
return null;
}
}
and then you inject the binder into your services:
var interfaceBinderOptions = new TypeResolverOptions();
interfaceBinderOptions.TypeResolverMap.Add(typeof(IFilterResolver<>),
new List<Type> { typeof(QueryTreeNode<>), typeof(QueryTreeBranch<>) });
var interfaceProvider = new InterfaceBinderProvider(interfaceBinderOptions);
services.AddSingleton(typeof(TypeResolverOptions), interfaceBinderOptions);
services.AddMvc(config => {
config.ModelBinderProviders.Insert(0, interfaceProvider);
});
Then you have your controllers setup like so
public MessageDTO Get(IFilterResolver<Message> foo)
{
//now you can resolve expression etc...
}