mercredi 20 décembre 2017

Custom server-side DataTables processing: "typeof(Enumerable).GetMethod" is null


I have been struggling for days over an old (2011) piece of C# code I extracted from a DLL my boss wants me to edit.

The application queries a database with LINQ, server-side processes it and displays the data with a DataTable. From what I gathered, the guy who wrote it had created an ASP.NET Web Forms Site in Visual Studio 2010, with .NET Framework 4.0.

He used the DataTables plugin along with the server-side parser from Zack Owens. In the project I recreated in VS 2017, everything builds well, but at runtime, a bug comes from the little customizations he made to the DataTable parser: among them, the SelectProperties() function has been completely rewritten from this:

private Expression<Func<T, List<string>>> SelectProperties
{
    get
    {
        return value => _properties.Select
        (
            // empty string is the default property value
            prop => (prop.GetValue(value, new object[0]) ?? string.Empty).ToString()
        )
        .ToList();
    }
}

to this:

private Expression<Func<T, List<string>>> SelectProperties
{
    get
    {
        var parameterExpression = Expression.Parameter(typeof(T), "value");

       // (Edited) The bug happens there: type_RD is null because GetMethod is not valid
        var type_RD = typeof(Enumerable).GetMethod(
            "ToList",
            new Type[] {
                typeof(IEnumerable<string>)
            }
        );

        // The programs crashes there (System.Null.Exception)
        var methodFromHandle = (MethodInfo)MethodBase.GetMethodFromHandle(
            type_RD
        .MethodHandle);

        var expressionArray = new Expression[1];

        var methodInfo = (MethodInfo)MethodBase.GetMethodFromHandle(
            typeof(Enumerable).GetMethod("Select", new Type[] {
                typeof(IEnumerable<PropertyInfo>),
                typeof(Func<PropertyInfo, string>)
            })
        .MethodHandle);

        var expressionArray1 = new Expression[] {
            Expression.Field(
                Expression.Constant(
                    this,
                    typeof(DataTableParser<T>)
                ),
                FieldInfo.GetFieldFromHandle(
                    typeof(DataTableParser<T>).GetField("_properties").FieldHandle,
                    typeof(DataTableParser<T>).TypeHandle
                )
            ), null
        };

        var parameterExpression1 = Expression.Parameter(
            typeof(PropertyInfo),
            "prop"
        );

        var methodFromHandle1 = (MethodInfo)MethodBase.GetMethodFromHandle(
            typeof(PropertyInfo).GetMethod(
                "GetValue",
                new Type[] {
                    typeof(object),
                    typeof(object[])
                }
            )
        .MethodHandle);

        var expressionArray2 = new Expression[] {
            Expression.Convert(
                parameterExpression,
                typeof(object)
            ),
            Expression.NewArrayInit(
                typeof(object),
                new Expression[0]
            )
        };

        var methodCallExpression = Expression.Call(
            Expression.Coalesce(
                Expression.Call(
                    parameterExpression1,
                    methodFromHandle1,
                    expressionArray2
                ),
                Expression.Field(
                    null,
                    FieldInfo.GetFieldFromHandle(
                        typeof(string).GetField("Empty").FieldHandle
                    )
                )
            ),
            (MethodInfo)MethodBase.GetMethodFromHandle(
                typeof(object).GetMethod("ToString").MethodHandle
            ),
            new Expression[0]
        );

        expressionArray1[1] = Expression.Lambda<Func<PropertyInfo, string>>(
            methodCallExpression,
            parameterExpression1
        );
        expressionArray[0] = Expression.Call(
            null,
            methodInfo,
            expressionArray1
        );

        // Return Lambda
        return Expression.Lambda<Func<T, List<string>>>(
            Expression.Call(
                null,
                methodFromHandle,
                expressionArray
            ),
            parameterExpression
        );
    }
}

My questions:

  • How to make this SelectProperties function work?
  • What exactly is its purpose, compared to the original one? I didn't get any of the "MethodHandle" bits ...

The only hint I have is that, when I use the original SelectProperties code, the data are in the wrong columns and the sorting causes errors 500 with some of the columns. Here is the full code of this custom DataTable.cs parser:
Tip: look for the (Edited) tags

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;

// (Edited) Source : Zack Owens commented http://ift.tt/2ks1VOY

/* (Edited) Adapting to custom namespace
namespace DataTables
*/
namespace MyApp.Web.WebServices.Utils
{
    /// <summary>
    /// Parses the request values from a query from the DataTables jQuery plugin
    /// </summary>
    /// <typeparam name="T">List data type</typeparam>
    public class DataTableParser<T>
    {
        /*
         * int: iDisplayStart - Display start point
        * int: iDisplayLength - Number of records to display
        * string: string: sSearch - Global search field
        * boolean: bEscapeRegex - Global search is regex or not
        * int: iColumns - Number of columns being displayed (useful for getting individual column search info)
        * string: sSortable_(int) - Indicator for if a column is flagged as sortable or not on the client-side
        * string: sSearchable_(int) - Indicator for if a column is flagged as searchable or not on the client-side
        * string: sSearch_(int) - Individual column filter
        * boolean: bEscapeRegex_(int) - Individual column filter is regex or not
        * int: iSortingCols - Number of columns to sort on
        * int: iSortCol_(int) - Column being sorted on (you will need to decode this number for your database)
        * string: sSortDir_(int) - Direction to be sorted - "desc" or "asc". Note that the prefix for this variable is wrong in 1.5.x, but left for backward compatibility)
        * string: sEcho - Information for DataTables to use for rendering
         */

        private const string INDIVIDUAL_SEARCH_KEY_PREFIX = "sSearch_";
        private const string INDIVIDUAL_SORT_KEY_PREFIX = "iSortCol_";
        private const string INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX = "sSortDir_";
        private const string DISPLAY_START = "iDisplayStart";
        private const string DISPLAY_LENGTH = "iDisplayLength";
        private const string ECHO = "sEcho";
        private const string ASCENDING_SORT = "asc";
        private const string OBJECT_DATA_PREFIX = "mDataProp_";

        private IQueryable<T> _queriable;
        private readonly HttpRequestBase _httpRequest;
        private readonly Type _type;
        private readonly PropertyInfo[] _properties;

        public DataTableParser(HttpRequestBase httpRequest, IQueryable<T> queriable)
        {
            _queriable = queriable;
            _httpRequest = httpRequest;
            _type = typeof(T);
            _properties = _type.GetProperties();
        }

        public DataTableParser(HttpRequest httpRequest, IQueryable<T> queriable)
            : this(new HttpRequestWrapper(httpRequest), queriable) { }

        /// <summary>
        /// Parses the <see cref="HttpRequestBase"/> parameter values for the accepted 
        /// DataTable request values
        /// </summary>
        /// <returns>Formated output for DataTables, which should be serialized to JSON</returns>
        /// <example>
        ///     In an ASP.NET MVC from a controller, you can call the Json method and return this result.
        ///     
        ///     public ActionResult List()
        ///     {
        ///         // change the following line per your data configuration
        ///         IQueriable<User> users = datastore.Linq();
        ///         
        ///         if (Request["sEcho"] != null) // always test to see if the request is from DataTables
        ///         {
        ///             var parser = new DataTableParser<User>(Request, users);
        ///             return Json(parser.Parse());
        ///         }
        ///         return Json(_itemController.CachedValue);
        ///     }
        ///     
        ///     If you're not using MVC, you can create a web service and write the JSON output as such:
        ///     
        ///     using System.Web.Script.Serialization;
        ///     public class MyWebservice : System.Web.Services.WebService
        ///     {
        ///         public string MyMethod()
        ///         {
        ///             // change the following line per your data configuration
        ///             IQueriable<User> users = datastore.Linq();
        ///             
        ///             response.ContentType = "application/json";
        ///             
        ///             JavaScriptSerializer serializer = new JavaScriptSerializer();
        ///             var parser = new DataTableParser<User>(Request, users);
        ///             return new JavaScriptSerializer().Serialize(parser.Parse());
        ///         }
        ///     }
        /// </example>
        public FormatedList<T> Parse()
        {
            var list = new FormatedList();
            list.Import(_properties.Select(x => x.Name).ToArray());

            list.sEcho = int.Parse(_httpRequest[ECHO]);

            list.iTotalRecords = _queriable.Count();

            ApplySort();

            int skip = 0, take = 10;
            int.TryParse(_httpRequest[DISPLAY_START], out skip);
            int.TryParse(_httpRequest[DISPLAY_LENGTH], out take);

            /* (Edited) This new syntax works well
            list.aaData = _queriable.Where(ApplyGenericSearch)
                                    .Where(IndividualPropertySearch)
                                    .Skip(skip)
                                    .Take(take)
                                    .Select(SelectProperties)
                                    .ToList();

            list.iTotalDisplayRecords = list.aaData.Count;
            */
            list.aaData = _queriable.Where(ApplyGenericSearch)
                                    .Where(IndividualPropertySearch)
                                    .Skip(skip)
                                    .Take(take)
                                    .ToList()
                                    .AsQueryable()
                                    .Select(SelectProperties)
                                    .ToList();

            list.iTotalDisplayRecords = list.iTotalRecords;

            return list;
        }

        private void ApplySort()
        {
            // (Edited) Added one line, see after
            bool firstSort = true;
            foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SORT_KEY_PREFIX)))
            {
                int sortcolumn = int.Parse(_httpRequest[key]);
                if (sortcolumn < 0 || sortcolumn >= _properties.Length)
                    break;

                string sortdir = _httpRequest[INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX + key.Replace(INDIVIDUAL_SORT_KEY_PREFIX, string.Empty)];

                var paramExpr = Expression.Parameter(typeof(T), "val");

                /* Edited as per http://ift.tt/2kPUpNd and mentioned here too http://ift.tt/2krIKVz
                var propertyExpr = Expression.Lambda<Func<T, object>>(Expression.Property(paramExpr, _properties[sortcolumn]), paramExpr);
                */
                var expression = Expression.Convert(Expression.Property(paramExpr, _properties[sortcolumn]),typeof(object));
                var propertyExpr = Expression.Lambda<Func<T, object>>(expression, paramExpr);

                /* Edited cf. http://ift.tt/2krIKVz
                 * Correcting multi-sort errors
                if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
                    _queriable = _queriable.OrderBy(propertyExpr);
                else
                    _queriable = _queriable.OrderByDescending(propertyExpr);
                 */
                if (firstSort)
                {
                    if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
                        _queriable = _queriable.OrderBy(propertyExpr);
                    else
                        _queriable = _queriable.OrderByDescending(propertyExpr);

                    firstSort = false;
                }
                else
                {
                    if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(ASCENDING_SORT, StringComparison.OrdinalIgnoreCase))
                        _queriable = ((IOrderedQueryable<T>)_queriable).ThenBy(propertyExpr);
                    else
                        _queriable = ((IOrderedQueryable<T>)_queriable).ThenByDescending(propertyExpr);
                }
            }
        }

        /// <summary>
        /// Expression that returns a list of string values, which correspond to the values
        /// of each property in the list type
        /// </summary>
        /// <remarks>This implementation does not allow indexers</remarks>
        private Expression<Func<T, List<string>>> SelectProperties
        {
            get
            {
                /* (Edited) This is the edit that does not work and that I don't understand
                return value => _properties.Select
                (
                    // empty string is the default property value
                    prop => (prop.GetValue(value, new object[0]) ?? string.Empty).ToString()
                )
               .ToList();
               */
                var parameterExpression = Expression.Parameter(typeof(T), "value");

               // (Edited) The bug happens there: type_RD is null because GetMethod is not valid
                var type_RD = typeof(Enumerable).GetMethod(
                    "ToList",
                    new Type[] {
                        typeof(IEnumerable<string>)
                    }
                );

                // The programs crashes there (System.Null.Exception)
                var methodFromHandle = (MethodInfo)MethodBase.GetMethodFromHandle(
                    type_RD
                .MethodHandle);

                var expressionArray = new Expression[1];

                var methodInfo = (MethodInfo)MethodBase.GetMethodFromHandle(
                    typeof(Enumerable).GetMethod("Select", new Type[] {
                        typeof(IEnumerable<PropertyInfo>),
                        typeof(Func<PropertyInfo, string>)
                    })
                .MethodHandle);

                var expressionArray1 = new Expression[] {
                    Expression.Field(
                        Expression.Constant(
                            this,
                            typeof(DataTableParser<T>)
                        ),
                        FieldInfo.GetFieldFromHandle(
                            typeof(DataTableParser<T>).GetField("_properties").FieldHandle,
                            typeof(DataTableParser<T>).TypeHandle
                        )
                    ), null
                };

                var parameterExpression1 = Expression.Parameter(
                    typeof(PropertyInfo),
                    "prop"
                );

                var methodFromHandle1 = (MethodInfo)MethodBase.GetMethodFromHandle(
                    typeof(PropertyInfo).GetMethod(
                        "GetValue",
                        new Type[] {
                            typeof(object),
                            typeof(object[])
                        }
                    )
                .MethodHandle);

                var expressionArray2 = new Expression[] {
                    Expression.Convert(
                        parameterExpression,
                        typeof(object)
                    ),
                    Expression.NewArrayInit(
                        typeof(object),
                        new Expression[0]
                    )
                };

                var methodCallExpression = Expression.Call(
                    Expression.Coalesce(
                        Expression.Call(
                            parameterExpression1,
                            methodFromHandle1,
                            expressionArray2
                        ),
                        Expression.Field(
                            null,
                            FieldInfo.GetFieldFromHandle(
                                typeof(string).GetField("Empty").FieldHandle
                            )
                        )
                    ),
                    (MethodInfo)MethodBase.GetMethodFromHandle(
                        typeof(object).GetMethod("ToString").MethodHandle
                    ),
                    new Expression[0]
                );

                expressionArray1[1] = Expression.Lambda<Func<PropertyInfo, string>>(
                    methodCallExpression,
                    parameterExpression1
                );
                expressionArray[0] = Expression.Call(
                    null,
                    methodInfo,
                    expressionArray1
                );

                // Return Lambda
                return Expression.Lambda<Func<T, List<string>>>(
                    Expression.Call(
                        null,
                        methodFromHandle,
                        expressionArray
                    ),
                    parameterExpression
                );
            }
        }

        /// <summary>
        /// Compound predicate expression with the individual search predicates that will filter the results
        /// per an individual column
        /// </summary>
        private Expression<Func<T, bool>> IndividualPropertySearch
        {
            get
            {
                var paramExpr = Expression.Parameter(typeof(T), "val");
                Expression whereExpr = Expression.Constant(true); // default is val => True
                List<Expression> le = new List<Expression>() { whereExpr };
                List<ParameterExpression> lp = new List<ParameterExpression>() { paramExpr };

                foreach (string key in _httpRequest.Params.AllKeys.Where(x => x.StartsWith(INDIVIDUAL_SEARCH_KEY_PREFIX)))
                {
                    var mDataProp = key.Replace(INDIVIDUAL_SEARCH_KEY_PREFIX, OBJECT_DATA_PREFIX);
                    if (string.IsNullOrEmpty(_httpRequest[key]) || string.IsNullOrEmpty(_httpRequest[mDataProp]))
                    {
                        continue; // ignore if the option is invalid
                    }
                    var f = _properties.First(p => p.Name == _httpRequest[mDataProp]);
                    string query = _httpRequest[key].ToLower();

                    MethodCallExpression mce;
                    if (f.PropertyType != typeof(string))
                    {
                        // val.{PropertyName}.ToString().ToLower().Contains({query})
                        mce = Expression.Call(Expression.Call(Expression.Property(paramExpr, f), "ToString", new Type[0]), typeof(string).GetMethod("ToLower", new Type[0]));
                    }
                    else
                    {
                        mce = Expression.Call(Expression.Property(paramExpr, f), typeof(string).GetMethod("ToLower", new Type[0]));
                    }

                    // reset where expression to also require the current constraint
                    whereExpr = Expression.And(whereExpr, Expression.Call(mce, typeof(string).GetMethod("Contains"), Expression.Constant(query)));
                    le.Add(whereExpr);
                }

                var agg = le.Aggregate((prev, next) => Expression.And(prev, next));
                return Expression.Lambda<Func<T, bool>>(agg, paramExpr);
            }
        }

        /// <summary>
        /// Expression for an all column search, which will filter the result based on this criterion
        /// </summary>
        private Expression<Func<T, bool>> ApplyGenericSearch
        {
            get
            {
                string search = _httpRequest["sSearch"];

                // default value
                if (string.IsNullOrEmpty(search) || _properties.Length == 0)
                    return x => true;

                // invariant expressions
                var searchExpression = Expression.Constant(search.ToLower());
                var paramExpression = Expression.Parameter(typeof(T), "val");

                // query all properties and returns a Contains call expression 
                // from the ToString().ToLower()
                var propertyQuery = (from property in _properties
                                     let tostringcall = Expression.Call(
                                                         Expression.Call(
                                                             Expression.Property(paramExpression, property), "ToString", new Type[0]),
                                                             typeof(string).GetMethod("ToLower", new Type[0]))
                                     select Expression.Call(tostringcall, typeof(string).GetMethod("Contains"), searchExpression)).ToArray();

                // we now need to compound the expression by starting with the first
                // expression and build through the iterator
                Expression compoundExpression = propertyQuery[0];

                // add the other expressions
                for (int i = 1; i < propertyQuery.Length; i++)
                    compoundExpression = Expression.Or(compoundExpression, propertyQuery[i]);

                // compile the expression into a lambda 
                return Expression.Lambda<Func<T, bool>>(compoundExpression, paramExpression);
            }
        }
    }

    public class FormatedList<T>
    {
        public FormatedList()
        {
        }

        public int sEcho { get; set; }
        public int iTotalRecords { get; set; }
        public int iTotalDisplayRecords { get; set; }
        public List<T> aaData { get; set; }
        public string sColumns { get; set; }

        public void Import(string[] properties)
        {
            sColumns = string.Empty;
            for (int i = 0; i < properties.Length; i++)
            {
                sColumns += properties[i];
                if (i < properties.Length - 1)
                    sColumns += ",";
            }
        }
    }
}

I am a beginner at C#, .NET, Linq etc., though I know quite a few other languages.
Setup: Windows 7 64, Visual Studio 2017.

Thank you for your help!





Aucun commentaire:

Enregistrer un commentaire