I've created a generic to target tables in my database. I've also added a generic .where() with an expression containing only 1 parameter in the lambda expression. Is there a way that I can add more than 1 parameter in the same expression? Please ignore the fact that the below method returns 1 item, I would like to make it return a IList.
Here's my method:
public virtual T GetById( Int32 id, String someStringColumn )
{
ParameterExpression itemParameter = Expression.Parameter( typeof( T ), "item" );
var whereExpression = Expression.Lambda<Func<T, Boolean>>
(
Expression.Equal( Expression.Property( itemParameter, "Id" ), Expression.Constant( id ) ),
new[] { itemParameter }
);
return context.Set<T>().Where( whereExpression ).FirstOrDefault();
}
My actual intentions for this method is to later perform a Contains() on "string" properties of the target table "T". I would like to do something like below and append to the above Expression a check if the String property contains a value in the "someStringColumn". Just an insight, the "someStringColumn" will be a general search string on my page past by Ajax on every back-end call.
var properties = item.GetType().GetProperties().Where( p => p.PropertyType == typeof( string ) ).ToArray();
for ( Int32 i = 0; i < properties.Length; i++ )
{
}
I'm trying to achieve something like this in a non-generic method:
public override List<TableInDatabase> List( PagingModel pm, CustomSearchModel csm )
{
String[] qs = ( pm.Query ?? "" ).Split( ' ' );
return context.TableInDatabase
.Where( t => ( qs.Any( q => q != "" ) ? qs.Contains( t.ColumnName) : true ) )
.OrderBy( String.Format( "{0} {1}", pm.SortBy, pm.Sort ) )
.Skip( pm.Skip )
.Take( pm.Take )
.ToList();
}
If I understand correctly, you are seeking for something like this:
var item = Expression.Parameter(typeof(T), "item");
Expression body = Expression.Equal(Expression.Property(item, "Id"), Expression.Constant(id));
if (!string.IsNullOrEmpty(someStringColumn))
{
var properties = typeof(T).GetProperties().Where(p => p.PropertyType == typeof(string)).ToList();
if (properties.Any())
body = Expression.AndAlso(body,
properties.Select(p => (Expression)Expression.Call(
Expression.Property(item, p), "Contains", Type.EmptyTypes, Expression.Constant(someStringColumn))
).Aggregate(Expression.OrElse));
}
var whereExpression = Expression.Lambda<Func<T, bool>>(body, item);
i.e. build Contains expression for each string property, combine them using Or and finally combine the result with the first condition using And.
Related
I want to create a lambda expression dynamically for this:
(o => o.Year == year && o.CityCode == cityCode && o.Status == status)
and I write this:
var body = Expression.AndAlso(
Expression.Equal(
Expression.PropertyOrField(param, "Year"),
Expression.Constant(year)
),
Expression.Equal(
Expression.PropertyOrField(param, "CityCode"),
Expression.Constant(cityCode)
)
,
Expression.Equal(
Expression.PropertyOrField(param, "Status"),
Expression.Constant(status)
)
);
but for this chunk of code:
Expression.Equal(
Expression.PropertyOrField(param, "Status"),
Expression.Constant(status)
)
I got an error:
Cannot convert from 'System.Linq.Expressions.BinaryExpression' to 'System.Reflection.MethodInfo'
How I can add 3 conditions to a lambda expression?
Expression.AndAlso takes two expressions. There is an overload that takes three arguments, but that third argument is a MethodInfo of a method that implements an and operation on the two operands (there are further restrictions in the case of AndAlso as it doesn't allow the details of truthiness to be overridden, so the first operand will still need to either have a true and false operator or be castable to bool).
So what you want is the equivalent of:
(o => o.Year == year && (o.CityCode == cityCode && o.Status == status))
Which would be:
var body = Expression.AndAlso(
Expression.Equal(
Expression.PropertyOrField(param, "Year"),
Expression.Constant(year)
),
Expression.AndAlso(
Expression.Equal(
Expression.PropertyOrField(param, "CityCode"),
Expression.Constant(cityCode)
),
Expression.Equal(
Expression.PropertyOrField(param, "Status"),
Expression.Constant(status)
)
)
);
There is no method called Expression.AndAlso which can take 3 Expressions as arguments.
Please refer below links,
https://msdn.microsoft.com/en-us/library/bb353520(v=vs.110).aspx
https://msdn.microsoft.com/en-us/library/bb382914(v=vs.110).aspx
I'm trying to write manually this linq sentence:
IEnumerable<C> classes = this.cs
.Where(c => c.Properties.Any(p =>
p.Key.Equals("key1") &&
p.Value.Equals("v1")
)
);
As you can see I'm calling to Any extension method on a c.Properties property of C class.
Then, inside Any method I've written an And with two Equals.
I need to create this sentence using Expression builders.
Up to now, I've been able to write this:
Type entityType = typeof(T);
PropertyInfo collectionPropertyInfo = entityType.GetProperty("Properties");
if (collectionPropertyInfo == null)
throw new ArgumentException(
string.Format(
"{0} collection doesn't appear in {1}",
"Properties",
entityType.Name
)
);
Type collGenericType = collectionPropertyInfo
.GetType()
.GetGenericArguments()
.FirstOrDefault();
MemberExpression collectionMemberExpression = Expression.Property(
Expression.Parameter(entityType),
collectionPropertyInfo
);
MethodInfo anyMethod = typeof(Enumerable)
.GetMethods()
.Where(m =>
m.Name.Equals("Any") &&
m.GetParameters().Length == 2
)
.Single()
.MakeGenericMethod(collGenericType);
BinaryExpression innerConditionExpression = Expression.AndAlso(
Expression.Equal(
Expression.Property(Expression.Parameter(collGenericType, "p"), "Key"),
Expression.Constant("key1")
),
Expression.Equal(
Expression.Property(Expression.Parameter(collGenericType, "p"), "Value"),
Expression.Constant("v1")
)
);
I don't know how to write Call to Any method where instance is collectionMemberExpression and iiner condition is innerConditionExpression.
In our database we have a number of tables which have corresponding Translation tables, with language and region IDs (mapped to other tables) with language 1 being English and the default region of language 1 being UK. All tables which have a translation table have the following default columns (although no interface has been defined on the entity framework classes):
<EntityTableName>
EntityTableNameID INT PK
Reference NVARCHAR NULL
[Any other columns]
<EntityTableNameTranslation>
EntityTableNameID INT NOT NULL
LanguageID INT NOT NULL
RegionID INT NULL
Title NVARCHAR NOT NULL
Description NVARCHAR NULL
The naming is consistent throughout the database, so we could add interfaces if required, but for now I've been trying to do it without to save the effort.
The logic for determining which translation title & description to return is:
1) If there is an exact match for both the language and region, return it
2) If there is a match for the language, but not the region, return the "default" for that language (which is where the RegionID is null, and there will always be one for every language)
3) If there is no match for language, just return the system default (LanguageID = 1, RegionID IS NULL).
I know this might all sound weird and everyone has better ways of doing it, but this is the brief I have to work with. So this is the lambda group join function I created, which is using an entity in the database called "OrgGroup":
public static IEnumerable<TransViewModel> GetUserAreaOrgGroups(TransTestEntities context, int companyID, int languageID, int? regionID)
{
var transFull = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
var transLang = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && !tr.RegionID.HasValue);
var transDefault = context.OrgGroupTranslations.Where(tr => tr.LanguageID == 1 && !tr.RegionID.HasValue);
var results = context.OrgGroups.Where(en => en.CompanyID == companyID)
.GroupJoin(transFull, en => en.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en, TransFull = tr.DefaultIfEmpty().FirstOrDefault(), TransLang = null, TransDefault = null})
.GroupJoin(transLang, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en.Entity, TransFull = en.TransFull, TransLang = tr.DefaultIfEmpty().FirstOrDefault(), TransDefault = null })
.GroupJoin(transDefault, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en.Entity, TransFull = en.TransFull, TransLang = en.TransLang, TransDefault = tr.DefaultIfEmpty().FirstOrDefault() })
.Select(vm => new TransViewModel
{
EntityID = vm.Entity.OrgGroupID,
Title = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Title,
Description = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Description
});
return results;
}
Which seems to work as expected, and now I'm trying to convert this into a function which will accept the two table types and use expression trees to create, execute, and return the equivalent query. I've got as far as:
public static IEnumerable<TransViewModel> GetUserAreaTranslations<TEntity, TTrans>(TransTestEntities context, int companyID, int languageID, int? regionID)
{
// Get types
Type entityType = typeof(TEntity);
Type transType = typeof(TTrans);
string entityName = entityType.Name;
string transName = transType.Name;
// Parameters
var entityParam = Expression.Parameter(entityType, "en");
var transParam = Expression.Parameter(transType, "tr");
var combinedParam = new ParameterExpression[] { entityParam, transParam };
// Properties
var CompanyIDProp = Expression.Property(entityParam, "CompanyID");
var entityIDProp = Expression.Property(entityParam, entityName + "ID");
var transIDProp = Expression.Property(transParam, entityName + "ID");
var transLanProp = Expression.Property(transParam, "LanguageID");
var transRegProp = Expression.Property(transParam, "RegionID");
var transTitleProp = Expression.Property(transParam, "Title");
var transDescProp = Expression.Property(transParam, "Description");
// Tables
//TODO: Better way of finding pluralised table names
var entityTable = Expression.PropertyOrField(Expression.Constant(context), entityName + "s");
var transTable = Expression.PropertyOrField(Expression.Constant(context), transName + "s");
// Build translation subqueries
//e.g. context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
MethodCallExpression fullTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.Equal(transRegProp, Expression.Convert(Expression.Constant(languageID), transRegProp.Type))
), transParam
)
)
});
MethodCallExpression lanTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
});
MethodCallExpression defaultTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(1)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
});
MethodCallExpression entityWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { entityType },
new Expression[]
{
entityTable,
Expression.Quote(
Expression.Lambda
(
Expression.Equal(CompanyIDProp, Expression.Convert(Expression.Constant(companyID), CompanyIDProp.Type))
, entityParam
)
)
});
// Create the "left join" call:
// tr.DefaultIfEmpty().FirstOrDefault()
var joinType = typeof(TransJoin<TEntity, TTrans>);
var joinParam = Expression.Parameter(joinType, "tr");
var leftJoinMethods =
Expression.Call(
typeof(Enumerable),
"FirstOrDefault",
new Type[] { transType },
Expression.Call(
typeof(Enumerable),
"DefaultIfEmpty",
new Type[] { transType },
Expression.Parameter(typeof(IEnumerable<TTrans>), "tr"))
);
// Create the return bindings
var emptyTrans = Expression.Constant(null, typeof(TTrans));
//var emptyTrans = Expression.Constant(null);
var fullBindings = new List<MemberBinding>();
fullBindings.Add(Expression.Bind(joinType.GetProperty("Entity"), entityParam));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransFull"), leftJoinMethods));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransLang"), emptyTrans));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransDefault"), emptyTrans));
// Create an object initialiser which also sets the properties
Expression fullInitialiser = Expression.MemberInit(Expression.New(joinType), fullBindings);
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda <Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
// Create first group join
var fullJoin = Expression.Call(
typeof(Queryable),
"GroupJoin",
new Type[]
{
typeof (TEntity), // TOuter,
typeof (TTrans), // TInner,
typeof (int), // TKey,
typeof (TransJoin<TEntity, TTrans>) // TResult
},
new Expression[]
{
entityWhereLambda,
fullTranWhereLambda,
Expression.Lambda<Func<TEntity, int>>(entityIDProp, entityParam),
Expression.Lambda<Func<TTrans, int>>(transIDProp, transParam),
fullResultSelector
}
);
The problem is that groupjoin is expecting to return an IEnumerable of TTrans, which I don't seem to be able to bind, and I can't change it to a standard join because I won't be able to use the coalesce in the projection as no result will be returned.
I'm sure I'm doing something very dumb, so can someone help me get my group joins working please?
The expression node you're looking for is MemberInitExpression, which is what results from compiling a lambda containing a new { } statement.
Let's say we have a simple key-value class like this:
public class KV
{
public int Key;
public string Value;
}
I can build a new expression for this to load some constants like so:
Type tKV = typeof(KV);
MemberInfo miKey = tKV.GetMember("Key")[0];
MemberInfo miValue = tKV.GetMember("Value")[0];
Expression meminit =
Expression.MemberInit(
Expression.New(tKV),
Expression.Bind(miKey, Expression.Constant(1)),
Expression.Bind(miValue, Expression.Constant("Some Value"))
);
Or for the more complete version, constructing a lambda expression that fully initializes the variable:
public Expression<Func<int, string, KV>> InitKV()
{
var pK = Expression.Parameter(typeof(int), "k");
var pV = Expression.Parameter(typeof(string), "v");
Type tKV = typeof(KV);
MemberInfo miKey = tKV.GetMember("Key")[0];
MemberInfo miValue = tKV.GetMember("Value")[0];
Expression meminit =
Expression.MemberInit(
Expression.New(tKV),
Expression.Bind(miKey, pK),
Expression.Bind(miValue, pV)
);
return (Expression<Func<int, string, KV>>)Expression.Lambda(meminit, pK, pV);
}
In your case there are going to be a lot more Bind expressions in there.
Answer #2... this time with more actual answer :P
The problem appears to be that the types of the lambdas and so on that you are passing to the GroupJoin method are wrong.
Specifically:
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
...although some of the others also look a bit iffy, but that could just be me.
The selector expression expected by GroupJoin is of type Expression<Func<TEntity, IEnumerable<TTrans>, TransJoin<TEntity, TTrans>>>. It's going to pass in a single TEntity and a group of TTrans (as IEnumerable<TTrans>) even if there's only one instance in that group. Your expression tree needs to deal with that IEnumerable<TTrans> correctly, which it currently doesn't.
Are you sure you wanted a GroupJoin and not a Join here?
I wrote some code in LINQPad to test the concept. It's over at >PasteBin< if you want to look into it.
Incidentally, using LINQPad's Dump extension on an expression will give you a full breakout of how the expression is constructed. Assign a lambda to a variable of an appropriate Expression<Func<....>> type and then call Dump to see how it is constructed. Helps ferret out the usages and show what you need to do to construct it.
So far, I find Linq can be used on existing fields and properties of a class, not on virtual properties. In other words, ITypedList can not work with Linq, even by dynamic Linq.
I tried the following code:
IQueryable contact ; ...
dynamic l = contact.Select("Customer.Name as Name");
// Customer is a virtual property provided by the interface of ITypedList.
Then, I met the exception of "No linkedPropertyName or field 'Customer' exists in type 'Contact'".
I traced into dynamic Linq and found the following code raised the exception:
MemberInfo member = FindPropertyOrField(type, id, instance == null);
if (member == null)
throw ParseError(errorPos, Res.UnknownPropertyOrField,
id, GetTypeName(type));
return member is PropertyInfo ?
Expression.Property(instance, (PropertyInfo)member) :
Expression.Field(instance, (FieldInfo)member);
in the method of Expression ParseMemberAccess(Type type, Expression instance).
It is obvious only real member of fields and properties are supported in Linq.
But I still expect someone may have found a way to do Linq on virtual properties.
If you find a way to do so, please share your experience.
Thank you in advance,
Ying
This code isn't much better than code you'd write without linq expressions but here it goes.
This code assumes that your ITypedList is also an IList. Gets a property descriptor for the desired property and tests each item in the collection by doing what amounts to a for(int i = 0; i<((ICollection)list).Count;i++)
{
...
}
ParameterExpression listParameter = Expression.Parameter(
typeof(ITypedList),
"list"
);
ParameterExpression propertyDescriptorVariable = Expression.Variable(
typeof(PropertyDescriptor),
"propertyDescriptor"
);
ParameterExpression indexVariable = Expression.Variable(
typeof(int),
"index"
);
ParameterExpression resultVariable = Expression.Variable(
typeof(bool),
"result"
);
LabelTarget #break = Expression.Label();
Expression<Func<ITypedList, bool>> lambdaExpression = Expression.Lambda<Func<ITypedList, bool>>(
Expression.Block(
new[] { propertyDescriptorVariable, indexVariable, resultVariable },
Expression.Assign(
propertyDescriptorVariable,
Expression.Property(
Expression.Call(
listParameter,
typeof(ITypedList).GetMethod(
"GetItemProperties",
BindingFlags.Instance | BindingFlags.Public
),
Expression.Default(
typeof(PropertyDescriptor[])
)
),
typeof(PropertyDescriptorCollection).GetProperty(
"Item",
typeof(PropertyDescriptor),
new[] { typeof(string) }
),
Expression.Constant(
"Name"
)
)
),
Expression.Assign(
indexVariable,
Expression.Constant(
0,
typeof(int)
)
),
Expression.Assign(
resultVariable,
Expression.Constant(
true
)
),
Expression.Loop(
Expression.IfThenElse(
Expression.LessThan(
indexVariable,
Expression.Property(
Expression.Convert(
listParameter,
typeof(ICollection)
),
"Count"
)
),
Expression.IfThenElse(
Expression.Equal(
Expression.Constant(
null
),
Expression.Call(
propertyDescriptorVariable,
"GetValue",
Type.EmptyTypes,
Expression.Property(
Expression.Convert(
listParameter,
typeof(IList)
),
"Item",
indexVariable
)
)
),
Expression.Block(
Expression.Assign(
resultVariable,
Expression.Constant(
false
)
),
Expression.Break(
#break
)
),
Expression.PostIncrementAssign(
indexVariable
)
),
Expression.Break(
#break
)
),
#break
),
resultVariable
),
listParameter
);
bool isEveryNameNotNull = lambdaExpression.Compile().Invoke(list);
EDIT: Let's try this again. This time I've used the AdventureWorks sample database so you can all play along. This will rule out anything crazy I've done in my own database. Here's a new example demonstrating what works and what I would expect to work (but doesn't). Can anyone explain why it doesn't work or suggest a different way of achieving my goal (refactoring out the common expression so it can be reused elsewhere)?
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>
The following LINQ to SQL works fine:
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...
};
However, I want to break out the predicate passed to the Where since it's used throughout the code. But if I try and pass a defined predicate into the Where it fails, such as:
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...
};
This makes no sense to me. Can anyone explain what's going on? Maintenances is of type EntitySet<DatabaseAccess.Maintenance>. The error I get is:
System.NotSupportedException:
Unsupported overload used for query
operator 'Where'..
EDIT: For those interested, here's what Reflector has for the first (working) example with Optimization set to .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>();
}
EDIT: The Reflector output for the second example (using a predicate) is mostly similar. The biggest difference being that, in the call to Enumerable.Where, rather than passing an Expression.Lambda it passes Expression.Constant(activePlanPredicate).
I don't fully understand the guts of Linq to Entities, but there is an Open Source (usable in proprietary software) toolkit specifically designed to help solve this problem, called LinqKit, linked off this O'Reilly-related article:
http://www.albahari.com/nutshell/predicatebuilder.aspx
Since I don't fully understand the guts, I'll just quote them:
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.
Here is a direct link to LinqKit.
And here is the type of code that this project enables:
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
);
The result is:
There are 3 products in SubCategory Bib-Shorts with ListPrice > 0.
Yay, no exception, and we can extract the predicate!
I'd refactor the original like this
private bool IsYourPredicateSatisfied(Maintenance plan)
{
return plan.CancelDate == null && plan.UpgradeDate == null;
}
Then your Where clause is Where(m => IsYourPredicateSatisfied(m))
Try this:
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...
};
I don't have VisualStudio in front of me, so that may require some tweaking. The issue you're running into is that you want to access the IQueryable extension of Where, but just having a Func<T,bool> gives you the IEnumerable extension.