Accessing nested property from a collection in an Expression
Asked Answered
I

1

6

Ok to set the context a little I am building up a dynamic Linq search clause using an expression tree using this class

public class HomeTableInvoice {
    public int Sys_InvoiceID { get; set; }
    public bool Turnover { get; set; }
    public int FK_StatusID { get; set; }
    public string InvoiceNumber { get; set; }
    public DateTime InvoiceDate { get; set; }
    public string DocType { get; set; }

    public ICollection<InvoiceCustomFields> InvoiceCustomFields { get; set; }
}

I have managed to get everything working and the parameter I use is the HomeTableInvoice and I can get an of the properties for the expression using

var parameter = Expression.Parameter(typeof(HomeTableInvoice), "invoice");
prop = Expression.Property(param, filter.SysName);

with filter.SysName being the field I wish to filter.

The issue comes when trying to build an expression for the ICollection at the bottom. the class InvoiceCustomFields contains

public class InvoiceCustomFields : CustomFieldsBase {
        public int? FK_SysInvoiceID { get; set; }    
        public string FK_CustomFieldHeader { get; set; }    
        public string Value { get; set; }    
    }

I'm trying to access the string for FkCustomFieldHeader and the string for value so when i query for example the condition can look like

where InvoiceNumber == 34 AndAlso (Invoice.InvoiceCustomField.FK_CustomFieldHeader == "Test" && Invoice.InvoiceCustomField.FK_CustomFieldHeader.Value == 42)

I've tried using

prop = Expression.PropertyOrField(Expression.PropertyOrField(param, "InvoiceCustomFields"), "FK_CustomFieldHeader");

but it throws this error

FK_CustomFieldHeader' is not a member of type 'System.Collections.Generic.ICollection`1[APData.Audit.Entityframework.Entities.InvoiceCustomFields]'

any help is much appreciated

--Edit--

After trying the answer by Ivan I get the error

No generic method 'Any' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments

I then tried this

prop = Expression.PropertyOrField(parameter, "InvoiceCustomFields");

   var queryableType = typeof(Enumerable);
   var whereMethod = queryableType.GetMethods()
      .First(m => {
         var parameters = m.GetParameters().ToList();                               
             return m.Name == "Any" && m.IsGenericMethodDefinition &&
                                                 parameters.Count == 2;
                       });

   MethodInfo methoInfo = whereMethod.MakeGenericMethod(prop.Type);
   var x = Expression.Call(methoInfo, Expression.PropertyOrField(parameter, "InvoiceCustomFields"), whereQuery);

And this then throws

Expression of type `'System.Collections.Generic.ICollection`1[InvoiceCustomFields]' cannot be used for parameter of type 'System.Linq.IQueryable`1[System.Collections.Generic.ICollection`1[InvoiceCustomFields]]' of method 'Boolean Any[ICollection`1](System.Linq.IQueryable`1[System.Collections.Generic.ICollection`1[InvoiceCustomFields]], System.Linq.Expressions.Expression`1[System.Func`2[System.Collections.Generic.ICollection`1[.InvoiceCustomFields],System.Boolean]])`
Indies answered 24/8, 2016 at 8:36 Comment(0)
W
7

Let see how it looks if it wasn't dynamic. The following:

Expression<Func<HomeTableInvoice, bool>> predicate = invoice =>
    invoice.InvoiceCustomField.FK_CustomFieldHeader == "Test" &&
    invoice.InvoiceCustomField.Value == "42";

is not a valid expression.

What you actually need to do is something like this:

Expression<Func<HomeTableInvoice, bool>> predicate = invoice =>
    invoice.InvoiceCustomFields.Any(field => 
        field.InvoiceCustomField.FK_CustomFieldHeader == "Test" &&
        field.InvoiceCustomField.Value == "42");

And here is how you can build that dynamically (hope you can adjust it for your needs replacing the hardcoded parts with your variables):

var parameter = Expression.Parameter(typeof(HomeTableInvoice), "invoice");

var fieldParameter = Expression.Parameter(typeof(InvoiceCustomFields), "field");
var anyPredicate = Expression.Lambda(
    Expression.AndAlso(
        Expression.Equal(
            Expression.PropertyOrField(fieldParameter, "FK_CustomFieldHeader"),
            Expression.Constant("Test")),
        Expression.Equal(
            Expression.PropertyOrField(fieldParameter, "Value"),
            Expression.Constant("42"))),
    fieldParameter);
var fieldCondition = Expression.Call(
    typeof(Enumerable), "Any", new[] { fieldParameter.Type },
    Expression.PropertyOrField(parameter, "InvoiceCustomFields"), anyPredicate);

// You can use the fieldCondition in your combinator,
// the following is just to complete the example
var predicate = Expression.Lambda<Func<HomeTableInvoice, bool>>(fieldCondition, parameter);

// Test
var input = new List<HomeTableInvoice>
{
    new HomeTableInvoice
    {
        InvoiceNumber = "1",
        InvoiceCustomFields = new List<InvoiceCustomFields>
        {
            new InvoiceCustomFields { FK_CustomFieldHeader = "Test", Value = "42" }
        }
    },
}.AsQueryable();
var output = input.Where(predicate).ToList();
Willock answered 24/8, 2016 at 9:31 Comment(4)
I'm getting the error No generic method 'Any' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments.Indies
That's strange because I've tested it and it's working w/o problem (see the updated answer with the test code at the end).Willock
Hmm, there is no way to pass the property type (ICollection<InvoiceCustomFields>) as generic argument to Any<T> because Any expects the type of the collection element (InvoiceCustomFields). Again, just for the record, the above snippet works and is the correct way to do what you asked. Anyone can copy/paste it in a console app (as I did) and see.Willock
Scratch that :) it did work i had missed the Expression.Lambda bit... my bad, thanks for the helpIndies

© 2022 - 2024 — McMap. All rights reserved.