Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

2016-12-26

Castle Windsor Part 2 - Array configuration, dictionary configuration

Xem original

Part 2 – Array Configuration
Part 3 – Dictionary configuration

Array configuration

Đầu tiên là config array.

Ví dụ holiday service có code như sau:
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
using System;

namespace ConsoleApp
{
    public class HolidayService
    {
        private DateTime[] holidays;

        public DateTime[] Holidays
        {
            get { return holidays; }
            set { holidays = value; }
        }

        public bool IsHoliday(DateTime date)
        {
            if (holidays != null)
            {
                DateTime matchDate = date.Date;
                foreach (DateTime dt in Holidays)
                {
                    if (dt.Date.Equals(matchDate))
                    {
                        return true;
                    }
                }
            }

            return false;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            WindsorContainer container = new WindsorContainer(new XmlInterpreter());

            HolidayService holidayService = container.Resolve<HolidayService>();

            DateTime xmas = new DateTime(2016, 12, 25);
            DateTime newYears = new DateTime(2017, 1, 1);

            if (holidayService.IsHoliday(xmas))
            {
                Console.WriteLine("Merry X'mas!");
            }
            else
            {
                Console.WriteLine("X'mas is only for management!");
            }

            if (holidayService.IsHoliday(newYears))
            {
                Console.WriteLine("Happy new year!");
            }
            else
            {
                Console.WriteLine("New year, you haven't done all the work for last year!");
            }

            Console.ReadLine();
        }
    }
}
Config trong App.config như sau:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component type="ConsoleApp.HolidayService, ConsoleApp">
        <parameters>
          <holidays>
            <array>
              <item>2016-12-24</item>
              <item>2016-12-25</item>
              <item>2017-1-1</item>
            </array>
          </holidays>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Có thể dùng <Holidays> hoặc <holidays> đều được vì Windsor đủ smart để inject. Nếu muốn resolve trực tiếp ra IList (hoặc IList, IEnumerable ... nói chung là generic collection)
static void Main(string[] args)
{
    WindsorContainer container = new WindsorContainer(new XmlInterpreter());
    var holidays = container.Resolve<IList<DateTime>>("holidays");
    Console.WriteLine(string.Join("\r\n", holidays));
    Console.ReadLine();
}
File config tương ứng như sau:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component id="holidays" type="System.Collections.Generic.List`1[System.DateTime]">
        <parameters>
          <collection>
            <array>
              <item>2016-12-24</item>
              <item>2016-12-25</item>
              <item>2017-1-1</item>
            </array>
          </collection>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>

Cần nhớ ở dạng config này là thực hiện theo <parameters><collection><array><item>. Nếu đầy đủ hơn thì type chỉ định có cả assembly là <component id="holidays" type="System.Collections.Generic.List`1[[System.DateTime, mscorlib]], mscorlib">.

Dictionary configuration

Tương tự như array, dictionary configuration thực hiện với <parameters><dictionary><dictionary><entry>. Ví dụ AliasService như sau:
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
using System;
using System.Collections.Generic;

namespace ConsoleApp
{
    public class AliasService
    {
        private Dictionary<string, string> dict;

        public Dictionary<string, string> Aliases
        {
            get { return dict; }
            set { dict = value; }
        }

        public string Evaluate(string term)
        {
            if (dict == null)
            {
                return term;
            }

            while (dict.ContainsKey(term))
            {
                term = dict[term];
            }

            return term;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            WindsorContainer container = new WindsorContainer(new XmlInterpreter());

            AliasService aliasService = container.Resolve<AliasService>();
            string sentence = "A dog ate my homework";

            foreach (string word in sentence.Split(new char[] { ' ' }, 
                StringSplitOptions.RemoveEmptyEntries))
            {
                Console.Write("{0} ", aliasService.Evaluate(word));
            }

            Console.ReadLine();
        }
    }
}
App.config cấu hình dictionary:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component type="ConsoleApp.AliasService, ConsoleApp">
        <parameters>
          <Aliases>
            <dictionary>
              <entry key="dog">duck</entry>
              <entry key="ate">broke</entry>
              <entry key="homework">code</entry>
            </dictionary>
          </Aliases>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Để resolve trực tiếp ra IDictionary
static void Main(string[] args)
{
    WindsorContainer container = new WindsorContainer(new XmlInterpreter());
    var states = container.Resolve<IDictionary<string, string>>("states");
    Console.WriteLine(string.Join("\r\n", states.Keys));
    Console.ReadLine();
}
File config tương ứng như sau:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component id="states" type="System.Collections.Generic.Dictionary`2[System.String, System.String]">
        <parameters>
          <dictionary>
            <dictionary>
              <entry key="VN-CT">Cần Thơ</entry>
              <entry key="VN-DN">Đà Nẵng</entry>
              <entry key="VN-HN">Hà Nội</entry>
              <entry key="VN-HP">Hải Phòng</entry>
              <entry key="VN-SG">Hồ Chí Minh</entry>
            </dictionary>
          </dictionary>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Tới đây là xong part 2. Về type convertor có thể tham khảo thêm phần Configuration with type converters trong series của Mike Hadlow '10 Advanced Windsor tricks'.

Container tutorials ... example with Castle Windsor. Part 1 - Configuration parameters.

Mình dùng Castle Windsor từ lâu. Cũng có note nhưng rồi để đâu mất. Mình đã tính viết 1 series làm sao đủ để xài Windsor từ cái thời còn dùng CAB/SCSF, sau đó là Prism nhưng rồi chẳng biết sao lúc đó bỏ nửa chừng.

Inversion of Control (IoC), Dependency Inversion Principle (DIP) và Dependency Injection (DI), CAB&SCSF cũng chỉ viết tới Part 03: Dependency Injection

Gần đây coi lại và thấy lần này nên note kỹ. Nói chung đầu tiên là lược dịch từ những tutorials trên NET bắt đầu từ series trên BitterCoder (đã rất lâu rồi từ tận 2007). Có thể đọc thêm hướng dẫn từ project chính thức trên Github castleproject/Windsor.

Gần đây có nhiều lựa chọn IoC container cho .NET gồm có Autofac, StructureMap, Unity. Một vài container có vẻ dần chiếm được nhiều quan tâm hơn như Autofac với simple API, dễ sử dụng và performance tốt. Tuy nhiên Windsor nói chung vẫn đáp ứng được yêu cầu đặt ra với nhiều module (facilities) từ logging, NHibernate, ASP.NET MVC ...

Configuration parameters

Part đầu tiên là configuration parameters. Windsor cho phép cấu hình component với parameters run-time. Mặc dù có thể configuration với .NET qua app.config bình thường nhưng sử dụng với Windsor khá là đơn giản và tiện dụng.

Tạo một Project ConsoleApp, dùng NuGet install Castle Windsor. Tạo file app.config.


Giả sử có một class là Tax, mặc định tax rate là 10%. Có thể config rate thông qua app.config.
public class Tax
{
    private decimal rate = 0.10m;

    public decimal Rate
    {
        set { rate = value; }
        get { return rate; }
    }

    public decimal Calculate(decimal gross)
    {
        return Math.Round(rate * gross, 2);
    }
}
Sau khi tạo class Tax, thực hiện dùng với Windsor như sau:
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
using System;

namespace ConsoleApp
{
    public class Tax
    {
        private decimal rate = 0.10m;

        public decimal Rate
        {
            set { rate = value; }
            get { return rate; }
        }

        public decimal Calculate(decimal gross)
        {
            return Math.Round(rate * gross, 2);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            WindsorContainer container = new WindsorContainer(new XmlInterpreter());

            // Resolve
            Tax calculator = container.Resolve<Tax>();

            decimal gross = 100;
            decimal tax = calculator.Calculate(gross);

            Console.WriteLine("Gross: {0}, Tax: {1}", gross, tax);
            Console.ReadLine();
        }
    }
}
Container được tạo với XmlInterpreter() sẽ đọc configuration từ file app.config (hoặc web.config với web app), instance calculator được tạo bởi container thông qua Resolve().

Thực hiện Build và Run sẽ ra báo lỗi không có config section 'castle'.
Thực hiện thêm config section vào App.config. Giả sử App.config không có cấu hình component sẽ gây exception ComponentNotFoundException
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Thực hiện config component nhưng không set tax rate như sau
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component id="tax" type="ConsoleApp.Tax, ConsoleApp">
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Type có thể không cần dùng AssemblyQualifiedName mà để đơn giản là Tax hoặc ConsoleApp.Tax cũng OK. Kết quả trả ra 'Gross: 100, Tax: 10.00'.

Thực hiện setup rate bằng parameters như sau:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component id="tax" type="ConsoleApp.Tax, ConsoleApp">
        <parameters>
          <rate>0.25</rate>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
Kết quả trả ra 'Gross: 100, Tax: 25.00'.

Với id="tax" có thể dùng để resolve component khác nhau, ví dụ:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
  </configSections>
  <castle>
    <components>
      <component id="tax1" type="ConsoleApp.Tax, ConsoleApp">
        <parameters>
          <rate>0.25</rate>
        </parameters>
      </component>
      <component id="tax2" type="ConsoleApp.Tax, ConsoleApp">
        <parameters>
          <rate>0.05</rate>
        </parameters>
      </component>
    </components>
  </castle>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
  </startup>
</configuration>
và code
WindsorContainer container = new WindsorContainer(new XmlInterpreter());
decimal gross = 100;

// Resolve tax #1
Tax calculator1 = container.Resolve<Tax>("tax1");
decimal tax1 = calculator1.Calculate(gross);
Console.WriteLine("Gross: {0}, Tax: {1}", gross, tax1);

// Resolve tax #2
Tax calculator2 = container.Resolve<Tax>("tax2");
decimal tax2 = calculator2.Calculate(gross);
Console.WriteLine("Gross: {0}, Tax: {1}", gross, tax2);

Console.ReadLine();
Như vậy coi như là đủ cho part 1. Phần tiếp theo sẽ thực hiện config arrays, dictionary ...

2016-05-19

Using jQuery DataTables with ASP.NET and NHibernate, QueryOver


Mình viết lại cái server-side data của DataTables 1.10+ cho ASP.NET MVC.

Đã có rất nhiều trên NuGet để có thể ngó qua tham khảo. Ví dụ:
https://github.com/ALMMa/datatables.aspnet
https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables

Nhưng mình thấy rằng cũng ko cần support nhiều thứ như vậy, chủ yếu là ModelBinder để request thôi, còn lại putput JSON thì tự do. Cho nên modify lại chút, lấy những cái cần xài thôi.

Việc còn lại là query data theo cái DataTablesRequest thì dùng QueryOver là tiện nhất. Nếu cần biết về QueryOver thì series của Andrew Whitaker là nên đọc
http://blog.andrewawhitaker.com/blog/2014/03/12/queryover-series-part-1-why-queryover/

Request

OK code thôi. Lười không tạo project trên GitHub nên post code thẳng lên đây. Giả sử code query Project, nếu cần thì có thể refactor đoạn này thành DataTables<T>Đầu tiên DataTablesRequest là request gửi AJAX lên và đã parse bởi ModelBinder. Cái này sẽ nói sau. Request có các thông tin:

columns[i][data]    
columns[i][name]    
columns[i][orderable]
columns[i][search][regex]
columns[i][search][value]   
columns[i][searchable]  
...     
draw    
length  
order[i][column]    
order[i][dir]
... 
search[regex]
search[value]   
start

Column name

Mình điều chỉ lại column name cần lấy từ column data và column name trong request gửi lên. Nếu không name mà có data thì xài Inflector chuyển dạng Pascal case ví dụ start_date thành StartDate.
Nếu có column data nhưng là số thường là những column như column số thứ tự, column action (Edit, View, Delete) thì bỏ qua.

Sorting

Dùng 

query.UnderlyingCriteria.AddOrder(new Order(column.Name, sort.Direction == 1));

Searching

Với search không phải regular expression thì dùng Restrictions

query.Where(Restrictions.InsensitiveLike(column.Name, val, MatchMode.Anywhere));

Trường hợp regular expression có thể viết thêm extension cho MS SQL Server, tạm thời không giải quyết ở đây.

Select list

Để select các column sẽ dùng SelectList(), tuy nhiên cần phần AliasToBean nên phải dùng WithAlias. Còn nếu không cần thì mình có thể dùng ngay IList<object>

public static class MemberExpressionBuilder
{
    public static Expression<Func<T>> Create<TModel, T>(string propertyName)
    {
        var propertyInfo = typeof(TModel).GetProperty(propertyName);

        var entityParam = Expression.Parameter(typeof(TModel), "e");
        Expression columnExpr = Expression.Property(entityParam, propertyInfo);

        if (propertyInfo.PropertyType != typeof(T))
        {
            columnExpr = Expression.Convert(columnExpr, typeof(T));
        }

        return Expression.Lambda<Func<T>>(columnExpr);
    }
}

Sau khi có data xong thì transform data theo JSON property name yêu cầu. Dùng JsonProperty cho property như draw, recordsTotal, recordsFiltered...

Code của controller

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/// <summary>
/// GET: /Projects/DataTable
/// </summary>
[HttpPost]
[Route("Projects/DataTables")]
public ActionResult DataTables(DataTablesRequest filterRequest)
{
 //if (!Request.IsAjaxRequest())
 //{
 //    ViewBag.Message = "Not Supported";
 //    return View();
 //}

 var query = NHibernateSession.Current.QueryOver<Project>();

 foreach (var column in filterRequest.Columns.Values)
 {

  if (string.IsNullOrEmpty(column.Name) &&
   !string.IsNullOrEmpty(column.Data))
  {
   // If data is number, maybe it is compute column?
   int i;
   if (int.TryParse(column.Data, out i))
   {
    continue;
   }

   // Use reflector from data
   column.Name = column.Data.Pascalize();
  }
 }

 // Order by
 if (filterRequest.Sort.Count > 0)
 {
  foreach (var sort in filterRequest.Sort)
  {
   var column = filterRequest.Columns[sort.Column];
   if (!column.Sortable ||
    string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   query.UnderlyingCriteria.AddOrder(
    new Order(column.Name, sort.Direction == 1));
  }
 }

 // Apply search
 if (filterRequest.Search != null &&
  !string.IsNullOrWhiteSpace(filterRequest.Search.Value))
 {
  foreach (var column in filterRequest.Columns.Values)
  {
   if (!column.Searchable ||
    string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   var val = filterRequest.Search.Value;
   if (filterRequest.Search.IsRegex)
   {

   }
   else
   {
    query.Where(Restrictions.InsensitiveLike(
     column.Name, val, MatchMode.Anywhere));
   }
  }
 }

 var countQuery = query.ToRowCountQuery();
 var totalCount = countQuery.FutureValue<int>();

 Project project = null;
 query.SelectList(
  list =>
  {
   list = list.Select(x => x.Id).WithAlias(() => project.Id);

   foreach (var column in filterRequest.Columns.Values)
   {
    if (string.IsNullOrEmpty(column.Name) ||
     string.IsNullOrEmpty(column.Data))
    {
     continue;
    }

    list = list.Select(Projections.Property(column.Name))
     .WithAlias(MemberExpressionBuilder
      .Create<Project, object>(column.Name));
   }

   return list;
  }
 ).TransformUsing(Transformers.AliasToBean<Project>())
  .Skip(filterRequest.Start);

 if (filterRequest.Length > 0)
 {
  query.Take(filterRequest.Length);
 }

 var results = query.List<Project>() ?? new List<Project>();

 var dataSource = results.ToArray();
 var response = new DataTablesResponse<Project>(filterRequest.Draw, totalCount.Value,
  dataSource.Length, dataSource);

 var transformResponse = response.Transform<Project, object>(x =>
 {
  var row = new Dictionary<string, object>();

  row.Add("id", x.Id);
  foreach (var column in filterRequest.Columns.Values)
  {
   if (string.IsNullOrEmpty(column.Name) ||
    string.IsNullOrEmpty(column.Data))
   {
    continue;
   }

   // Get property value
   var propertyInfo = typeof(Project).GetProperty(column.Name);
   object propertyValue = null;
   if (propertyInfo != null)
   {
    propertyValue = propertyInfo.GetValue(x, null);
   }

   if (!row.ContainsKey(column.Data))
   {
    row.Add(column.Data, propertyValue);
   }
  }

  return row;
 });

 string serialized = JsonConvert.SerializeObject(
  transformResponse, Formatting.Indented);

 Response.ContentEncoding = Encoding.UTF8;
 string contentType = "application/json; charset=utf-8";

 var bytes = Encoding.UTF8.GetBytes(serialized);
 return new FileContentResult(bytes, contentType);
}

DataTables request

Các interface class dùng parse request.

IColumn

/// <summary>
/// Columns' parameters for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IColumn
{
 /// <summary>
 /// Column's data source.
 /// </summary>
 string Data { get; set; }

 /// <summary>
 /// Column's name.
 /// </summary>
 string Name { get; set; }

 /// <summary>
 /// Flag to indicate if this column is searchable (true) or not (false). 
 /// </summary>
 bool Searchable { get; set; }

 /// <summary>
 /// Flag to indicate if this column is sortable (true) or not (false).
 /// </summary>
 bool Sortable { get; set; }

 /// <summary>
 /// The search component for the column.
 /// </summary>
 ISearch Search { get; set; }
}


ISearch

/// <summary>
/// Search configuration for columns of a filter request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface ISearch
{
 /// <summary>
 /// Search value to apply to this specific column.
 /// </summary>
 string Value { get; set; }

 /// <summary>
 /// Flag to indicate if the search term for this column should be treated as regular expression (true) or not (false). As with global search, normally server-side processing scripts will not perform regular expression searching for performance reasons on large data sets, but it is technically possible and at the discretion of your script.
 /// </summary>
 bool IsRegex { get; set; }
}


ISort

/// <summary>
/// Sort configuration for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface ISort
{
 /// <summary>
 /// Column to which ordering should be applied. This is an index 
 /// reference to the columns array of information that is also submitted to the server.
 /// </summary>
 int Column { get; set; }

 /// <summary>
 /// Ordering direction for this column. It will be ASC or DESC to 
 /// indicate ascending ordering or descending ordering, respectively.
 /// </summary>
 int Direction { get; set; }
}


IDataTablesRequest

/// <summary>
/// Filter request parameters
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IDataTablesRequest
{
 /// <summary>
 /// Draw counter. This is used by DataTables to ensure that the AJAX 
 /// returns from server-side processing requests are drawn in sequence by DataTables 
 /// (AJAX requests are asynchronous and thus can return out of sequence). 
 /// </summary>
 int Draw { get; set; }

 /// <summary>
 /// Paging first record indicator. This is the start point in the current data set 
 /// (0 index based - i.e. 0 is the first record).
 /// </summary>
 int Start { get; set; }

 /// <summary>
 /// Number of records that the table can display in the current draw. 
 /// It is expected that the number of records returned will be equal to 
 /// this number, unless the server has fewer records to return. 
 /// Note that this can be -1 to indicate that all records should be returned 
 /// (although that negates any benefits of server-side processing!)
 /// </summary>
 int Length { get; set; }

 /// <summary>
 /// Global search component to be applied to all columns which have searchable as true.
 /// </summary>
 ISearch Search { get; set; }

 /// <summary>
 /// The columns' sorting configuration.
 /// </summary>
 List<ISort> Sort { get; set; }

 /// <summary>
 /// The columns collection.
 /// </summary>
 Dictionary<int, IColumn> Columns { get; set; }
}


IDataTablesResponse

/// <summary>
/// Mutable page response
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public interface IDataTablesResponse
{
 /// <summary>
 /// The draw counter that this object is a response to - 
 /// from the draw parameter sent as part of the data request.
 /// </summary>
 int Draw { get; set; }

 /// <summary>
 /// Total records, before filtering (i.e. the total number of records in the database).
 /// </summary>
 int TotalRecords { get; set; }

 /// <summary>
 /// Total records, after filtering (i.e. the total number of records 
 /// after filtering has been applied - not just the number of records being returned for this page of data).
 /// </summary>
 int TotalFilteredRecords { get; set; }

 /// <summary>
 /// The data to be displayed in the table. This is an array of data 
 /// source objects, one for each row, which will be used by DataTables. 
 /// Note that this parameter's name can be changed using the ajaxDT option's dataSrc property.
 /// </summary>
 object[] Data { get; set; }

 /// <summary>
 /// Optional: If an error occurs during the running of 
 /// the server-side processing script, you can inform the user of 
 /// this error by passing back the error message to be displayed using this parameter. 
 /// Do not include if there is no error.
 /// </summary>
 string Error { get; set; }
}

/// <summary>
/// Type-safe page response
/// </summary>
/// <typeparam name="TSource">The type of the data array</typeparam>
public interface IDataTablesResponse<TSource> : IDataTablesResponse
{
 TSource[] DataSource { get; set; }

 IDataTablesResponse<TSource> Transform(Func<TSource, TSource> rowTransform);

 IDataTablesResponse<object> Transform<TMutableSource, TTransform>(
  Func<TMutableSource, TTransform> rowTransform);
}


FormConvertHelper

Dùng parse optional parameter cho DataTablesRequest, không cần thì có thể bỏ luôn.

/// <summary>
/// Form convert helper
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public static class FormConvertHelper
{
 public static void ReadForm(object message, Dictionary<string, object> otherValues, 
  Func<PropertyDescriptor, bool> shouldBypass)
 {
  var objectType = message.GetType();
  var typeDescriptor = TypeDescriptor.GetProvider(objectType).GetTypeDescriptor(objectType);

  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   if (shouldBypass != null && shouldBypass(propertyDescriptor))
   {
    continue;
   }

   if (propertyDescriptor.Attributes.Cast<Attribute>()
    .OfType<JsonIgnoreAttribute>().Count() == 0)
   {
    var propAttr = propertyDescriptor.Attributes.Cast<Attribute>()
     .OfType<JsonPropertyAttribute>().SingleOrDefault();
    var propertyName = propAttr != null ? 
     propAttr.PropertyName : propertyDescriptor.Name;

    if (otherValues.ContainsKey(propertyName))
    {
     if (otherValues[propertyName] != null)
     {
      var convertedValue = TypeHelper.GetDefaultValue(propertyDescriptor.PropertyType);
      var success = TypeHelper.TryCast(otherValues[propertyName], out convertedValue, propertyDescriptor.PropertyType);
      if (success)
      {
       propertyDescriptor.SetValue(message, convertedValue);
      }
     }
    }
   }
  }
 }

 public static void ReadForm(object message, IValueProvider valueProvider, 
  Func<PropertyDescriptor, bool> shouldBypass)
 {
  var objectType = message.GetType();
  var typeDescriptor = TypeDescriptor.GetProvider(objectType).GetTypeDescriptor(objectType);

  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   if (shouldBypass != null && shouldBypass(propertyDescriptor))
   {
    continue;
   }

   if (propertyDescriptor.Attributes.Cast<Attribute>().OfType<JsonIgnoreAttribute>().Count() == 0)
   {
    var propAttr = propertyDescriptor.Attributes
     .Cast<Attribute>().OfType<JsonPropertyAttribute>().SingleOrDefault();
    var propertyName = propAttr != null ? propAttr.PropertyName : propertyDescriptor.Name;

    var valueResult = valueProvider.GetValue(propertyName);
    if (valueResult != null)
    {
     object convertedValue = valueResult.ConvertTo(propertyDescriptor.PropertyType);
     propertyDescriptor.SetValue(message, convertedValue);
    }
   }
  }
 }

 public static IEnumerable<PropertyDescriptor> GetPropertiesFromType(Type type)
 {
  var typeDescriptor = TypeDescriptor.GetProvider(type).GetTypeDescriptor(type);
  foreach (PropertyDescriptor propertyDescriptor in typeDescriptor.GetProperties())
  {
   yield return propertyDescriptor;
  }
 }
}


TypeHelper

/// <summary>
/// Type helper
/// 
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public static class TypeHelper
{
 public static bool CanAssignValue(this PropertyDescriptor p, object value)
 {
  return value == null ? p.IsNullable() : p.PropertyType.IsInstanceOfType(value);
 }

 public static bool IsNullable(this PropertyDescriptor p)
 {
  return p.PropertyType.IsNullable();
 }

 public static bool IsNullable(this Type t)
 {
  return !t.IsValueType || Nullable.GetUnderlyingType(t) != null;
 }

 public static T GetDefaultValue<T>()
 {
  // We want an Func<T> which returns the default.
  // Create that expression here.
  var e = Expression.Lambda<Func<T>>(
   // The default value, always get what the *code* tells us.
   Expression.Default(typeof(T))
  );

  // Compile and return the value.
  return e.Compile()();
 }

 public static object GetDefaultValue(Type type)
 {
  // Validate parameters.
  if (type == null)
  {
   throw new ArgumentNullException("type");
  }

  // We want an Func<object> which returns the default.
  // Create that expression here.
  var e = Expression.Lambda<Func<object>>(
   // Have to convert to object.
   Expression.Convert(
   // The default value, always get what the *code* tells us.
    Expression.Default(type), typeof(object)
   )
  );

  // Compile and return the value.
  return e.Compile()();
 }

 public static bool TryCast<T>(object value, out T result)
 {
  var type = typeof(T);

  // If the type is nullable and the result should be null, set a null value.
  if (type.IsNullable() && (value == null || value == DBNull.Value))
  {
   result = default(T);
   return true;
  }

  // Convert.ChangeType fails on Nullable<T> types.  We want to try to cast to the underlying type anyway.
  var underlyingType = Nullable.GetUnderlyingType(type) ?? type;

  try
  {
   if (underlyingType == typeof(Guid))
   {
    if (value is string)
    {
     value = new Guid(value as string);
    }
    if (value is byte[])
    {
     value = new Guid(value as byte[]);
    }
   }
   else if (underlyingType.IsEnum && value != null)
   {
    value = Enum.Parse(underlyingType, value.ToString(), true);
   }

   result = (T)Convert.ChangeType(value, underlyingType);
   return true;
  }
  catch
  {
   result = default(T);
   return false;
  }
 }

 public static bool TryCast(object value, out object result, Type type)
 {
  var s = "TryCast";
  var openTryCastMethod = typeof(TypeHelper)
   .GetMethods().Single(x => x.Name == s && x.GetGenericArguments().Count() == 1);
  var closedTryCastMethod = openTryCastMethod.MakeGenericMethod(type);

  result = TypeHelper.GetDefaultValue(type);
  var args = new object[] { value, result };
  bool success = (bool)closedTryCastMethod.Invoke(null, args);
  result = args[1];

  return success;
 }
}


Column

/// <summary>
/// Datatables column
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Column : IColumn
{
 /// <summary>
 /// Column's data source.
 /// </summary>
 public virtual string Data { get; set; }

 /// <summary>
 /// Column's name.
 /// </summary>
 public virtual string Name { get; set; }

 /// <summary>
 /// Flag to indicate if this column is searchable (true) or not (false). 
 /// </summary>
 public virtual bool Searchable { get; set; }

 /// <summary>
 /// Flag to indicate if this column is sortable (true) or not (false).
 /// </summary>
 public virtual bool Sortable { get; set; }

 /// <summary>
 /// The search component for the column.
 /// </summary>
 private ISearch _search;

 public virtual ISearch Search
 {
  get
  {
   if (_search == null)
   {
    _search = new Search();
   }

   return _search;
  }
  set
  {
   _search = value ?? new Search();
  }
 }

 public Column() { }
}


Search

/// <summary>
/// Search configuration for columns of a filter request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Search : ISearch
{
 /// <summary>
 /// Search value to apply to this specific column.
 /// </summary>
 public virtual string Value { get; set; }

 /// <summary>
 /// Flag to indicate if the search term for this column should be 
 /// treated as regular expression (true) or not (false). 
 /// As with global search, normally server-side processing scripts 
 /// will not perform regular expression searching for performance 
 /// reasons on large data sets, but it is technically possible and at the discretion of your script.
 /// </summary>
 public virtual bool IsRegex { get; set; }

 public Search() { }

 public Search(string value, bool isRegex)
 {
  Value = value;
  IsRegex = isRegex;
 }
}


Sort

/// <summary>
/// Sort configuration for filter requests
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class Sort : ISort
{
 /// <summary>
 /// Column to which ordering should be applied. This is an index reference 
 /// to the columns array of information that is also submitted to the server.
 /// </summary>
 public virtual int Column { get; set; }

 /// <summary>
 /// Ordering direction for this column. It will be ASC or DESC to 
 /// indicate ascending ordering or descending ordering, respectively.
 /// </summary>
 public virtual int Direction { get; set; }

 public Sort() { }

 public Sort(int column, int direction)
 {
  Column = column;
  Direction = direction;
 }

 public Sort(int column, bool asc)
 {
  Column = column;
  Direction = asc ? 1 : 0;
 }
}


ModelBinder

Để sử dụng ModelBinder, register trong Global.asax

public class BinderConfig
{
 public static void RegisterBinders(ModelBinderDictionary modelBinderDictionary)
 {
  modelBinderDictionary.Add(typeof(DataTablesRequest), new ModelBinder());
 }
}

...Global.asax
protected void Application_Start()
{
 ...
 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
 RouteConfig.RegisterRoutes(RouteTable.Routes);
 BundleConfig.RegisterBundles(BundleTable.Bundles);

 // Register DataTables FilterRequest
 BinderConfig.RegisterBinders(ModelBinders.Binders);
 ...
}


Model để parse request

/// <summary>
/// Model binder for datatables.js parameters
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class ModelBinder : IModelBinder
{
 private readonly Type _concreteType;

 public ModelBinder(Type concreteType = null)
 {
  if (concreteType != null)
  {
   if (!concreteType.IsClass)
   {
    throw new NotSupportedException();
   }

   if (!concreteType.GetInterfaces().Any(x => x == typeof(IDataTablesRequest)))
   {
    throw new NotSupportedException();
   }

   _concreteType = concreteType;
  }
  else
  {
   _concreteType = typeof(DataTablesRequest);
  }
 }

 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {
  var request = Activator.CreateInstance(_concreteType) as IDataTablesRequest;

  Parse(ref request, controllerContext.HttpContext.Request.QueryString);
  Parse(ref request, controllerContext.HttpContext.Request.Form);

  return request;
 }

 private void Parse(ref IDataTablesRequest request, NameValueCollection collection)
 {
  var otherValues = new Dictionary<string, object>();

  foreach (string key in collection.AllKeys)
  {
   if (key == "draw")
   {
    request.Draw = GetValue<int>(collection[key]);
   }
   else if (key == "start")
   {
    request.Start = GetValue<int>(collection[key]);
   }
   else if (key == "length")
   {
    request.Length = GetValue<int>(collection[key]);
   }
   else if (key == "search[value]")
   {
    request.Search.Value = GetValue<string>(collection[key]);
   }
   else if (key == "search[regex]")
   {
    request.Search.IsRegex = GetValue<bool>(collection[key]);
   }
   else if (key.StartsWith("order"))
   {
    ParseSorting(ref request, key, collection[key]);
   }
   else if (key.StartsWith("columns"))
   {
    ParseColumn(ref request, key, collection[key]);
   }
   else
   {
    otherValues.Add(key, collection[key]);
   }
  }

  FormConvertHelper.ReadForm(request, otherValues,
   prop => FormConvertHelper.GetPropertiesFromType(typeof(IDataTablesRequest))
    .Select(x => x.Name).Contains(prop.Name));
 }

 private void ParseSorting(ref IDataTablesRequest request, string key, object value)
 {
  var match = Regex.Match(key, @"order\[([0-9]+)\](.+)");
  if (match.Success && match.Groups.Count == 3)
  {
   var index = Convert.ToInt32(match.Groups[1].Value);
   var propertyName = match.Groups[2].Value;

   while (index >= request.Sort.Count)
   {
    request.Sort.Add(new Sort());
   }

   if (propertyName == "[column]")
   {
    request.Sort[index].Column = GetValue<int>(value);
   }

   else if (propertyName == "[dir]")
   {
    request.Sort[index].Direction =
     string.Compare(GetValue<string>(value), "asc", true) == 0 ?
      1 : 0;
   }
  }
 }

 private void ParseColumn(ref IDataTablesRequest request, string key, object value)
 {
  var match = Regex.Match(key, @"columns\[([0-9]+)\](.+)");
  if (match.Success && match.Groups.Count == 3)
  {
   var index = Convert.ToInt32(match.Groups[1].Value);
   var propertyName = match.Groups[2].Value;

   IColumn currentColumn = null;

   if (!request.Columns.ContainsKey(index))
   {
    currentColumn = new Column();
    request.Columns.Add(index, currentColumn);
   }
   else
   {
    currentColumn = request.Columns[index];
   }

   if (propertyName == "[data]")
   {
    currentColumn.Data = GetValue<string>(value);
   }
   else if (propertyName == "[name]")
   {
    currentColumn.Name = GetValue<string>(value);
   }
   else if (propertyName == "[searchable]")
   {
    currentColumn.Searchable = GetValue<bool>(value);
   }
   else if (propertyName == "[orderable]")
   {
    currentColumn.Sortable = GetValue<bool>(value);
   }
   else if (propertyName == "[search][value]")
   {
    currentColumn.Search.Value = GetValue<string>(value);
   }
   else if (propertyName == "[search][regex]")
   {
    currentColumn.Search.IsRegex = GetValue<bool>(value);
   }
  }
 }

 private static T GetValue<T>(object value)
 {
  return (value == null) ? default(T) : (T)Convert.ChangeType(value, typeof(T));
 }
}


DataTablesRequest

/// <summary>
/// Datatables request
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public class DataTablesRequest : IDataTablesRequest
{
 /// <summary>
 /// Draw counter. This is used by DataTables to ensure that the AJAX returns 
 /// from server-side processing requests are drawn in sequence by DataTables 
 /// (Ajax requests are asynchronous and thus can return out of sequence). 
 /// </summary>
 public virtual int Draw { get; set; }

 /// <summary>
 /// Paging first record indicator. This is the start point in the 
 /// current data set (0 index based - i.e. 0 is the first record).
 /// </summary>
 public virtual int Start { get; set; }

 /// <summary>
 /// Number of records that the table can display in the current draw. 
 /// It is expected that the number of records returned will be equal 
 /// to this number, unless the server has fewer records to return. 
 /// Note that this can be -1 to indicate that all records should be returned 
 /// (although that negates any benefits of server-side processing!)
 /// </summary>
 public virtual int Length { get; set; }

 /// <summary>
 /// Global search component to be applied to all columns which have searchable as true.
 /// </summary>
 private ISearch _search;

 public virtual ISearch Search
 {
  get
  {
   if (_search == null)
   {
    _search = new Search();
   }

   return _search;
  }
  set
  {
   _search = value ?? new Search();
  }
 }

 /// <summary>
 /// The columns' sorting configuration.
 /// </summary>
 private List<ISort> _sort;

 public virtual List<ISort> Sort
 {
  get
  {
   if (_sort == null)
   {
    _sort = new List<ISort>();
   }

   return _sort;
  }
  set
  {
   _sort = value ?? new List<ISort>();
  }
 }

 /// <summary>
 /// The columns collection.
 /// </summary>
 private Dictionary<int, IColumn> _columns;

 public virtual Dictionary<int, IColumn> Columns
 {
  get
  {
   if (_columns == null)
   {
    _columns = new Dictionary<int, IColumn>();
   }

   return _columns;
  }
  set
  {
   _columns = value ?? new Dictionary<int, IColumn>();
  }
 }

 public DataTablesRequest()
 {
  Length = 10;
 }
}


DataTables response

Sau khi có response dạng IDataTablesResponse<Projectchuyển về dạng IDataTablesResponse<object>

DataTablesResponse

/// <summary>
/// Mutable page response
/// 
/// https://github.com/ALMMa/datatables.aspnet
/// https://github.com/offspringer/mvc.datatables/blob/master/Mvc.Datatables
/// </summary>
public abstract class DataTablesResponse
{
 /// <summary>
 /// The draw counter that this object is a response to - 
 /// from the draw parameter sent as part of the data request.
 /// </summary>
 [JsonProperty("draw")]
 public virtual int Draw { get; set; }

 /// <summary>
 /// Total records, before filtering (i.e. the total number of records in the database).
 /// </summary>
 [JsonProperty("recordsTotal")]
 public virtual int TotalRecords { get; set; }

 /// <summary>
 /// Total records, after filtering (i.e. the total number of records after 
 /// filtering has been applied - not just the number of records being returned for this page of data).
 /// </summary>
 [JsonProperty("recordsFiltered")]
 public virtual int TotalFilteredRecords { get; set; }

 /// <summary>
 /// The data to be displayed in the table. This is an array of data source objects, 
 /// one for each row, which will be used by DataTables. Note that this 
 /// parameter's name can be changed using the ajaxDT option's dataSrc property.
 /// </summary>
 [JsonProperty("data")]
 public virtual object[] Data { get; set; }

 /// <summary>
 /// Optional: If an error occurs during the running of the server-side processing script, 
 /// you can inform the user of this error by passing back the error 
 /// message to be displayed using this parameter. Do not include if there is no error.
 /// </summary>
 [JsonProperty("error")]
 public virtual string Error { get; set; }

 public DataTablesResponse() { }
}

/// <summary>
/// Type-safe page response
/// </summary>
/// <typeparam name="TSource">The type of the data array</typeparam>
public class DataTablesResponse<TSource> : DataTablesResponse, IDataTablesResponse<TSource>
{
 [JsonIgnore]
 public virtual TSource[] DataSource
 {
  get
  {
   return base.Data != null ? base.Data.Cast<TSource>().ToArray() : null;
  }
  set
  {
   base.Data = value != null ? value.Cast<object>().ToArray() : null;
  }
 }

 public DataTablesResponse() { }

 public DataTablesResponse(int draw, TSource[] dataSource)
 {
  Draw = draw;
  TotalRecords = dataSource.Length;
  TotalFilteredRecords = dataSource.Length;
  DataSource = dataSource;
 }

 public DataTablesResponse(int draw, int totalRecords, int totalDisplayRecords, TSource[] dataSource)
 {
  Draw = draw;
  TotalRecords = totalRecords;
  TotalFilteredRecords = totalDisplayRecords;
  DataSource = dataSource;
 }

 public virtual IDataTablesResponse<TSource> Transform(Func<TSource, TSource> rowTransform)
 {
  var data = DataSource.Select(rowTransform).ToArray();

  var response = new DataTablesResponse<TSource>(
   Draw, TotalRecords, TotalFilteredRecords, data);

  return response;
 }

 public virtual IDataTablesResponse<object> Transform<TMutableSource, TTransform>(
  Func<TMutableSource, TTransform> rowTransform)
 {
  var data = Data.Cast<TMutableSource>()
   .Select(rowTransform).Cast<object>().ToArray();

  var response = new DataTablesResponse<object>(
   Draw, TotalRecords, TotalFilteredRecords, data);

  return response;
 }
}