The problem is based on the following example (borrowed from http://stackoverflow.com/questions/2290436/linq-orderby-breaks-with-navigation-property-being-null) :
table Users -> has basic user info including a userid and a departmentid (int)
table Departments -> basic department info including deptid
"Not every user has a department"
var userQuery = from u in grp.Users
select u;
userQuery = userQuery.OrderBy(u => u.Department.Name);
If Department is equal to null, Linq will die with an null reference exception.
We can protect against that by testing on null:
userQuery = userQuery.OrderBy(u => (u.Department != null) ? u.Department.Name : String.Empty
The code used in the webgrid helper does not test on null, resulting for this specific case in a null reference exception.
The code sample underneath is a possible solution without to much changing the original MVC workflow.
Attention I did not test this code for each possible case, it was just done to understand the problem and to be able to post a meaningfull bug request. In our solution, we actually deactivated the sort possibility on this column because we didn't want to create ourselves a fork of the MVC project, we use only the official releases of MVC.
- WebGrid.cs: private string GetTableBodyHtml(IEnumerable<WebGridColumn> columns, string rowStyle, string alternatingRowStyle, string selectedRowStyle)
In this method the rows with data are retrieved from the datasource so that the underlying table can be build.
When accessing the property Rows, we are transferred to the file
- WebGridDataSource.cs: public IList<WebGridRow> GetRows(SortInfo sortInfo, int pageIndex).
- The creation of the sorting Linq expression is done in the method "private IQueryable<dynamic> Sort(IQueryable<dynamic> data, Type elementType, SortInfo sort)"
private IQueryable<dynamic> Sort(IQueryable<dynamic> data, Type elementType, SortInfo sort) {
Debug.Assert(data != null);
if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(elementType)) {
// IDynamicMetaObjectProvider properties are only available through a runtime binder, so we
// must build a custom LINQ expression for getting the dynamic property value.
// Lambda: o => o.Property (where Property is obtained by runtime binder)
// NOTE: lambda must not use internals otherwise this will fail in partial trust when Helpers assembly is in GAC
var binder = RB.Binder.GetMember(RB.CSharpBinderFlags.None, sort.SortColumn, typeof(WebGrid), new RB.CSharpArgumentInfo[] {
RB.CSharpArgumentInfo.Create(RB.CSharpArgumentInfoFlags.None, null) });
var param = Expression.Parameter(typeof(IDynamicMetaObjectProvider), "o");
var getter = Expression.Dynamic(binder, typeof(object), param);
return SortGenericExpression<IDynamicMetaObjectProvider, object>(data, getter, param, sort.SortDirection);
}
else {
// The IQueryable<dynamic> data source is cast as IQueryable<object> at runtime. We must call
// SortGenericExpression using reflection so that the LINQ expressions use the actual element type.
// Lambda: o => o.Property[.NavigationProperty,etc]
var param = Expression.Parameter(elementType, "o");
Expression member = param;
var type = elementType;
var sorts = sort.SortColumn.Split('.');
//**********************************************************************************************************************
List<Expression> conditionNullExpressions = new List<Expression>(sorts.Length);
Expression conditionNull = null;
//**********************************************************************************************************************
foreach (var name in sorts)
{
PropertyInfo prop = type.GetProperty(name);
if (prop == null) {
// no-op in case navigation property came from querystring (falls back to default sort)
if ((DefaultSort != null) && !sort.Equals(DefaultSort) && !String.IsNullOrEmpty(DefaultSort.SortColumn)) {
return Sort(data, elementType, DefaultSort);
}
return data;
}
member = Expression.Property(member, prop);
//**********************************************************************************************************************
// protect agains nulls
// see example: userQuery = userQuery.OrderBy(u => (u.Department != null) ? u.Department.Name : String.Empty
conditionNull = Expression.NotEqual(member, Expression.Constant(null));
conditionNullExpressions.Add(conditionNull);
//**********************************************************************************************************************
type = prop.PropertyType;
}
//**********************************************************************************************************************
// delete the last added condition on null, not needed because last property
if (conditionNull != null)
conditionNullExpressions.Remove(conditionNull);
if (conditionNullExpressions.Count > 0)
{
conditionNullExpressions.Reverse();
Expression buildCond = null;
foreach (Expression expression in conditionNullExpressions)
{
buildCond = Expression.Condition(expression, buildCond ?? member,
Expression.Constant("zzzzzzzzz"));
}
member = buildCond;
}
//**********************************************************************************************************************
MethodInfo m = this.GetType().GetMethod("SortGenericExpression", BindingFlags.Static | BindingFlags.NonPublic);
m = m.MakeGenericMethod(elementType, member.Type);
return (IQueryable<dynamic>)m.Invoke(null, new object[] { data, member, param, sort.SortDirection });
}
}
Regards,
PS: in attachment the WebGridDataSource codefile.
Comments: If the backing source for the WebGrid is an EntityFramework query, then the null propagation is dealt with by delegating it to the backing SQL database, so that the joins are outer, and the order-by is treating nulls (because of missing values in the joined temp table) in the default way the SQL engine is doing so. If the backing source is an in-memory collection as listed in the minimal repro on the previous comment, then the error shows up. A possible fix (which is somewhat more complex than the proposed solution, which for instance does not take structs into account) would make the in-memory case working, but would hurt the SQL backed case by either causing it to break, or to treat nulls in non-default way. Instead, I added an ability to set a custom sorter function for a given column, so that the user can provide a function that will do any required custom checks in the case of an in-memory collection backing the webgrid.
table Users -> has basic user info including a userid and a departmentid (int)
table Departments -> basic department info including deptid
"Not every user has a department"
var userQuery = from u in grp.Users
select u;
userQuery = userQuery.OrderBy(u => u.Department.Name);
If Department is equal to null, Linq will die with an null reference exception.
We can protect against that by testing on null:
userQuery = userQuery.OrderBy(u => (u.Department != null) ? u.Department.Name : String.Empty
The code used in the webgrid helper does not test on null, resulting for this specific case in a null reference exception.
The code sample underneath is a possible solution without to much changing the original MVC workflow.
Attention I did not test this code for each possible case, it was just done to understand the problem and to be able to post a meaningfull bug request. In our solution, we actually deactivated the sort possibility on this column because we didn't want to create ourselves a fork of the MVC project, we use only the official releases of MVC.
- WebGrid.cs: private string GetTableBodyHtml(IEnumerable<WebGridColumn> columns, string rowStyle, string alternatingRowStyle, string selectedRowStyle)
In this method the rows with data are retrieved from the datasource so that the underlying table can be build.
When accessing the property Rows, we are transferred to the file
- WebGridDataSource.cs: public IList<WebGridRow> GetRows(SortInfo sortInfo, int pageIndex).
- The creation of the sorting Linq expression is done in the method "private IQueryable<dynamic> Sort(IQueryable<dynamic> data, Type elementType, SortInfo sort)"
private IQueryable<dynamic> Sort(IQueryable<dynamic> data, Type elementType, SortInfo sort) {
Debug.Assert(data != null);
if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(elementType)) {
// IDynamicMetaObjectProvider properties are only available through a runtime binder, so we
// must build a custom LINQ expression for getting the dynamic property value.
// Lambda: o => o.Property (where Property is obtained by runtime binder)
// NOTE: lambda must not use internals otherwise this will fail in partial trust when Helpers assembly is in GAC
var binder = RB.Binder.GetMember(RB.CSharpBinderFlags.None, sort.SortColumn, typeof(WebGrid), new RB.CSharpArgumentInfo[] {
RB.CSharpArgumentInfo.Create(RB.CSharpArgumentInfoFlags.None, null) });
var param = Expression.Parameter(typeof(IDynamicMetaObjectProvider), "o");
var getter = Expression.Dynamic(binder, typeof(object), param);
return SortGenericExpression<IDynamicMetaObjectProvider, object>(data, getter, param, sort.SortDirection);
}
else {
// The IQueryable<dynamic> data source is cast as IQueryable<object> at runtime. We must call
// SortGenericExpression using reflection so that the LINQ expressions use the actual element type.
// Lambda: o => o.Property[.NavigationProperty,etc]
var param = Expression.Parameter(elementType, "o");
Expression member = param;
var type = elementType;
var sorts = sort.SortColumn.Split('.');
//**********************************************************************************************************************
List<Expression> conditionNullExpressions = new List<Expression>(sorts.Length);
Expression conditionNull = null;
//**********************************************************************************************************************
foreach (var name in sorts)
{
PropertyInfo prop = type.GetProperty(name);
if (prop == null) {
// no-op in case navigation property came from querystring (falls back to default sort)
if ((DefaultSort != null) && !sort.Equals(DefaultSort) && !String.IsNullOrEmpty(DefaultSort.SortColumn)) {
return Sort(data, elementType, DefaultSort);
}
return data;
}
member = Expression.Property(member, prop);
//**********************************************************************************************************************
// protect agains nulls
// see example: userQuery = userQuery.OrderBy(u => (u.Department != null) ? u.Department.Name : String.Empty
conditionNull = Expression.NotEqual(member, Expression.Constant(null));
conditionNullExpressions.Add(conditionNull);
//**********************************************************************************************************************
type = prop.PropertyType;
}
//**********************************************************************************************************************
// delete the last added condition on null, not needed because last property
if (conditionNull != null)
conditionNullExpressions.Remove(conditionNull);
if (conditionNullExpressions.Count > 0)
{
conditionNullExpressions.Reverse();
Expression buildCond = null;
foreach (Expression expression in conditionNullExpressions)
{
buildCond = Expression.Condition(expression, buildCond ?? member,
Expression.Constant("zzzzzzzzz"));
}
member = buildCond;
}
//**********************************************************************************************************************
MethodInfo m = this.GetType().GetMethod("SortGenericExpression", BindingFlags.Static | BindingFlags.NonPublic);
m = m.MakeGenericMethod(elementType, member.Type);
return (IQueryable<dynamic>)m.Invoke(null, new object[] { data, member, param, sort.SortDirection });
}
}
Regards,
PS: in attachment the WebGridDataSource codefile.
Comments: If the backing source for the WebGrid is an EntityFramework query, then the null propagation is dealt with by delegating it to the backing SQL database, so that the joins are outer, and the order-by is treating nulls (because of missing values in the joined temp table) in the default way the SQL engine is doing so. If the backing source is an in-memory collection as listed in the minimal repro on the previous comment, then the error shows up. A possible fix (which is somewhat more complex than the proposed solution, which for instance does not take structs into account) would make the in-memory case working, but would hurt the SQL backed case by either causing it to break, or to treat nulls in non-default way. Instead, I added an ability to set a custom sorter function for a given column, so that the user can provide a function that will do any required custom checks in the case of an in-memory collection backing the webgrid.