简体   繁体   中英

Using an intermediate entity in LINQ to Entities leads to System.NotSupportedException

My entity model is made primarily of 6 entities that join among themselves based on 2 attributes.

I have to build 182 LINQ queries based on old plaintext SQL queries. Those queries have some parts in common, so that in order to avoid repeats I have built a little framework to build the queries on top of building blocks. I will show examples right away.

Since all queries are made of a join between a combination of the 6 entities (starting from root entity SectionA ) I have built, for my convenience, a wrapper class JoinOfSections that wraps all those sections.

On that class I can perform simple LINQ evaluations that are common to all 182 queries.

For example

    public override IQueryable<QueryRow> Run(Models.auitool2014Entities dataContext, int aUserId, DateTime? dataInizioControllo, DateTime? dataFineControllo, string[] a52Exclude, bool codifiche2014)
        string now = DateTime.Now.ToString("yyyyMMdd");

        var sj = dataContext.sezione_a.SelectMany(
            a => dataContext.sezione_d.Where(d => a.A03 == d.A03 && a.utente == d.utente).DefaultIfEmpty(),
            (a, d) => new SezioneJoin { A = a, D = d });

        sj = CommonFiltering(sj, aUserId, dataInizioControllo, dataFineControllo, a52Exclude);

        return (from SezioneJoin ssj in sj
                let a = ssj.A
                let d = ssj.D

                where
                 a.utente == aUserId &&
                 (
                 String.IsNullOrEmpty(a.A21) || String.IsNullOrEmpty(a.A51) ||
                 a.A51.CompareTo(a.A21) < 0 ||
                 a.A21.CompareTo(now) > 0 ||
                 a.A51.CompareTo(now) > 0 ||
                 a.A21.CompareTo("19000101") < 0 ||
                 a.A51.CompareTo("19000101") < 0
                 )
                select ssj).Select(Select());
    }

    protected virtual IQueryable<SezioneJoin> CommonFiltering(IQueryable<SezioneJoin> sj, int aUserId, DateTime? dataInizioControllo, DateTime? dataFineControllo, string[] a52Exclude)
    {
        sj = sj.Where(x => x.A.utente == aUserId);
        if (dataInizioControllo != null)
        {
            string dataInzio = dataInizioControllo.Value.ToString("yyyyMMdd");
            sj = sj.Where(x => x.A.A21.CompareTo(dataInzio) >= 0);
        }
        if (dataFineControllo != null)
        {
            string dataFine = dataFineControllo.Value.ToString("yyyyMMdd");
            sj = sj.Where(x => x.A.A21.CompareTo(dataFine) <= 0);
        }
        if (a52Exclude != null)
            sj = sj.Where(x => !a52Exclude.Contains(x.A.A52));

        return sj.Take(Parameters.ROW_LIMIT);
    }

Select() method is a simplification of a common pattern. Since the result set must be flattened in order for older components to process it I have invented another adapter layer

[Serializable]
public class QueryRow
{
    public string A01 { get; set; }

    public string A01a { get; set; }

    public string A01b { get; set; }

    public string A02 { get; set; }

    public string A03 { get; set; }

    public string A11 { get; set; }

    public string A12 { get; set; }

    // Dozens of string members
}

In order not to copy&paste...

    protected virtual Expression<Func<SezioneJoin, QueryRow>> Select()
    {
        return sj => new QueryRow
        {
            A01 = sj.A.A01,
            A01a = sj.A.A01a,
            A01b = sj.A.A01b,
            A02 = sj.A.A02,
            A03 = sj.A.A03,
            A11 = sj.A.A11,
            A12 = sj.A.A12,
            A12a = sj.A.A12a,
            A12b = sj.A.A12b,
            A12c = sj.A.A12c,
            A21 = sj.A.A21,
            A22 = sj.A.A22,
            A23 = sj.A.A23,
            A24 = sj.A.A24,
            A25 = sj.A.A25,
            A31 = sj.A.A31,
            A31a = sj.A.A31a,
            A31b = sj.A.A31b,
            A32 = sj.A.A32,
        }

SezioneJoin class represents a row in the dataset that is a combination of JOIN s between a number of entities, is as follows. It is designed in order for any query to instantiate its custom JOIN (eg A inner D, A left D left E, A inner D left H)

public class SezioneJoin
{

    public SezioneA A { get; set; }

    public SezioneD D { get; set; }

    public SezioneE E { get; set; }

    public SezioneF F { get; set; }

    public SezioneG G { get; set; }

    public SezioneH H { get; set; }

}

Basically all queries require that the dataset is filtered on the current user ID and optional check dates, plus they all allow a maximum number of results.

I paid my attempt to generalize the concept with NotSupportedException (exception message translated by me)

Unable to cast type 'DiagnosticoSite.Data.Query.SezioneJoin' into 'DiagnosticoSite.Data.Query.SezioneJoin'. LINQ to Entities supports only Enum or primitive EDM data types

The problem may lie in the (a, d) => new SezioneJoin { A = a, D = d } line: if I select an anonymous type the LINQ query works just flawlessly, but then I cannot pass the query object to the protected method that decorates it with additional common checks.

Being them 182 queries it is important for me to find a way to add common checks to all queries that is not copy&paste.

I would like to know how I can manipulate a LINQ to Entities query using a "buffer" or "intermediate" entity that is not in the data context so that the query itself, being complex, may be passed as a parameter to a decorator method.

The error occurs both on enumerating and on calling ToString() method of the IQueryable returned by Run . I needed the ToString to extract the query issued to DB

EF presumably doesn't know what a SezioneJoin is.

You could define such a types as an entity, and maybe it would work. However, if your CommonFiltering is realistic, that wouldn't be necessary. Consider:

var q = CommonFiltering(dataContext.sezione_a, aUserId, dataInizioControllo, dataFineControllo, a52Exclude)
  .Join(
        dataContext.sezione_d,
        a => new { A03 = a.A03, User = a.utente },
        d => new { A03 = d.A03, User = d.utente },
        (a, d) => new SezioneJoin { A = a, D = d }
      )
    .Where(x =>
      ...
    ).Take(Parameters.ROW_LIMIT);

protected virtual IQueryable<AType> CommonFiltering(IQueryable<SezioneA> sj, int aUserId, DateTime? dataInizioControllo, DateTime? dataFineControllo, string[] a52Exclude)
{
  sj = sj.Where(x => x.utente == aUserId);
  if (dataInizioControllo != null)
  {
    string dataInzio = dataInizioControllo.Value.ToString("yyyyMMdd");
    sj = sj.Where(x => x.A21.CompareTo(dataInzio) >= 0);
  }
  if (dataFineControllo != null)
  {
    string dataFine = dataFineControllo.Value.ToString("yyyyMMdd");
    sj = sj.Where(x => x.A21.CompareTo(dataFine) <= 0);
  }
  if (a52Exclude != null)
    sj = sj.Where(x => !a52Exclude.Contains(x.A52));

  return sj;
}

Really, you only need to worry about passing the result of the join rather than the components to it, if a point of the filtering cares about some quality of both A and D at the same step. Here your filtering cares only about A and that's a defined type.

If you really need to deal with something like that though, you could build up the expression.

To simplify let's just consider a version with the signature IQueryable<T> Filter<T>(IQueryable<T> source, bool makeA21Match) . If makeA21Match is true then we add Where(sj => sj.A.A21 == sj.D.A21) to the query, and otherwise we don't:

private static IQueryable<T> Filter<T>(IQueryable<T> source, bool makeA21Match)
{
  if(makeA21Match)
  {
    var getA = typeof(T).GetProperty("A"); // .A
    var getD = typeof(T).GetProperty("D"); // .D
    var getAA21 = typeof(SezioneA).GetProperty("A21"); // a.A21 for some A.
    var getDA21 = typeof(SezioneD).GetProperty("A21"); // d.A21 for some D.
    var parExp = Expression.Parameter(typeof(T)); // sj.
    var getAExp = Expression.Property(parExp, getA); // sj.A
    var getDExp = Expression.Property(parExp, getD); // sj.D
    var getAA21Exp = Expression.Property(getAExp, getAA21); // sj.A.A21
    var getDA21Exp = Expression.Property(getDExp, getDA21); // sj.D.A21
    var eqExp = Expression.Equal(getAA21Exp, getDA21Exp); // sj.A.A21 == sj.D.A21
    var λExp = Expression.Lambda<Func<T, bool>>(eqExp, parExp); // sj => sj.A.A21 == sj.D.A21
    source = source.Where(λExp);
  }
  return source;
}

This is a lot more convoluted than just having a lambda expression in the C# code, and lacks compile-time checking of types, but it does mean we can apply the expression to a queryable of anonymous type, and performance should be comparable in such a case.

A balanced approach would be to first filter sezione_a and sezione_d as much as possible, since that can be done more easily, and then have a method that only deals with the more complicated hand-coding of expression-building for those cases that absolutely need it. You can even cache the Expression<Func<T, bool>> produced if necessary.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM