0

I have taken my first dive into expressions as a way to reduce code duplication.

I have created 2 expression functions to isolate the work for 2 separate entities.

  • Site
  • SiteServer
public class CMSite
{
   public int ID {get;set;}
   public string Name {get;set;}
   public ICollection<CMSiteServer> SiteServers { get; set; }
}

public class CMSiteServer
{
   public int ID {get;set;}
   public string Name {get;set;}
   public int SiteID {get;set;}
   [ForeignKey("CMSite")]
   public decimal SiteID { get; set; }
   public CMSite Site {get;set;}
}

The relationship is 1 site = multiple site servers

Each site and server has a series of properties and i've created various filter objects to permit searching these enties.

Use case:

  1. Search sites containing servers with property x
  2. search servers containing sites with property x

My code contains the following methods:

public static Expression<Func<CMSiteServer, bool>> GetServerExpression(CMSiteServerFilter filter, bool Active) {
   ...
}

public static Expression<Func<CMSite, bool>> GetSiteExpression(CMSiteFilter filter, bool Active) {
   ...
}

I can solve use case 1 with this method:

    public static IQueryable<CMSite> ApplyQueryFilterServer(this IQueryable<CMSite> qry, CMSiteServerFilter filter, bool Active)
    {
        if (filter == null)
            return qry;


        var exp = GetServerExpression(filter, Active);
        qry = qry.Where((s) => s.CMSiteServers.AsQueryable().Any(exp));

        return qry;
    }

However, i can't find an equivalent for use case 2.

    public static IQueryable<CMSite> ApplyQueryFilterSite(this IQueryable<CMSiteServer> qry, CMSiteFilter filter, bool Active)
    {
        if (filter == null)
            return qry;


        var exp = GetServerExpression(filter, Active);

        // this is the piece that won't work:
        qry = qry.Where((s) => s.CMSite.AsQueryable().Any(exp));

        return qry;
    }

because Server.CMSite is singular the "AsQueryable" won't work

i'm trying to avoid defining all my filter criteria once for each IQueryable source element.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
Justin
  • 1,303
  • 15
  • 30
  • Why are you using `AsQueryable`? – NetMage Feb 28 '23 at 22:08
  • In case 2, you have `qry` as a collection of `CMSiteServer` so `qry.Where()` will also be a collection of `CMSiteServer`, so how do you expect to get a collection of `CMSite` from that? Does it matter if your answer has duplicate `CMSite`s? – NetMage Feb 28 '23 at 22:10
  • C# is a language of types - you haven't shown any type definitions. What is the definition of `s.CMSite`? Is it a collection or a single site? – NetMage Feb 28 '23 at 22:11
  • I have added example class definitions. In use case 1, in order to use "Any" i needed to use the AsQueryable, this may not have been strictly necessary. In use case 2 i provided as an example, but ultimately i could not use any "LINQ" methods to apply a where clause because it is a singular entity. ultimately i need a way to apply my expression criteria to evaluate whether the server should be returned, based on the search properties of the site (singular parent). – Justin Mar 01 '23 at 03:30
  • I just re-confirmed, the AsQueryable().Any() in use case 1 is required. – Justin Mar 01 '23 at 04:22
  • If `s.CMSite` is singular, you could just do the `Expression` equivalent of `exp.Invoke(s.CMSite)` (use `ReplacingExpressionVisitor.Replace` to expand the `exp` by replacing the parameter with `s.CMSite` and then making a new lambda for the `Where`). – NetMage Mar 01 '23 at 17:05
  • Are you sure case 1 actually runs, as opposed to compiles? Using `AsQueryable` is suspicious in that context. Are you using LINQKit or something similar to expand the `exp` variable in the query? – NetMage Mar 01 '23 at 17:06
  • yes, use case 1 actually runs. the application will not compile without the .AsQueryable and it behaves exactly as expected. No i am not using anything to expand the exp variable. I did try using exp.Invoke however this results in an exception at runtime. "LINQ to Entities does not recognize the method 'Boolean Invoke(LNOSCMPortal.Common.Models.Tables.CMSite)' method, and this method cannot be translated into a store expression.","StackTrace":" at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.DefaultTranslator.Translate – Justin Mar 01 '23 at 20:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252236/discussion-between-justin-and-netmage). – Justin Mar 01 '23 at 20:31

1 Answers1

0

After a lot of digging i found the solution to my problem through this article:

Define part of an Expression as a variable in c#

I added a new helper function:

public static Expression<Func<CMSiteServer, bool>> GetSiteExpressionForServer(CMSiteFilter filter, bool Active)
{
     var exp = GetSiteExpression(filter, Active);
     return exp.ApplyTo((CMSiteServer x) => x.CMSite);
}

Combined with the helper functions from the article:

public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector)
{
     var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body);
     var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters);
     return lambda;
}

public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector)
{
     return innerSelector.Bind(source);
}

In addition, i also solved a related use case of handling multiple filter conditions IEnumerable where each condition gets "or" together e.g. where @{} is a filter object @{Name == 'A'}, @{Name == 'B'}, @{ID == 4}

should compose the following SQL criteria: (Name = 'A' or Name = 'B' or ID = 4)

// function convert a list of filters into a new expression that combines them together
public static Expression<Func<CMSite, bool>> GetServerExpression(IEnumerable<CMSiteServerFilter> filters, bool Active)
    {
        // make sure we got some filters
        if (filters == null)
            return (s => true);

        // compile the list of expressions, one for each filter
        var expressions = filters.Select((f) => GetServerExpression(f, Active)).ToList();

        // if no expressions, we are done
        if (expressions.Count == 0)
            return (s => true);

        //set our initial condition, since we are using "OR" we want to default to false
        Expression<Func<CMSiteServer, bool>> exp = (s => false);
        // merge the multiple expressions into a new single expression
        foreach (var e in expressions)
            exp = exp.Or(e);

        // apply the combined expression to our list of servers
        return (s => s.CMSiteServers.AsQueryable().Where(exp).Any());
    }

the "Or" extension above came from this article: Combining two expressions (Expression<Func<T, bool>>)

public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b)
{

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
}

//And the utility class to substitute the parameters in a lambda:
internal class SubstExpressionVisitor : ExpressionVisitor
{
    public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

    protected override Expression VisitParameter(ParameterExpression node)
    {
        Expression newValue;
        if (subst.TryGetValue(node, out newValue))
        {
            return newValue;
        }
        return node;
    }
}
Justin
  • 1,303
  • 15
  • 30