2009-09-14 7 views
5

EDIT: Probemos esto de nuevo. Esta vez he utilizado la base de datos de ejemplo de AdventureWorks para que todos puedan seguir el juego. Esto eliminará cualquier cosa loca que haya hecho en mi propia base de datos. Aquí hay un nuevo ejemplo que demuestra lo que funciona y lo que espero que funcione (pero no funciona). ¿Alguien puede explicar por qué no funciona o sugerir una forma diferente de lograr mi objetivo (refactorizar la expresión común para que pueda ser reutilizada en otro lugar)?EntitySet <T>. Where (myPredicate) arroja NotSupportedException

using (AdventureWorksDataContext db = new AdventureWorksDataContext()) 
{ 
    // For simplicity's sake we'll just grab the first result. 
    // The result should have the name of the SubCategory and an array of Products with ListPrice greater than zero. 
    var result = db.ProductSubcategories.Select(subCategory => new 
    { 
     Name = subCategory.Name, 
     ProductArray = subCategory.Products.Where(product => product.ListPrice > 0).ToArray() 
    }).First(); 
    Console.WriteLine("There are {0} products in SubCategory {1} with ListPrice > 0.", result.ProductArray.Length, result.Name); 
    // Output should say: There are 3 products in SubCategory Bib-Shorts with ListPrice > 0. 

    // This won't work. I want to pull the expression out so that I can reuse it in several other places. 
    Expression<Func<Product, bool>> expression = product => product.ListPrice > 0; 
    result = db.ProductSubcategories.Select(subCategory => new 
    { 
     Name = subCategory.Name, 
     ProductArray = subCategory.Products.Where(expression).ToArray() // This won't compile because Products is an EntitySet<Product> and that doesn't have an overload of Where that accepts an Expression. 
    }).First(); 
    Console.WriteLine("There are {0} products in SubCategory {1} with ListPrice > 0.", result.ProductArray.Length, result.Name); 
} 

</Edit>

La siguiente LINQ a SQL funciona bien:

var result = from subAccount in db.SubAccounts 
      select new ServiceTicket 
      { 
       MaintenancePlans = subAccount.Maintenances.Where(plan => plan.CancelDate == null && plan.UpgradeDate == null).Select(plan => plan.ToString()).ToArray() 
       // Set other properties... 
      }; 

Sin embargo, quiero romper el predicado pasado al Where ya que se utiliza en todo el código. Pero si lo intento y pasar un predicado definido en el Where falla, tales como:

Func<DatabaseAccess.Maintenance, bool> activePlanPredicate = plan => plan.CancelDate == null && plan.UpgradeDate == null; 
var result = from subAccount in db.SubAccounts 
      select new ServiceTicket 
      { 
       MaintenancePlans = subAccount.Maintenances.Where(activePlanPredicate).Select(plan => plan.ToString()).ToArray() 
       // Set other properties... 
      }; 

Esto no tiene sentido para mí. ¿Alguien puede explicar lo que está pasando? Maintenances es del tipo EntitySet<DatabaseAccess.Maintenance>. El error que consigo es:

System.NotSupportedException: Unsupported overload used for query operator 'Where'..

EDITAR: Para los interesados, esto es lo reflector tiene para el primer ejemplo (de trabajo) con la optimización establecido en .NET 2.0:

using (BugsDatabaseDataContext db = new BugsDatabaseDataContext()) 
{ 
    ParameterExpression CS$0$0001; 
    ParameterExpression CS$0$0006; 
    ParameterExpression CS$0$0010; 
    return db.SubAccounts.Select<SubAccount, ServiceTicket>(Expression.Lambda<Func<SubAccount, ServiceTicket>>(
     Expression.MemberInit(
      Expression.New(
       (ConstructorInfo) methodof(ServiceTicket..ctor), 
       new Expression[0]), 
       new MemberBinding[] 
       { 
        Expression.Bind(
         (MethodInfo) methodof(ServiceTicket.set_MaintenancePlans), 
         Expression.Call(
          null, 
          (MethodInfo) methodof(Enumerable.ToArray), 
          new Expression[] 
          { 
           Expression.Call(
            null, 
            (MethodInfo) methodof(Enumerable.Select), 
            new Expression[] 
            { 
             Expression.Call(
              null, 
              (MethodInfo) methodof(Enumerable.Where), 
              new Expression[] 
              { 
               Expression.Property(CS$0$0001 = Expression.Parameter(typeof(SubAccount), "subAccount"), (MethodInfo) methodof(SubAccount.get_Maintenances)), 
               Expression.Lambda<Func<Maintenance, bool>>(
                Expression.AndAlso(
                 Expression.Equal(
                  Expression.Property(CS$0$0006 = Expression.Parameter(typeof(Maintenance), "plan"), (MethodInfo) methodof(Maintenance.get_CancelDate)), 
                  Expression.Convert(Expression.Constant(null, typeof(DateTime?)), typeof(DateTime?)), false, (MethodInfo) methodof(DateTime.op_Equality) 
                 ), 
                 Expression.Equal(
                  Expression.Property(CS$0$0006, (MethodInfo) methodof(Maintenance.get_UpgradeDate)), 
                  Expression.Convert(Expression.Constant(null, typeof(DateTime?)), typeof(DateTime?)), false, (MethodInfo) methodof(DateTime.op_Equality) 
                 ) 
                ), 
                new ParameterExpression[] { CS$0$0006 } 
               ) 
              } 
             ), 
             Expression.Lambda<Func<Maintenance, string>>(
              Expression.Call(
               CS$0$0010 = Expression.Parameter(typeof(Maintenance), "plan"), 
               (MethodInfo) methodof(object.ToString), 
               new Expression[0] 
              ), 
              new ParameterExpression[] { CS$0$0010 } 
             ) 
            } 
           ) 
          } 
         ) 
        ) 
       } 
      ), 
      new ParameterExpression[] { CS$0$0001 } 
     ) 
    ).ToList<ServiceTicket>(); 
} 

EDITAR : La salida Reflector para el segundo ejemplo (usando un predicado) es en su mayoría similar. La mayor diferencia es que, en la llamada al Enumerable.Where, en lugar de pasar un Expression.Lambda pasa Expression.Constant(activePlanPredicate).

+0

Compilar utilizando la primera parte y ver cómo se genera el código utilizando el reflector. Eso ayudará a todos a entender el tipo real del predicado. – shahkalpesh

+0

Salida del reflector agregada arriba. Avísame si no entendí bien. – Ecyrb

Respuesta

2

No entiendo completamente las entrañas de Linq to Entities, pero hay un kit de herramientas de código abierto (utilizable en software propietario) específicamente diseñado para ayudar a resolver este problema, llamado LinqKit, vinculado a este artículo relacionado con O'Reilly :

http://www.albahari.com/nutshell/predicatebuilder.aspx

Dado que no entiendo plenamente las agallas, sólo voy a citarlos:

Entity Framework's query processing pipeline cannot handle invocation expressions, which is why you need to call AsExpandable on the first object in the query. By calling AsExpandable, you activate LINQKit's expression visitor class which substitutes invocation expressions with simpler constructs that Entity Framework can understand.

Aquí es a direct link to LinqKit.

Y aquí es el tipo de código que permite a este proyecto:

using LinqKit; 

// ... 

Expression<Func<Product, bool>> expression = product => product.ListPrice > 0; 

var result = db.ProductSubcategories 
    .AsExpandable() // This is the magic that makes it all work 
    .Select(
     subCategory => new 
     { 
      Name = subCategory.Name, 
      ProductArray = subCategory.Products 
       // Products isn't IQueryable, so we must call expression.Compile 
       .Where(expression.Compile()) 
     }) 
    .First(); 

Console.WriteLine("There are {0} products in SubCategory {1} with ListPrice > 0." 
    , result.ProductArray.Count() 
    , result.Name 
    ); 

El resultado es:

There are 3 products in SubCategory Bib-Shorts with ListPrice > 0.

Yay, no es una excepción, y podemos extraer el predicado!

+0

Tenga en cuenta que no estoy seguro de que 'ToArray() 'de su ejemplo alguna vez funcione. No pude hacer que funcionara, incluso sin intentar extraer el predicado ... –

0

me gustaría refactorizar el original como esto

private bool IsYourPredicateSatisfied(Maintenance plan) 
{ 
    return plan.CancelDate == null && plan.UpgradeDate == null; 
} 

Luego, su cláusula WHERE es Where(m => IsYourPredicateSatisfied(m))

+0

Parece que su objetivo es poder pasar un predicado personalizado. Estás haciendo lo opuesto aquí. –

+0

Lo intenté, pero resulta en el mismo error. – Ecyrb

+0

Er, lo siento, leí mal la primera vez. Todavía no funciona, pero el error que obtengo es: System.NotSupportedException: El método 'Boolean Test (DatabaseAccess.Maintenance)' no tiene traducción soportada a SQL ... – Ecyrb

0

Prueba esto:

Expression<Func<DatabaseAccess.Maintenance, bool>> activePlanPredicate = plan => plan.CancelDate == null && plan.UpgradeDate == null; 
var result = from subAccount in db.SubAccounts 
     select new ServiceTicket 
     { 
      MaintenancePlans = subAccount.Maintenances.Where(activePlanPredicate).Select(plan => plan.ToString()).ToArray() 
      // Set other properties... 
     }; 

no tengo VisualStudio frente a mí, por lo que puede requerir algunos ajustes. El problema al que se está enfrentando es que desea acceder a la extensión IQueryable de Where, pero solo tener un Func<T,bool> le brinda la extensión IEnumerable.

+0

Eso es lo que habría pensado, pero 'EntitySet ' implementa 'IEnumerable ', no 'IQueryable '. Por eso, el uso de una expresión produce el siguiente error: Los argumentos de tipo para el método 'System.Linq.Enumerable.Where (System.Collections.Generic.IEnumerable , System.Func )' no pueden inferirse del uso. Intente especificar los argumentos de tipo explícitamente. – Ecyrb

+0

@Ecyrb: Pruebe 'EntitySet .ToQueryable()' – leppie

Cuestiones relacionadas