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.
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.