Ben Jones Ben Jones - 11 months ago 74
C# Question

Accessing nested property from a collection in an Expression

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


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]])`

Answer Source

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.PropertyOrField(fieldParameter, "FK_CustomFieldHeader"),
            Expression.PropertyOrField(fieldParameter, "Value"),
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" }
var output = input.Where(predicate).ToList();