Polymorphic Model Bindable Expression Trees Resolver
Asked Answered
P

3

12

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

Pharmaceutics answered 5/9, 2017 at 14:7 Comment(11)
Do you have a fixed set of expressions, query filters, that can be used or is it completely dynamic?Bissonnette
Can you provide a not working example of what you wanted this to do?Amorete
@Bissonnette I'm whitelisting my access normally with member expression I'll update the question in a secondPharmaceutics
@Nkosi eventually this will be a light weight OData, but I'm allowing the client to pass in simple filters and whitelisting my access, so you can dynamically filter entities. I.E { propertyName: "Id", filterCondition: "equals", value: "3" }Pharmaceutics
@Nkosi I need to support simple binary expression and or etcPharmaceutics
@johnny5, It feels very over engineered way to re-invent the wheel. if using EF why not just use DbContext.SqlQuery which would allow you to send (someProperty = true AND someOtherProperty = false) OR UserId = 2Caeoma
@Nkosi, this an abstract crud framework, additional expressions could be applied in the context base for access permissions on the DbSet, Id prefer not to have to combine those with this raw SQL.Pharmaceutics
So you are looking for an OData type of solution?Bissonnette
@Bissonnette No I know about OData, I'm not using it because I will have to change too much of my architecture. I'm looking for the most efficient and readable way to Serialize that model in the model binder, Or a way to rerepresent the data so that it model binds easierPharmaceutics
Can you share the Entity object definition?Bissonnette
@Bissonnette the Entity object definition is made up in actuality I'm using this with a generic function I just displayed it this way for simplicity, it should have no berring in the bindingPharmaceutics
R
6

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.

Ramon answered 8/9, 2017 at 17:25 Comment(4)
+1 this is definitely valid and should work, I just don't like that 1, you have an entity which defines 2 object but only 1 is valid at a time, and now I have to use 3 models, when there probably is a way we can do it with 1Pharmaceutics
@johnny5 I agree. Unfortunately, C# seems to have no pretty way to define disjoint union - that's the main problem here. As I wrote, my implementation is a tradeoff to make the model binder happy.Ramon
@johnny5 I think it's possible to have only 1 model object here, see update in the answer.Ramon
I was hoping to magically find a cleaner way to do this but this looks like the only way 'thanksPharmaceutics
B
0

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.

Bissonnette answered 8/9, 2017 at 14:51 Comment(4)
The trouble isn't the generics it's binding an interface with 2 implementations that should be resolved based on the dataPharmaceutics
What is the second implementation?Bissonnette
Isn't that one implementation that is the same but rather used on two properties?Bissonnette
IFilterResolver can be a QueryTreeNode or QueryTreeBranch, its class should be slected based off of the data providedPharmaceutics
P
0

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...
}
Pharmaceutics answered 13/9, 2017 at 14:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.