Expresión de LINQ para devolver el valor de la propiedad?

Intento crear una función genérica que me ayude a seleccionar miles de registros usando LINQ to SQL desde una lista local. SQL Server (al menos en 2005) limita las consultas a 2100 parámetros y me gustaría seleccionar más registros que eso.

Aquí hay un buen ejemplo de uso:

var some_product_numbers = new int[] { 1,2,3 ... 9999 }; Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber); 

Aquí está mi implementación (no funcional):

 public static IEnumerable SelectByParameterList(Table items, IEnumerable parameterList, Expression<Func> property) where T : class { var groups = parameterList .Select((Parameter, index) => new { GroupID = index / 2000, //2000 parameters per request Parameter } ) .GroupBy(x => x.GroupID) .AsEnumerable(); var results = groups .Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } ) .SelectMany(g => /* THIS PART FAILS MISERABLY */ items.Where(item => g.Parameters.Contains(property.Compile()(item))) ); return results; } 

He visto muchos ejemplos de creación de predicados utilizando expresiones. En este caso, solo quiero ejecutar el delegado para devolver el valor del ProductNumber actual. O más bien, quiero traducir esto en la consulta SQL (funciona bien en forma no genérica).

Sé que comstackr Expression solo me lleva al punto de partida (pasar el delegado como Func), pero no estoy seguro de cómo pasar un parámetro a una expresión “sin comstackr”.

¡Gracias por tu ayuda!

**** EDIT: ** Permítanme aclarar más:

Aquí hay un ejemplo de trabajo de lo que quiero generalizar:

 var local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray(); var groups = local_refill_ids .Select((Parameter, index) => new { GroupID = index / 5, //5 parameters per request Parameter } ) .GroupBy(x => x.GroupID) .AsEnumerable(); var results = groups .Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } ) .SelectMany(g => Refills.Where(r => g.Parameters.Contains(r.Id)) ) .ToArray() ; 

Resultados en este código SQL:

 SELECT [t0].[Id], ... [t0].[Version] FROM [Refill] AS [t0] WHERE [t0].[Id] IN (@p0, @p1, @p2, @p3, @p4) ... That query 4 more times (20 / 5 = 4) 

He encontrado una manera de dividir la consulta en partes, es decir, le das 4000 valores, por lo que podría hacer 4 solicitudes de 1000 cada una; con el ejemplo completo de Northwind. Tenga en cuenta que esto podría no funcionar en Entity Framework, debido a Expression.Invoke , pero está bien en LINQ to SQL:

 using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace ConsoleApplication5 { /// SAMPLE USAGE class Program { static void Main(string[] args) { // get some ids to play with... string[] ids; using(var ctx = new DataClasses1DataContext()) { ids = ctx.Customers.Select(x => x.CustomerID) .Take(100).ToArray(); } // now do our fun select - using a deliberately small // batch size to prove it... using (var ctx = new DataClasses1DataContext()) { ctx.Log = Console.Out; foreach(var cust in ctx.Customers .InRange(x => x.CustomerID, 5, ids)) { Console.WriteLine(cust.CompanyName); } } } } /// THIS IS THE INTERESTING BIT public static class QueryableChunked { public static IEnumerable InRange( this IQueryable source, Expression> selector, int blockSize, IEnumerable values) { MethodInfo method = null; foreach(MethodInfo tmp in typeof(Enumerable).GetMethods( BindingFlags.Public | BindingFlags.Static)) { if(tmp.Name == "Contains" && tmp.IsGenericMethodDefinition && tmp.GetParameters().Length == 2) { method = tmp.MakeGenericMethod(typeof (TValue)); break; } } if(method==null) throw new InvalidOperationException( "Unable to locate Contains"); foreach(TValue[] block in values.GetBlocks(blockSize)) { var row = Expression.Parameter(typeof (T), "row"); var member = Expression.Invoke(selector, row); var keys = Expression.Constant(block, typeof (TValue[])); var predicate = Expression.Call(method, keys, member); var lambda = Expression.Lambda>( predicate, row); foreach(T record in source.Where(lambda)) { yield return record; } } } public static IEnumerable GetBlocks( this IEnumerable source, int blockSize) { List list = new List(blockSize); foreach(T item in source) { list.Add(item); if(list.Count == blockSize) { yield return list.ToArray(); list.Clear(); } } if(list.Count > 0) { yield return list.ToArray(); } } } } 

La forma más sencilla de hacerlo: utilice LINQKit ( licencia gratuita y no restrictiva)

Versión de trabajo del código:

 public static IEnumerable SelectByParameterList(this Table items, IEnumerable parameterList, Expression> propertySelector, int blockSize) where T : class { var groups = parameterList .Select((Parameter, index) => new { GroupID = index / blockSize, //# of parameters per request Parameter } ) .GroupBy(x => x.GroupID) .AsEnumerable(); var selector = LinqKit.Linq.Expr(propertySelector); var results = groups .Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } ) .SelectMany(g => /* AsExpandable() extension method requires LinqKit DLL */ items.AsExpandable().Where(item => g.Parameters.Contains(selector.Invoke(item))) ); return results; } 

Ejemplo de uso:

  Guid[] local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray(); IEnumerable results = Refills.SelectByParameterList(local_refill_ids, r => r.Id, 10); //runs 2 SQL queries with 10 parameters each 

¡Gracias, de nuevo, por toda su ayuda!

LINQ-to-SQL todavía funciona a través de parámetros SQL estándar, por lo que escribir una expresión elegante no va a ayudar. Hay 3 opciones comunes aquí:

  • empacar los identificadores en (por ejemplo) csv / tsv; pasar como varchar(max) y usar un udf para dividirlo (en el servidor) en una variable de tabla; unirse a la variable de tabla
  • utilice un parámetro con valores de tabla en SQL Server 2008
  • tener una tabla en el servidor en la que pueda insertar los identificadores (quizás a través de SqlBulkCopy) (quizás con una “guía de sesión” o similar); únete a esta mesa

El primero es el más simple; obtener un “split csv udf” es trivial (solo búscalo). Arrastre el udf al contexto de datos y consum desde allí.

Pase IQuerable a la función Contains lugar de lista o matriz. por favor mira el ejemplo de abajo

 var df_handsets = db.DataFeed_Handsets.Where(m => m.LaunchDate != null). Select(m => m.Name); var Make = (from m in db.MobilePhones where (m.IsDeleted != true || m.IsDeleted == null) && df_handsets.Contains(m.Name) orderby m.Make select new { Value = m.Make, Text = m.Make }).Distinct(); 

cuando pasa la lista o matriz, se pasa en forma de parámetros y excede los recuentos cuando los elementos de la lista cuentan más que 2100.

Puede crear su propio QueryProvider

 public class QueryProvider : IQueryProvider { // Translates LINQ query to SQL. private readonly Func _translator; // Executes the translated SQL and retrieves results. private readonly Func _executor; public QueryProvider( Func translator, Func executor) { this._translator = translator; this._executor = executor; } #region IQueryProvider Members public IQueryable CreateQuery(Expression expression) { return new Queryable(this, expression); } public IQueryable CreateQuery(Expression expression) { throw new NotImplementedException(); } public TResult Execute(Expression expression) { bool isCollection = typeof(TResult).IsGenericType && typeof(TResult).GetGenericTypeDefinition() == typeof(IEnumerable<>); var itemType = isCollection // TResult is an IEnumerable`1 collection. ? typeof(TResult).GetGenericArguments().Single() // TResult is not an IEnumerable`1 collection, but a single item. : typeof(TResult); var queryable = Activator.CreateInstance( typeof(Queryable<>).MakeGenericType(itemType), this, expression) as IQueryable; IEnumerable queryResult; // Translates LINQ query to SQL. using (var command = this._translator(queryable)) { var parameters = command.Parameters.OfType() .Select(parameter => parameter) .ToList(); var query = command.CommandText; var newParameters = GetNewParameterList(ref query, parameters); queryResult = _executor(itemType,query,newParameters); } return isCollection ? (TResult)queryResult // Returns an IEnumerable`1 collection. : queryResult.OfType() .SingleOrDefault(); // Returns a single item. } public object Execute(Expression expression) { throw new NotImplementedException(); } #endregion private static object[] GetNewParameterList(ref string query, List parameters) { var newParameters = new List(parameters); foreach (var dbParameter in parameters.Where(p => p.DbType == System.Data.DbType.Int32)) { var name = dbParameter.ParameterName; var value = dbParameter.Value != null ? dbParameter.Value.ToString() : "NULL"; var pattern = String.Format("{0}[^0-9]", dbParameter.ParameterName); query = Regex.Replace(query, pattern, match => value + match.Value.Replace(name, "")); newParameters.Remove(dbParameter); } for (var i = 0; i < newParameters.Count; i++) { var parameter = newParameters[i]; var oldName = parameter.ParameterName; var pattern = String.Format("{0}[^0-9]", oldName); var newName = "@p" + i; query = Regex.Replace(query, pattern, match => newName + match.Value.Replace(oldName, "")); } return newParameters.Select(x => x.Value).ToArray(); } } static void Main(string[] args) { using (var dc=new DataContext()) { var provider = new QueryProvider(dc.GetCommand, dc.ExecuteQuery); var serviceIds = Enumerable.Range(1, 2200).ToArray(); var tasks = new Queryable(provider, dc.Tasks).Where(x => serviceIds.Contains(x.ServiceId) && x.CreatorId==37 && x.Creator.Name=="12312").ToArray(); } }