I have this code :
public void CreateOrdering(string field, string direction)
{
//direction : ASC/DESC
var result = context.MyTable
.Where(x => x.Code > 5)
.OrderBy()
.Skip(10)
.Take(5)
.ToList<MyTable>();
}
I rephrase, I have a method, this method receive as string field name for ordering and the direction ("ASC", "DESC")
I'd like create a Order with the field and the direction received in argument. I have to be able to :
I'd like in this Query be able to do an ascending and descending
Set the ordering field by programming, here Code may be later Id or other ...
The ordering must be done on the SQL Server side not on the list returned
Thanks,
You may use reflection in an extension method which allows for linq syntax:
public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string field, string direction)
{
string orderByMethod = (direction == "ASC") ? "OrderBy" : (direction == "DESC" ? "OrderByDescending" : null);
if(orderByMethod == null) throw new ArgumentException();
var propertyInfo = typeof (TSource).GetProperty(field);
var entityParam = Expression.Parameter(typeof(TSource), "e");
Expression columnExpr = Expression.Property(entityParam, propertyInfo);
LambdaExpression columnLambda = Expression.Lambda(columnExpr, entityParam);
MethodInfo orderByGeneric = typeof (Queryable).GetMethods().Single(m => m.Name == orderByMethod
&& m.GetParameters().Count() == 2
&& m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
&& m.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));
MethodInfo orderBy = orderByGeneric.MakeGenericMethod(new [] {typeof(TSource), propertyInfo.PropertyType});
return (IQueryable<TSource>) orderBy.Invoke(null, new object[] { source, columnLambda });
}
Sample use:
internal class SomeType
{
public string StringValue { get; set; }
}
IQueryable<SomeType> l = new List<SomeType>
{
new SomeType {StringValue = "bbbbb"},
new SomeType {StringValue = "cccc"},
new SomeType {StringValue = "aaaa"},
new SomeType {StringValue = "eeee"},
}.AsQueryable();
var asc = l.OrderBy("StringValue", "ASC");
var desc = l.OrderBy("StringValue", "DESC");
Or for your example:
context.MyTable
.Where(x => x.Code > 5)
.OrderBy(field, direction)
.Skip(10)
.Take(5)
.ToList<MyTable>();
I may have misunderstood your question, but can't you just do:
Ascending
.OrderBy(x => x.Property)
Descending
.OrderByDescending(x => x.Property)
Update
What you need is Dynamic LINQ. However, what you are trying to do it could get quite complicated. As a simple workaround you could do something like:
var result = context.MyTable
.Where(x => x.Code > 15);
if (direction == "ASC")
{
result = result.OrderBy(field);
}
else
{
result = result.OrderByDescending(field);
}
result = result.Skip(10)
.Take(5)
.ToList<MyTable>();
void Main() {
// Ascending by some other property
CreateOrdering(item => item.SomeProperty, SortDirection.Ascending).Dump("Ascending order for SomeClass.SomeProperty");
// Descending by some other property
CreateOrdering(item => item.SomeProperty, SortDirection.Descending).Dump("Descending order for SomeClass.SomeProperty");
// Ascending by the Code property
CreateOrdering(item => item.Code, SortDirection.Ascending).Dump("Ascending order for SomeClass.Code");
// Descending by the Code property
CreateOrdering(item => item.Code, SortDirection.Descending).Dump("Descending order for SomeClass.Code");
}
// I reccomend not using bare strings, and instead use an enum
public enum SortDirection {
Ascending = 0,
Descending = 1
}
// Define other methods and classes here
public List<SomeClass> CreateOrdering<T>(Expression<Func<SomeClass, T>> field, SortDirection direction) {
// query does not get executed yet, because we have not enumerated it.
var query = context.MyTable
.Where(x => x.Code > 5);
if (direction.Equals(SortDirection.Ascending)) {
query = query.OrderBy (field);
} else {
query = query.OrderByDescending (field);
}
// query gets executed when the call ToList is made.
return query.Skip(10)
.Take(5)
.ToList();
}
public static class context {
private static List<SomeClass> _MyTable = new List<SomeClass>() {
new SomeClass("A", 4), new SomeClass("B", 5), new SomeClass("C", 6),
new SomeClass("D", 7), new SomeClass("E", 8), new SomeClass("F", 9),
new SomeClass("G", 10), new SomeClass("H", 11), new SomeClass("I", 12),
new SomeClass("J", 13), new SomeClass("K", 14), new SomeClass("L", 15),
new SomeClass("M", 16), new SomeClass("N", 17), new SomeClass("O", 18)
};
public static IQueryable<SomeClass> MyTable {
get {
return _MyTable.AsQueryable();
}
}
}
public class SomeClass {
public SomeClass(string property, int code) {
this.SomeProperty = property;
this.Code = code;
}
public string SomeProperty { get; set; }
public int Code { get; set; }
}
normally you would do this:
.OrderBy(x => x.yourField)
or
.OrderByDescending(x => x.yourField)
if you need your field to be dynamic, check this answer
If the field is passed as a string (for instance when using an ObjectDataSource), you can map it using a switch:
var qry = context
.MyTable
.Where(x => x.Code > 5);
switch(orderBy) {
case "MyField": qry = qry.OrderBy(r => r.MyField); break;
case "MyField DESC": qry = qry.OrderByDescending(r => r.MyField); break;
}
// By the way, ToList can infer the generic type if you don't
// want to state it explicity
var result = qry.Skip(10).Take(5).ToList();
The query is not executed before the ToList, and at least with EF it is executed on the SQL Server. I admit the switch is quite a lot of boilerplate, but it did turn out to be quite reliable and fast.
Related
I have a lambda expression like
x => x.Property0 == "Z" && old.Any(y => y.Key0 == x.Key0 && y.Property0 != x.Property0)
This expression is passed into the method as a string because it comes from a configuration file. That means I have to convert the string into an expression in order to then execute it.
public override async Task<IList<T>> CalculateList<T>(IList<T> old, IList<T> current)
{
string filter = "x => x.Property0 == \"Z\" && old.Any(y => y.Key0 == x.Key0 && y.Property0 != x.Property0)";
var exp = DynamicExpressionParser.ParseLambda<T, bool>(ParsingConfig.Default, false, filter, new object[0]);
var func = exp.Compile();
return current.Where(func).ToList();
}
If I only enter "x => x.Property0 == \" Z \"" in the filter variable, then the result fits, so the problem seems to be the old.Any, but I have not yet found a solution to the problem . However, no error is thrown, so no indication of the problem.
Can anyone tell me why the expression is not working correctly, or what I need to adjust to make it work.
Thanks
old is a variable, you should pass a value to it.
string filter = "x => x.Property0 == \"AA\" && #0.Any(y => y.Key0 == x.Key0 && y.Property0 != x.Property0 )";
var exp = DynamicExpressionParser.ParseLambda<T, bool>(ParsingConfig.Default, false, filter, old);
var func = exp.Compile();
return current.Where(func).ToList();
Example:
public async Task<IActionResult> IndexAsync()
{
IList<Employee> current = new List<Employee>
{
new Employee{ Id = 1, Name = "AA"},
new Employee{ Id = 2, Name = "BB"},
new Employee{ Id = 3, Name = "CC"},
new Employee{ Id = 4, Name = "DD"},
};
IList<Employee> old = new List<Employee>
{
new Employee{ Id = 1, Name = "BB"},
new Employee{ Id = 2, Name = "AA"},
new Employee{ Id = 4, Name = "DD"},
};
var result = CalculateList(old, current);
return View();
}
public IList<T> CalculateList<T>(IList<T> old, IList<T> current)
{
string filter = "x => x.Name == \"AA\" && #0.Any(y => y.Id == x.Id && y.Name != x.Name)";
var exp = DynamicExpressionParser.ParseLambda<T, bool>(ParsingConfig.Default, false, filter, old);
var func = exp.Compile();
return current.Where(func).ToList();
}
Result:
these are my first steps with LINQ. I have two lists for filters as parameters, which can also be empty. If I execute the code this way, I don't get any values back from an empty list.
What does the code have to look like if empty lists are to be ignored?
public List<PersonDTO> GetPersons(int pageNumber, int pageSize, List<string> departments, List<string> locations, string filterText)
{
if (filterText == null)
{
filterText = "";
}
List<Person> personsList = _dbContext.Persons
.Where(a => (a.firstName.ToLower().Contains(filterText.ToLower()) || a.lastName.ToLower().Contains(filterText.ToLower()))
&& departments.Contains(a.department)
&& locations.Contains(a.location))
.Skip(pageNumber * pageSize).Take(pageSize).ToList();
return _mapper.Map<List<PersonDTO>>(personsList);
}
Handle the case that they are empty not in the query but with if:
IEnumerable<Person> persons = _dbContext.Persons;
if(!string.IsNullOrEmpty(filterText))
{
string lowerFilterText = filterText.ToLower();
persons = persons
.Where(p => p.firstName.ToLower().Contains(lowerFilterText) || a.lastName.ToLower().Contains(lowerFilterText));
}
if(departments.Any())
{
persons = persons.Where(p => departments.Contains(p.department));
}
if(locations.Any())
{
persons = persons.Where(p => locations.Contains(p.location));
}
List<Person> personList = persons.Skip(pageNumber * pageSize).Take(pageSize).ToList();
Due to LINQ's deferred execution this will execute the final query only once, at the final ToList.
See if negating Any() can help you.
Example:
string[] arr = new string[10] {"a","b","c","d","e","f","g","h","i","j"};
List<string> vowels = new List<string>() {"a","e","i","o","u"};
List<string> empty = new List<string>();
arr.Where(letter => vowels.Contains(letter));
//yields "a","e","i"
arr.Where(letter => (!empty.Any() || empty.Contains(letter)));
//yields "a","b","c","d","e","f","g","h","i","j"
Following your example, I'd chain .Where() expressions rather than putting it all into a big, single one.
You need to wrap the lists in parenthesis and use a count validation to make it optional for each list, like so:
List<Person> personsList = _dbContext.Persons
.Where(a =>
(a.firstName.ToLower().Contains(filterText.ToLower()) ||
a.lastName.ToLower().Contains(filterText.ToLower())) &&
(departments.Count == 0 || departments.Contains(a.department)) &&
(locations.Count == 0 || locations.Contains(a.location)))
.Skip(pageNumber * pageSize)
.Take(pageSize)
.ToList();
This way you switch the array conditional to an optional state, so whenever there is an item in the array !(list.Count == 0) it then tries to evaluate the filter.
You need something like this :
Why would you use Expression> rather than Func?
Expression<Func<Persons, bool>> expresionFinal = c => c.Active;
if (departments.Any())
{
Expression<Func<Persons, bool>> expresionDepartments = c => departments.Contains(p.department);
expresionFinal = PredicateBuilder.And(expresionFinal, expresionDepartments);
}
IQueryable query = dataContext.Persons;
query = query.Where(expresionFinal);
Try using .Where(s => !string.IsNullOrWhiteSpace(s)) to filter out null and empty strings entries in the list.
There are several way to check if list is empty:
1. If(list.Count() >0)
2. The best way, is to use "Any" instead of "Where" ,this will return boolian result, if true so there is some data, else there is nothing.
If you want to ignore empty list (departements and locations) in your where clause, you should be able to use Any():
public List<PersonDTO> GetPersons(int pageNumber, int pageSize, List<string> departments, List<string> locations, string filterText)
{
if (filterText == null)
{
filterText = "";
}
List<Person> personsList = _dbContext.Persons
.Where(a => (a.firstName.Contains(filterText, StringComparison.OrdinalIgnoreCase)
|| a.lastName.Contains(filterText, StringComparison.OrdinalIgnoreCase))
&& (!departments.Any() || departments.Contains(a.department))
&& (!locations.Any() || locations.Contains(a.location)))
.Skip(pageNumber * pageSize).Take(pageSize).ToList();
return _mapper.Map<List<PersonDTO>>(personsList);
}
and the code for contains:
public static class StringExtensions
{
public static bool Contains(this string source, string toCheck, StringComparison comp)
{
return source?.IndexOf(toCheck, comp) >= 0;
}
}
source: https://stackoverflow.com/a/444818/1248177
This should work. You should really build up your query before you call ToList() if you can. This will allow you to be able to do things in stages and make your application more efficient.
public List<PersonDTO> GetPersons(int pageNumber, int pageSize, List<string> departments, List<string> locations, string filterText = "")
{
List<Person> personList = new List<Person>();
if (!string.IsNullOrEmpty(filterText)) {
personsList = _dbContext.Persons
.Where(a => (a.firstName.ToLower().Contains(filterText.ToLower()) || a.lastName.ToLower().Contains(filterText.ToLower()))
&& departments.Contains(a.department)
&& locations.Contains(a.location)).ToList();
} else {
personList = _dbContext.Persons.ToList();
}
personList = personList.Skip(pageNumber * pageSize).Take(pageSize).ToList();
return _mapper.Map<List<PersonDTO>>(personsList);
}
Here is another example using IQueryable.
public List<PersonDTO> GetPersons(int pageNumber, int pageSize, List<string> departments, List<string> locations, string filterText = "")
{
IQueryable<List<Person>> personQuery = _dbContext.Persons.AsQueryable();
if (!string.IsNullOrEmpty(filterText))
{
personQuery = personQuery
.Where(a => (a.firstName.ToLower().Contains(filterText.ToLower()) || a.lastName.ToLower().Contains(filterText.ToLower()))
&& departments.Contains(a.department)
&& locations.Contains(a.location));
}
personQuery = personQuery.Skip(pageNumber * pageSize).Take(pageSize);
return _mapper.Map<List<PersonDTO>>(personQuery.ToList());
}
This is an example of how I did what you are trying to do.
public List<CourseSearchDetail> GetPaginated(SearchRequest searchRequest, bool admin, out int totalRecords,
out int recordsFiltered)
{
var query = _courseRepo
.GetDataTableQuery();
if (!admin) query = query.Where(x => x.CourseDate > DateTime.Now);
var courseList = query.ToList();
totalRecords = courseList.Count();
if (!string.IsNullOrEmpty(searchRequest.Search.Value))
courseList = courseList.Where(x => x.CourseTitle.ToLower().Contains(searchRequest.Search.Value.ToLower())).ToList();
recordsFiltered = courseList.Count();
if (searchRequest.Order == null)
courseList = courseList.OrderByDescending(x => x.CourseDate).ToList();
else
courseList = courseList.OrderResults(searchRequest);
var skip = searchRequest.Start;
var pageSize = searchRequest.Length;
courseList = pageSize > 0
? courseList.Skip(skip).Take(pageSize).ToList()
: courseList.ToList();
return courseList;
}
I didn't think this would be so rare to find but as it seems, it is. Here's the situation.
I've a ApplySort method which takes in 3 parameters posted from a mvc page.
private List<dynamic> ApplySort(List<dynamic> listToBeSorted, string sortBy, string sortOrder)
{
if (String.IsNullOrEmpty(sortBy))
sortBy = "createddate";
if (String.IsNullOrEmpty(sortOrder) || sortOrder.Trim() != "0")
sortOrder = "1"; // 1 = descending, 0 = ascending
if (sortOrder == "1")
{
switch (sortBy)
{
case "name":
listToBeSorted = listToBeSorted.OrderByDescending(a => a.name).ToList();
break;
case "assigned":
listToBeSorted = listToBeSorted.OrderByDescending(a => a.title).ToList();
break;
case "duedate":
listToBeSorted = listToBeSorted.OrderByDescending(a => a.end).ToList();
break;
case "status":
listToBeSorted = listToBeSorted.OrderByDescending(a => a.title).ToList();
break;
default:
listToBeSorted = listToBeSorted.OrderByDescending(a => a.title).ToList();
break;
}
}
else
{
// same code as in if-block, with just OrderBy calls instead of OrderByDescending
}
return listToBeSorted;
}
Two problems:
1) Method seems unnecessarily long (with very similar code inside if and else blocks).
2) I want to be able to sort using multiple columns. sortBy param can have values like "name,title,createddate,status". So the sort applied should be, first by name, thenby title, then by createddate...and so on. I can use ThenBy by checking params sequentially. But how to dynamically apply a chain of ThenBy(s) based on the param value, where the number of ThenBy can vary.
string[] sortParams = sortBy.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
listToBeSorted.OrderBy(i=>i.sortParams[0]).ThenBy(j=>j.sortParams[1]).ThenBy(k=>k.sortParams[2])...(so on till sortParams.length)
How would I do this ? Plus, also how can I use the sortOrder parameter to sort by ascending or descending in the same line instead of using if-else.
You can create dictionary with Func<string, IComparable> mapping to properties of your class.
public class ItemWithProperty
{
public string Property { get; set; }
}
public static void Main()
{
Dictionary<string, Func<ItemWithProperty, IComparable>> stringPropertyMap = new Dictionary<string, Func<ItemWithProperty, IComparable>>()
{
{"param1", item => item.Property}
};
List<ItemWithProperty> toBeOrdered = new List<ItemWithProperty>();
string[] parameters = {"param1"};
var sorted = toBeOrdered.OrderBy(stringPropertyMap[parameters[0]]);
}
You use this if you change your method signature:
private static IEnumerable<dynamic> ApplySort(IEnumerable<dynamic> listToBeSorted, ICollection<KeyValuePair<string, string>> sorters)
{
var orderBy = (sorters == null || sorters.Count == 0) ? new KeyValuePair<string, string>("createddate", "1") : sorters.First();
var thenBys = (sorters == null || sorters.Count == 1) ? new List<KeyValuePair<string, string>>() : sorters.Except(Enumerable.Repeat(orderBy, 1));
var orderedEnumerable = orderBy.Value == "1"
? listToBeSorted.OrderBy(x => GetPropertyValue(x, orderBy.Key))
: listToBeSorted.OrderByDescending(x => GetPropertyValue(x, orderBy.Key));
orderedEnumerable = thenBys.Aggregate(orderedEnumerable, (current, thenBy) => thenBy.Value == "1"
? current.ThenBy(x => GetPropertyValue(x, thenBy.Key))
: current.ThenByDescending(x => GetPropertyValue(x, thenBy.Key)));
return orderedEnumerable.ToList();
}
private static object GetPropertyValue(dynamic obj, string propName)
{
Type t = obj.GetType();
return t.GetProperty(propName).GetValue(obj, null);
}
Try :
static void Main(string[] args)
{
var list = new List<dynamic>();
list.Add(new { name = "Billy" });
list.Add(new { name = "Johnny" });
list.Add(new { name = "Ali" });
var list2 = ApplySort(list, new List<KeyValuePair<string, string>>(new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("name", "1") }));
foreach (var o in list2)
{
Console.WriteLine(o.name);
}
Console.ReadLine();
}
I'm looking to fill an object model with the count of a linq-to-sql query that groups by its key.
The object model looks somewhat like this:
public class MyCountModel()
{
int CountSomeByte1 { get; set; }
int CountSomeByte2 { get; set; }
int CountSomeByte3 { get; set; }
int CountSomeByte4 { get; set; }
int CountSomeByte5 { get; set; }
int CountSomeByte6 { get; set; }
}
This is what I have for the query:
var TheQuery = from x in MyDC.TheTable
where ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7
group x by x.SomeByte into TheCount
select new MyCountModel()
{
CountSomeByte1 = TheCount.Where(TheCount => TheCount.Key == 1)
.Select(TheCount).Count(),
CountSomeByte2 = TheCount.Where(TheCount => TheCount.Key == 2)
.Select(TheCount).Count(),
.....
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
}.Single();
ListOfRecordIDs is list of longs that's passed in as a parameter. All the CountSomeByteN are underlined red. How do you do a count of grouped elements with the group's key mapped to an object model?
Thanks for your suggestions.
The select is taking each element of your group and projecting them to identical newly created MyCountModels, and you're only using one of them. Here's how I'd do it:
var dict = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count());
var result = new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
}
EDIT: Here's one way to do it in one statement. It uses an extension method called Into, which basically works as x.Into(f) == f(x). In this context, it can be viewed as like a Select that works on the whole enumerable rather than on its members. I find it handy for eliminating temporary variables in this sort of situation, and if I were to write this in one statement, it's probably how I'd do it:
public static U Into<T, U>(this T self, Func<T, U> func)
{
return func(self);
}
var result = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count())
.Into(dict => new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
});
Your range variable is not correct in the subqueries:
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
In method notation you don't need the extra select:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Count(),
If you want to use it anyway:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Select(theCount => theCount).Count(),
I'm trying to create a generic function to help me select thousands of records using LINQ to SQL from a local list. SQL Server (2005 at least) limits queries to 2100 parameters and I'd like to select more records than that.
Here would be a good example usage:
var some_product_numbers = new int[] { 1,2,3 ... 9999 };
Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber);
Here is my (non-working) implementation:
public static IEnumerable<T> SelectByParameterList<T, PropertyType>(Table<T> items,
IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> 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;
}
I have seen plenty of examples of building predicates using expressions. In this case I only want to execute the delegate to return the value of the current ProductNumber. Or rather, I want to translate this into the SQL query (it works fine in non-generic form).
I know that compiling the Expression just takes me back to square one (passing in the delegate as Func) but I'm unsure of how to pass a parameter to an "uncompiled" expression.
Thanks for your help!
**** EDIT:** Let me clarify further:
Here is a working example of what I want to generalize:
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()
;
Results in this SQL code:
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)
I've come up with a way to chunk the query into pieces - i.e. you give it 4000 values, so it might do 4 requests of 1000 each; with full Northwind example. Note that this might not work on Entity Framework, due to Expression.Invoke - but is fine on 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<T> InRange<T, TValue>(
this IQueryable<T> source,
Expression<Func<T, TValue>> selector,
int blockSize,
IEnumerable<TValue> 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<Func<T,bool>>(
predicate, row);
foreach(T record in source.Where(lambda)) {
yield return record;
}
}
}
public static IEnumerable<T[]> GetBlocks<T>(
this IEnumerable<T> source, int blockSize) {
List<T> list = new List<T>(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();
}
}
}
}
Easiest way to do this: Use LINQKit (Free, non-restrictive license)
Working version of code:
public static IEnumerable<T> SelectByParameterList<T, PropertyType>(this Table<T> items, IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> 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;
}
Example usage:
Guid[] local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray();
IEnumerable<Refill> results = Refills.SelectByParameterList(local_refill_ids, r => r.Id, 10); //runs 2 SQL queries with 10 parameters each
Thanks again for all your help!
LINQ-to-SQL still works via standard SQL parameters, so writing a fancy expression isn't going to help. There are 3 common options here:
pack the ids into (for example) csv/tsv; pass down as a varchar(max) and use a udf to split it (at the server) into a table variable; join to the table variable
use a table-valued-parameter in SQL Server 2008
have a table on the server that you could push the ids into (perhaps via SqlBulkCopy) (perhaps with a "session guid" or similar); join to this table
The first is the simplest; getting a "split csv udf" is trivial (just search for it). Drag the udf onto the data-context and consume from there.
Pass IQuerable to the Contains function instead of list or array. please see the below example
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();
when you pass list or array it is passed in form of parameters and its exceed the counts when the list items count is greater than 2100.
You can create your own QueryProvider
public class QueryProvider : IQueryProvider
{
// Translates LINQ query to SQL.
private readonly Func<IQueryable, DbCommand> _translator;
// Executes the translated SQL and retrieves results.
private readonly Func<Type, string, object[], IEnumerable> _executor;
public QueryProvider(
Func<IQueryable, DbCommand> translator,
Func<Type, string, object[], IEnumerable> executor)
{
this._translator = translator;
this._executor = executor;
}
#region IQueryProvider Members
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new Queryable<TElement>(this, expression);
}
public IQueryable CreateQuery(Expression expression)
{
throw new NotImplementedException();
}
public TResult Execute<TResult>(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<DbParameter>()
.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<TResult>()
.SingleOrDefault(); // Returns a single item.
}
public object Execute(Expression expression)
{
throw new NotImplementedException();
}
#endregion
private static object[] GetNewParameterList(ref string query, List<DbParameter> parameters)
{
var newParameters = new List<DbParameter>(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<Task>(provider, dc.Tasks).Where(x => serviceIds.Contains(x.ServiceId) && x.CreatorId==37 && x.Creator.Name=="12312").ToArray();
}
}