├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── Dockerfile-test ├── Dockerfile-websample ├── README.md ├── csharp-datatables-parser.sln ├── docker-compose-test.yaml ├── global.json ├── screenshot.png ├── src ├── DatatablesParser │ ├── DatatablesParser.cs │ └── DatatablesParser.csproj └── aspnet-core-sample │ ├── Controllers │ └── HomeController.cs │ ├── Models │ ├── ErrorViewModel.cs │ └── Person.cs │ ├── Program.cs │ ├── Startup.cs │ ├── Views │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── aspnet-core-sample.csproj │ ├── bundleconfig.json │ ├── entrypoint.sh │ └── wwwroot │ ├── css │ ├── site.css │ └── site.min.css │ ├── favicon.ico │ ├── images │ ├── banner1.svg │ ├── banner2.svg │ ├── banner3.svg │ └── banner4.svg │ └── js │ └── site.js └── test └── DatatablesParser.Tests ├── DatatablesParser.Tests.csproj ├── EFlogger.cs ├── MssqlEntityTests.cs ├── MysqlEntityTests.cs ├── ParameterTests.cs ├── Person.cs ├── PersonContext.cs ├── PgsqlEntityTests.cs ├── TestHelper.cs └── test-runner.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-16.04 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: Build and Test 10 | run: docker-compose -f docker-compose-test.yaml up --force-recreate --exit-code-from test-runner --build test-runner 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | DataTablesParser.sln.ide/ 9 | 10 | # Build results 11 | 12 | [Dd]ebug/ 13 | [Rr]elease/ 14 | x64/ 15 | build/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # MSTest test Results 20 | [Tt]est[Rr]esult*/ 21 | [Bb]uild[Ll]og.* 22 | 23 | *_i.c 24 | *_p.c 25 | *.ilk 26 | *.meta 27 | *.obj 28 | *.pch 29 | *.pdb 30 | *.pgc 31 | *.pgd 32 | *.rsp 33 | *.sbr 34 | *.tlb 35 | *.tli 36 | *.tlh 37 | *.tmp 38 | *.tmp_proj 39 | *.log 40 | *.vspscc 41 | *.vssscc 42 | .builds 43 | *.pidb 44 | *.log 45 | *.scc 46 | 47 | # Visual C++ cache files 48 | ipch/ 49 | *.aps 50 | *.ncb 51 | *.opensdf 52 | *.sdf 53 | *.cachefile 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # Guidance Automation Toolkit 61 | *.gpState 62 | 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper*/ 65 | *.[Rr]e[Ss]harper 66 | 67 | # TeamCity is a build add-in 68 | _TeamCity* 69 | 70 | # DotCover is a Code Coverage Tool 71 | *.dotCover 72 | 73 | # NCrunch 74 | *.ncrunch* 75 | .*crunch*.local.xml 76 | 77 | # Installshield output folder 78 | [Ee]xpress/ 79 | 80 | # DocProject is a documentation generator add-in 81 | DocProject/buildhelp/ 82 | DocProject/Help/*.HxT 83 | DocProject/Help/*.HxC 84 | DocProject/Help/*.hhc 85 | DocProject/Help/*.hhk 86 | DocProject/Help/*.hhp 87 | DocProject/Help/Html2 88 | DocProject/Help/html 89 | 90 | # Click-Once directory 91 | publish/ 92 | 93 | # Publish Web Output 94 | *.Publish.xml 95 | *.pubxml 96 | 97 | # NuGet Packages Directory 98 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 99 | packages/ 100 | .nuget 101 | *.nupkg 102 | *.nupkg.bak 103 | 104 | # Windows Azure Build Output 105 | csx 106 | *.build.csdef 107 | 108 | # Windows Store app package directory 109 | AppPackages/ 110 | 111 | # Others 112 | sql/ 113 | *.Cache 114 | ClientBin/ 115 | [Ss]tyle[Cc]op.* 116 | ~$* 117 | *~ 118 | *.dbmdl 119 | *.[Pp]ublish.xml 120 | *.pfx 121 | *.publishsettings 122 | 123 | # RIA/Silverlight projects 124 | Generated_Code/ 125 | 126 | # Backup & report files from converting an old project file to a newer 127 | # Visual Studio version. Backup files are not needed, because we have git ;-) 128 | _UpgradeReport_Files/ 129 | Backup*/ 130 | UpgradeLog*.XML 131 | UpgradeLog*.htm 132 | 133 | # SQL Server files 134 | *.mdf 135 | *.ldf 136 | *.sdf 137 | 138 | 139 | #LightSwitch generated files 140 | GeneratedArtifacts/ 141 | _Pvt_Extensions/ 142 | ModelManifest.xml 143 | 144 | # ========================= 145 | # Windows detritus 146 | # ========================= 147 | 148 | # Windows image file caches 149 | Thumbs.db 150 | ehthumbs.db 151 | 152 | # Folder config file 153 | Desktop.ini 154 | 155 | # Recycle Bin used on file shares 156 | $RECYCLE.BIN/ 157 | 158 | # Mac desktop service store files 159 | .DS_Store 160 | 161 | #Dotnet Core 162 | project.lock.json 163 | 164 | .vs 165 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | language: c 5 | 6 | services: 7 | - docker 8 | 9 | before_install: 10 | - sudo /etc/init.d/mysql stop 11 | - sudo /etc/init.d/postgresql stop 12 | 13 | script: 14 | - docker-compose -f docker-compose-test.yaml up --force-recreate --exit-code-from test-runner --build test-runner -------------------------------------------------------------------------------- /Dockerfile-test: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 2 | RUN apt-get update && apt-get install -y netcat 3 | COPY /src /src 4 | COPY /test /test 5 | WORKDIR /test/DatatablesParser.Tests 6 | ENTRYPOINT ./test-runner.sh 7 | -------------------------------------------------------------------------------- /Dockerfile-websample: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore-build:2.0.0-preview2 as builder 2 | COPY . /workspace 3 | WORKDIR /workspace 4 | RUN mkdir /publish 5 | RUN dotnet publish -o /publish src/aspnet-core-sample/aspnet-core-sample.csproj 6 | 7 | FROM microsoft/aspnetcore:2.0.0-preview2 8 | EXPOSE 80/tcp 9 | COPY --from=builder /publish /app 10 | WORKDIR /app 11 | ENTRYPOINT ["dotnet", "aspnet-core-sample.dll"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C# datatables parser 2 | ======================== 3 | ![Build Status](https://travis-ci.org/garvincasimir/csharp-datatables-parser.svg?branch=master) 4 | 5 | A C# .Net Core Serverside parser for the popuplar [jQuery datatables plugin](http://www.datatables.net) 6 | 7 | ![Screenshot](screenshot.png) 8 | 9 | Supported Platforms 10 | ========================== 11 | The parser aims to be Database and Provider agnostic. It currently targets Netstandard 1.3. The solution includes tests for: 12 | * Entity Framework Core 13 | * In Memory 14 | * MySql 15 | * Sql Server 16 | * PostgreSQL 17 | 18 | If you intend to filter your dataset, the default configuration assumes your IQueryable uses a provider with support for .ToString() on DateTime and numeric types. Please note that EFCore will not fail but instead fall back to client evaluation if this requirement is not met. I have mixed feelings about this. I believe client evaluation should be opt-in and not opt-out. 19 | 20 | jQuery Datatables 21 | ======================== 22 | 23 | The jQuery Datatables plugin is a very powerful javascript grid plugin which comes with the following features out of the box: 24 | 25 | * Filtering 26 | * Sorting 27 | * Paging 28 | * Themes 29 | * Plugins 30 | * Ajax/Remote and local datasource support 31 | 32 | Using the Parser 33 | ======================== 34 | 35 | Please see the [official datatables documentation](http://datatables.net/release-datatables/examples/data_sources/server_side.html) for examples on setting it up to connect to a serverside datasource. 36 | 37 | The following snippets were taken from the aspnet-core-sample project also located in this repository 38 | 39 | **HomeController.cs** 40 | ```c# 41 | public class HomeController : Controller 42 | { 43 | private readonly PersonContext _context; 44 | 45 | public HomeController(PersonContext context) 46 | { 47 | _context = context; 48 | } 49 | public IActionResult Index() 50 | { 51 | return View(); 52 | } 53 | 54 | public IActionResult Data() 55 | { 56 | var parser = new Parser(Request.Form, _context.People); 57 | 58 | return Json(parser.Parse()); 59 | } 60 | } 61 | ``` 62 | **Startup.cs** 63 | ```c# 64 | public IServiceProvider ConfigureServices(IServiceCollection services) 65 | { 66 | 67 | services.AddDbContext(options => options.UseInMemoryDatabase("aspnet-core-websample")); 68 | 69 | services.AddMvc() 70 | .AddJsonOptions(options => 71 | { 72 | options.SerializerSettings.ContractResolver = new DefaultContractResolver(); 73 | }); 74 | 75 | return services.BuildServiceProvider(); 76 | } 77 | ``` 78 | **Index.cshtml** 79 | ```html 80 | @{ 81 | ViewData["title"] = "People Table"; 82 | } 83 | 84 |

Index

85 |
86 | 87 | @section Scripts 88 | { 89 | 113 | 114 | 115 | } 116 | ``` 117 | The included Dockerfile-websample builds, packages and runs the web sample project in a docker image. No tools, frameworks or runtimes are required on the host machine. The image has been published to docker for your convenience. 118 | 119 | docker run -p 80:80 garvincasimir/datatables-aspnet-core-sample:0.0.2 120 | 121 | Projections 122 | ======================== 123 | I recommended always using a projection with the query that is sent to the parser. This strategy has 4 main benefits: 124 | * Avoid inadverently serializing and sending sensitive fields to the client. 125 | * Avoid custom non-database fields in your model 126 | * Inlcude parent table fields 127 | * Include computed fields 128 | 129 | Below is an example of a self referencing table: 130 | 131 | | EmployeeID | FirstName | LastName | ManagerID | Token | BirthDate | 132 | | ----------- | --------- | -------- | -------- | ----------- | --------- | 133 | | 1 | Mary | Joe | null | s38fjsf8dj | 3/3/1921 | 134 | | 2 | Jane | Jim | 1 | 9fukfdflsl | 2/2/1921 | 135 | | 3 | Rose | Jack | 1 | s9fkf;;d; | 1/1/1931 | 136 | 137 | 138 | The model class: 139 | 140 | ```csharp 141 | public class Employee 142 | { 143 | public int EmployeeID {get;set;} 144 | public string FirstName {get;set;} 145 | public string LastName {get;set;} 146 | public int? ManagerID {get;set;} 147 | [ForeignKey("ManagerID")] 148 | public Employee Manager {get;set;} 149 | public string Token {get;set;} 150 | public DateTime BirthDate {get;set;} 151 | } 152 | ``` 153 | 154 | Projection class: 155 | 156 | ```csharp 157 | public class EmployeeResult 158 | { 159 | public int EmployeeID {get;set;} 160 | public string FullName {get;set;} 161 | public int? ManagerID {get;set;} 162 | public string ManagerFullName {get;set;} 163 | public DateTime BirthDate {get;set;} 164 | public string BirthDateFormatted 165 | { 166 | get 167 | { 168 | return String.Format("{0:M/d/yyyy}", BirthDate); 169 | } 170 | } 171 | } 172 | ``` 173 | Query: 174 | 175 | ```csharp 176 | var query = from e in context.Employees 177 | let FullName = e.FirstName + " " + e.LastName 178 | let ManagerFullName = e.Manager.FirstName + " " + e.Manager.LastName 179 | select new EmployeeResult 180 | { 181 | EmployeeID = e.EmployeeID, 182 | FullName = FullName, 183 | ManagerID = e.ManagerID, 184 | ManagerFullName = ManagerFullName, 185 | BirthDate = e.BirthDate 186 | }; 187 | 188 | var parser = new Parser(Request.Form, query); 189 | 190 | ``` 191 | 192 | 193 | Custom Filter Expressions 194 | ======================== 195 | The parser builds a set of expressions based on the settings and filter text sent from Datatables. The end result is a *WHERE* clause which looks something like this: 196 | 197 | ```sql 198 | FROM [People] AS [val] 199 | WHERE ((((CASE 200 | WHEN CHARINDEX(N'cromie', LOWER([val].[FirstName])) > 0 201 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 202 | END | CASE 203 | WHEN CHARINDEX(N'cromie', LOWER([val].[LastName])) > 0 204 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 205 | END) | CASE 206 | WHEN CHARINDEX(N'cromie', LOWER(CONVERT(VARCHAR(100), [val].[BirthDate]))) > 0 207 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 208 | END) | CASE 209 | WHEN CHARINDEX(N'cromie', LOWER(CONVERT(VARCHAR(100), [val].[Weight]))) > 0 210 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 211 | END) | CASE 212 | WHEN CHARINDEX(N'cromie', LOWER(CONVERT(VARCHAR(11), [val].[Children]))) > 0 213 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 214 | END) = 1 215 | ``` 216 | 217 | In the above example, each of the case statements will attempt to find the filter text 'cromie' within a string representation of the properties from *T*. The exact mechanics and syntax will vary by provider and db engine but this is an example of the actual query sent to your backend database. 218 | 219 | The expression generated by the parser looks like this: 220 | 221 | ``` 222 | where val.FirstName.ToLower().Contains("cromie") || val.LastName.ToLower().Contains("cromie") || val.BirthDate.ToString().ToLower().Contains("cromie") || val.Weight.ToString().ToLower().Contains("cromie") || val.Children.ToString().ToLower().Contains("cromie") 223 | ``` 224 | 225 | What is missing in the above expression is the ability to format dates to match the client side. So it may seem strange to a user if they enter a date in the filter text box and no results are returned. It would be nice if providers just supported *DateTime.ToString(string format)* right? Even if they did, the format strings expected by db engines are not consistent at all. As a result, I decided to expose some of the internals of the parser and allow library users to substitute .ToString() with a custom expression. 226 | 227 | For example, if your provider does support *DateTime.ToString(string format)*, you can substitute .ToString() with that expression after initializing the parser. This must be explicitly called for each applicable property. 228 | 229 | ```c# 230 | var parser = new Parser(p, context.People) 231 | .SetConverter(x => x.BirthDate, x => x.BirthDate.ToString("M/dd/yyyy")) 232 | .SetConverter(x => x.LastUpdated, x => x.LastUpdated.ToString("M/dd/yyyy")); 233 | ``` 234 | 235 | **EF Core 2** 236 | 237 | Thanks to [this](https://github.com/aspnet/EntityFrameworkCore/pull/8507) pull request by [Paul Middleton](https://github.com/pmiddleton), EF Core 2 supports mapping user defined and system scalar valued functions. These functions can be used for string conversions and custom formatting. The following is an example for SQL Server >= 2012. 238 | 239 | PersonContext.cs 240 | ```c# 241 | using Microsoft.EntityFrameworkCore; 242 | using System; 243 | 244 | namespace DataTablesParser.Tests 245 | { 246 | public class PersonContext : DbContext 247 | { 248 | public PersonContext(){ } 249 | 250 | public PersonContext(DbContextOptions options) 251 | : base(options){ } 252 | 253 | //Sql Server >= 2012 254 | //https://docs.microsoft.com/en-us/sql/t-sql/functions/format-transact-sql 255 | [DbFunction(Schema="")] 256 | public static string Format(DateTime data,string format) 257 | { 258 | throw new Exception(); 259 | } 260 | 261 | public DbSet People { get; set; } 262 | } 263 | } 264 | 265 | ``` 266 | Parser initialization 267 | 268 | ```c# 269 | 270 | var parser = new Parser(p, context.People) 271 | .SetConverter(x => x.BirthDate, x => PersonContext.Format(x.BirthDate,"M/dd/yyyy")); 272 | ``` 273 | 274 | The *WHERE* clause now looks like this: 275 | 276 | ```sql 277 | WHERE ((((CASE 278 | WHEN CHARINDEX(N'9/03/1953', LOWER([val].[FirstName])) > 0 279 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 280 | END | CASE 281 | WHEN CHARINDEX(N'9/03/1953', LOWER([val].[LastName])) > 0 282 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 283 | END) | CASE 284 | WHEN CHARINDEX(N'9/03/1953', LOWER(Format([val].[BirthDate], N'M/dd/yyyy'))) > 0 285 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 286 | END) | CASE 287 | WHEN CHARINDEX(N'9/03/1953', LOWER(CONVERT(VARCHAR(100), [val].[Weight]))) > 0 288 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 289 | END) | CASE 290 | WHEN CHARINDEX(N'9/03/1953', LOWER(CONVERT(VARCHAR(11), [val].[Children]))) > 0 291 | THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) 292 | END) = 1 293 | ``` 294 | 295 | **EF Core 3+** 296 | 297 | It isn't simple to call built-in functions in EF Core 3+. Use the same strategy from EF Core 2 but with a User Defined Function. Essentially, create a UDF and call the system function from there. 298 | 299 | **EF6** 300 | 301 | In EF6 you can make use of the [Sql Functions Class](https://msdn.microsoft.com/en-us/library/system.data.objects.sqlclient.sqlfunctions(v=vs.110).aspx) to format dates and numbers. 302 | 303 | ```csharp 304 | //Property BirthDate is a DateTime 305 | var parser = new Parser(p, context.People) 306 | .SetConverter(x => x.BirthDate, x => SqlFunctions.DateName("m",x.BirthDate)); 307 | ``` 308 | For examples using MySQL and PostgreSQL please see the test project 309 | 310 | **Partial String Searches** 311 | 312 | By default, the library does filtering by calling String.Contains() on a field with the search term as the argument. For those who want to match only the start or end of the field, there is a concept of start and end tokens. When these tokens are found at the start or end of a search term the library calls `String.StartsWith()` or `String.EndsWith()` respectively. If both tokens are present the default `String.Contains()` will be called. The default tokens are *\*|* for matching the beginning of the field and *|\** for matching the end of a field. 313 | 314 | For example, you might want to filter by all users with a name that begins with the letter *a*. In that case, you would allow the user to search as usual and prepend the token to the search term either in the [pre-xhr hook](https://datatables.net/reference/event/preXhr) or on the server side before the Datatable config vars are passed to the library. 315 | 316 | The start and end tokens can be replaced with custom strings by using the following methods: 317 | 318 | ```csharp 319 | var parser = new Parser(p, context.People) 320 | .SetStartsWithToken("--") 321 | .SetEndsWithToken("++"); 322 | ``` 323 | 324 | *This feature is still experimental. As of now it is not available in the Nuget package.* 325 | 326 | Installation 327 | ======================== 328 | 329 | **Visual Studio** 330 | 331 | You can search using the NuGet package manager, or you 332 | can enter the following command in your package manager console: 333 | 334 | PM> Install-Package DatatablesParser-core 335 | 336 | **Visual Studio Code** 337 | 338 | Use the built in terminal and run the following command: 339 | 340 | dotnet add package DatatablesParser-core 341 | 342 | 343 | Testing 344 | ========================= 345 | This solution is configured to run tests using xunit. However, the MySql and Sql Server entity tests require a running server. You can use the included docker-compose-test.yaml to run all the unit and integration tests. 346 | 347 | docker-compose -f docker-compose-test.yaml up --force-recreate --exit-code-from test-runner --build test-runner 348 | 349 | Contributions, comments, changes, issues 350 | ======================== 351 | 352 | I welcome any suggestions for improvement, contributions, questions or issues with using this code. 353 | 354 | * Please do not include multiple unrelated fixes in a single pull request 355 | * The diff for your pull request should only show changes related to your fix/addition (Some editors create unnecessary changes). 356 | * When possible include tests that cover the features/changes in your pull request 357 | * Before you submit make sure the existing tests pass with your changes 358 | * Also, issues that are accompanied by failing tests will probably get handled quicker 359 | 360 | Contact 361 | ======================== 362 | Twitter: [garvincasimir](https://twitter.com/garvincasimir) 363 | -------------------------------------------------------------------------------- /csharp-datatables-parser.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.29324.140 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EB0E12E9-C989-4F73-868E-5E5FEA1F747B}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatatablesParser.Tests", "test\DatatablesParser.Tests\DatatablesParser.Tests.csproj", "{9C1BC2C4-6595-494C-9261-E7D4B2B97032}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{143602A2-DF53-479F-84C4-35B33F4353AA}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatatablesParser", "src\DatatablesParser\DatatablesParser.csproj", "{110C8B7F-5098-49C5-A51D-2E34F67D7BDF}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aspnet-core-sample", "src\aspnet-core-sample\aspnet-core-sample.csproj", "{A1BE24AA-4276-4EB1-96E7-FD496C336D9F}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x64 = Debug|x64 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|x64 = Release|x64 22 | Release|x86 = Release|x86 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|x64.Build.0 = Debug|Any CPU 29 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Debug|x86.Build.0 = Debug|Any CPU 31 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|x64.ActiveCfg = Release|Any CPU 34 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|x64.Build.0 = Release|Any CPU 35 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|x86.ActiveCfg = Release|Any CPU 36 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032}.Release|x86.Build.0 = Release|Any CPU 37 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|x64.Build.0 = Debug|Any CPU 41 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Debug|x86.Build.0 = Debug|Any CPU 43 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|x64.ActiveCfg = Release|Any CPU 46 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|x64.Build.0 = Release|Any CPU 47 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|x86.ActiveCfg = Release|Any CPU 48 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF}.Release|x86.Build.0 = Release|Any CPU 49 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|x64.Build.0 = Debug|Any CPU 53 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Debug|x86.Build.0 = Debug|Any CPU 55 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|x64.ActiveCfg = Release|Any CPU 58 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|x64.Build.0 = Release|Any CPU 59 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|x86.ActiveCfg = Release|Any CPU 60 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {9C1BC2C4-6595-494C-9261-E7D4B2B97032} = {EB0E12E9-C989-4F73-868E-5E5FEA1F747B} 67 | {110C8B7F-5098-49C5-A51D-2E34F67D7BDF} = {143602A2-DF53-479F-84C4-35B33F4353AA} 68 | {A1BE24AA-4276-4EB1-96E7-FD496C336D9F} = {143602A2-DF53-479F-84C4-35B33F4353AA} 69 | EndGlobalSection 70 | GlobalSection(ExtensibilityGlobals) = postSolution 71 | SolutionGuid = {BDD9EADE-6D41-4EC1-BCD9-E1EFB4D9352B} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /docker-compose-test.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | test-runner: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile-test 8 | depends_on: 9 | - mysql 10 | - mssql 11 | - pgsql 12 | mysql: 13 | image: mysql 14 | container_name: dotnet-test-mysql 15 | environment: 16 | MYSQL_ROOT_PASSWORD: "Rea11ytrong_3" 17 | MYSQL_USER: "tester" 18 | MYSQL_PASSWORD: "Rea11ytrong_3" 19 | MYSQL_DATABASE: "dotnettest" 20 | 21 | mssql: 22 | image: microsoft/mssql-server-linux 23 | container_name: dotnet-test-mssql 24 | environment: 25 | ACCEPT_EULA: "Y" 26 | SA_PASSWORD: "Rea11ytrong_3" 27 | MSSQL_MEMORY_LIMIT_MB: "1024" 28 | pgsql: 29 | image: postgres 30 | environment: 31 | POSTGRES_PASSWORD: "Rea11ytrong_3" 32 | POSTGRES_USER: "tester" 33 | POSTGRES_DB: "dotnettest" 34 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1.201" 4 | } 5 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garvincasimir/csharp-datatables-parser/879da8acaa5597486162fef5fe10f0ef7ae8ad7e/screenshot.png -------------------------------------------------------------------------------- /src/DatatablesParser/DatatablesParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Microsoft.Extensions.Primitives; 6 | using System.Reflection; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace DataTablesParser 10 | { 11 | public class Parser where T : class 12 | { 13 | private IQueryable _originalQuery; 14 | private IQueryable _query; 15 | private readonly Dictionary _config; 16 | private readonly Type _type; 17 | private IDictionary _propertyMap ; 18 | 19 | //Global configs 20 | private int _take; 21 | private int _skip; 22 | private bool _sortDisabled = false; 23 | private string _startsWithtoken = Constants.DEFAULT_STARTS_WITH_TOKEN; 24 | private string _endsWithToken = Constants.DEFAULT_ENDS_WITH_TOKEN; 25 | private bool _isEnumerableQuery; 26 | 27 | private Dictionary _converters = new Dictionary(); 28 | 29 | private Type[] _convertable = 30 | { 31 | typeof(int), 32 | typeof(Nullable), 33 | typeof(decimal), 34 | typeof(Nullable), 35 | typeof(float), 36 | typeof(Nullable), 37 | typeof(double), 38 | typeof(Nullable), 39 | typeof(DateTime), 40 | typeof(Nullable), 41 | typeof(string) 42 | }; 43 | 44 | public Parser(IEnumerable> configParams, IQueryable query) 45 | { 46 | _originalQuery = query; 47 | _query = query; 48 | _config = configParams.ToDictionary(k => k.Key,v=> v.Value.First().Trim()); 49 | _type = typeof(T); 50 | 51 | //This associates class properties with corresponding datatable configuration options 52 | _propertyMap = (from param in _config 53 | join prop in _type.GetProperties() on param.Value equals prop.Name 54 | where Regex.IsMatch(param.Key,Constants.COLUMN_PROPERTY_PATTERN) 55 | let index = Regex.Match(param.Key, Constants.COLUMN_PROPERTY_PATTERN).Groups[1].Value 56 | let searchableKey = Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT,index) 57 | let searchable = _config.ContainsKey(searchableKey) && _config[searchableKey] == "true" 58 | let orderableKey = Constants.GetKey(Constants.ORDERABLE_PROPERTY_FORMAT, index) 59 | let orderable = _config.ContainsKey(orderableKey) && _config[orderableKey]== "true" 60 | let filterKey = Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT,index) 61 | let filter = _config.ContainsKey(filterKey)?_config[filterKey]:string.Empty 62 | // Set regex when implemented 63 | 64 | select new 65 | { 66 | index = int.Parse(index), 67 | map = new PropertyMap 68 | { 69 | Property = prop, 70 | Searchable = searchable, 71 | Orderable = orderable, 72 | Filter = filter 73 | } 74 | }).Distinct().ToDictionary(k => k.index, v => v.map); 75 | 76 | 77 | if(_propertyMap.Count == 0 ) 78 | { 79 | throw new Exception("No properties were found in request. Please map datatable field names to properties in T"); 80 | } 81 | 82 | if(_config.ContainsKey(Constants.DISPLAY_START)) 83 | { 84 | int.TryParse(_config[Constants.DISPLAY_START], out _skip); 85 | } 86 | 87 | 88 | if(_config.ContainsKey(Constants.DISPLAY_LENGTH)) 89 | { 90 | int.TryParse(_config[Constants.DISPLAY_LENGTH], out _take); 91 | } 92 | else 93 | { 94 | _take = 10; 95 | } 96 | 97 | _sortDisabled = _config.ContainsKey(Constants.ORDERING_ENABLED) && _config[Constants.ORDERING_ENABLED] == "false"; 98 | _isEnumerableQuery = _query is System.Linq.EnumerableQuery; 99 | } 100 | 101 | public Results Parse() 102 | { 103 | var list = new Results(); 104 | 105 | // parse the echo property 106 | list.draw = int.Parse(_config[Constants.DRAW]); 107 | 108 | // count the record BEFORE filtering 109 | list.recordsTotal = _originalQuery.Count(); 110 | 111 | //sort results if sorting isn't disabled or skip needs to be called 112 | if(!_sortDisabled || _skip > 0) 113 | { 114 | ApplySort(); 115 | } 116 | 117 | 118 | IEnumerable resultQuery; 119 | var hasFilterText = !string.IsNullOrWhiteSpace(_config[Constants.SEARCH_KEY]) || _propertyMap.Any( p => !string.IsNullOrWhiteSpace(p.Value.Filter)); 120 | //Use query expression to return filtered paged list 121 | //This is a best effort to avoid client evaluation whenever possible 122 | //No good api to determine support for .ToString() on a type 123 | if(hasFilterText) 124 | { 125 | var entityFilter = GenerateEntityFilter(); 126 | resultQuery = _query.Where(entityFilter) 127 | .Skip(_skip) 128 | .Take(_take); 129 | 130 | list.recordsFiltered = _query.Count(entityFilter); 131 | } 132 | else 133 | { 134 | resultQuery = _query 135 | .Skip(_skip) 136 | .Take(_take); 137 | 138 | if(_query == _originalQuery) 139 | list.recordsFiltered = list.recordsTotal; 140 | else 141 | list.recordsFiltered = _query.Count(); 142 | 143 | } 144 | 145 | 146 | list.data = resultQuery.ToList(); 147 | 148 | return list; 149 | } 150 | 151 | /// 152 | /// SetConverter accepts a custom expression for converting a property in T to string. 153 | /// This will be used during filtering. 154 | /// 155 | /// A lambda expression with a member expression as the body 156 | /// A lambda given T returns a string by performing a sql translatable operation on property 157 | public Parser SetConverter(Expression> property, Expression> tostring) 158 | { 159 | var memberExp = ((UnaryExpression)property.Body).Operand as MemberExpression; 160 | 161 | if(memberExp == null) 162 | { 163 | throw new ArgumentException("Body in property must be a member expression"); 164 | } 165 | 166 | _converters[memberExp.Member.Name] = tostring.Body; 167 | 168 | return this; 169 | } 170 | 171 | /// 172 | /// SetStartsWithToken overrides the default StartsWith filter token 173 | /// The default token is *| 174 | /// By default all filters are in the form of string.Contains(FILTER_STRING) 175 | /// If a filter string is in the form of token + FILTER_STRING eg. *|app, 176 | /// the search will be translated to string.StartsWith("app") 177 | /// 178 | /// A string used to replace the default token 179 | public Parser SetStartsWithToken(string token) 180 | { 181 | this._startsWithtoken = token; 182 | return this; 183 | } 184 | 185 | /// 186 | /// SetEndsWithToken overrides the default EndsWith filter token 187 | /// The default token is |* 188 | /// By default all filters are in the form of string.Contains(FILTER_STRING) 189 | /// If a filter string is in the form of FILTER_STRING + token eg. app|*, 190 | /// the search will be translated to string.EndsWith("app") 191 | /// 192 | /// A string used to replace the default token 193 | public Parser SetEndsWithToken(string token) 194 | { 195 | this._endsWithToken = token; 196 | return this; 197 | } 198 | 199 | /// 200 | /// AddCustomFilter add external custom filter to handle complex filtering logic 201 | /// For example date range filtering 202 | /// 203 | /// Filter logic 204 | /// 205 | public Parser AddCustomFilter(Expression> predicate) 206 | { 207 | _query = _query.Where(predicate); 208 | return this; 209 | } 210 | 211 | private void ApplySort() 212 | { 213 | var sorted = false; 214 | var paramExpr = Expression.Parameter(_type, "val"); 215 | 216 | // Enumerate the keys sort keys in the order we received them 217 | foreach (var param in _config.Where(k => Regex.IsMatch(k.Key, Constants.ORDER_PATTERN))) 218 | { 219 | // column number to sort (same as the array) 220 | int sortcolumn = int.Parse(param.Value); 221 | 222 | // ignore disabled columns 223 | if (!_propertyMap.ContainsKey(sortcolumn) || !_propertyMap[sortcolumn].Orderable) 224 | { 225 | continue; 226 | } 227 | 228 | var index = Regex.Match(param.Key, Constants.ORDER_PATTERN).Groups[1].Value; 229 | var orderDirectionKey = Constants.GetKey(Constants.ORDER_DIRECTION_FORMAT, index); 230 | 231 | // get the direction of the sort 232 | string sortdir = _config[orderDirectionKey]; 233 | 234 | 235 | var sortProperty = _propertyMap[sortcolumn].Property; 236 | var expression1 = Expression.Property(paramExpr, sortProperty); 237 | var propType = sortProperty.PropertyType; 238 | var delegateType = Expression.GetFuncType(_type, propType); 239 | var propertyExpr = Expression.Lambda(delegateType, expression1, paramExpr); 240 | 241 | // apply the sort (default is ascending if not specified) 242 | string methodName; 243 | if (string.IsNullOrEmpty(sortdir) || sortdir.Equals(Constants.ASCENDING_SORT, StringComparison.OrdinalIgnoreCase)) 244 | { 245 | methodName = sorted ? "ThenBy" : "OrderBy"; 246 | } 247 | else 248 | { 249 | methodName = sorted ? "ThenByDescending" : "OrderByDescending"; 250 | } 251 | 252 | _query = typeof(Queryable).GetMethods().Single( 253 | method => method.Name == methodName 254 | && method.IsGenericMethodDefinition 255 | && method.GetGenericArguments().Length == 2 256 | && method.GetParameters().Length == 2) 257 | .MakeGenericMethod(_type, propType) 258 | .Invoke(null, new object[] { _query, propertyExpr }) as IOrderedQueryable; 259 | 260 | sorted = true; 261 | } 262 | 263 | //Linq to entities needs a sort to implement skip 264 | //Not sure if we care about the queriables that come in sorted? IOrderedQueryable does not seem to be a reliable test 265 | if (!sorted ) 266 | { 267 | var firstProp = Expression.Property(paramExpr, _propertyMap.First().Value.Property); 268 | var propType = _propertyMap.First().Value.Property.PropertyType; 269 | var delegateType = Expression.GetFuncType(_type, propType); 270 | var propertyExpr = Expression.Lambda(delegateType, firstProp, paramExpr); 271 | 272 | _query = typeof(Queryable).GetMethods().Single( 273 | method => method.Name == "OrderBy" 274 | && method.IsGenericMethodDefinition 275 | && method.GetGenericArguments().Length == 2 276 | && method.GetParameters().Length == 2) 277 | .MakeGenericMethod(_type, propType) 278 | .Invoke(null, new object[] { _query, propertyExpr }) as IOrderedQueryable; 279 | 280 | } 281 | 282 | } 283 | 284 | private string GetFilterFn(string filter) 285 | { 286 | switch(filter) 287 | { 288 | case null: 289 | return Constants.CONTAINS_FN; 290 | case var f when f.StartsWith(_startsWithtoken) && f.EndsWith(_endsWithToken): 291 | return Constants.CONTAINS_FN; 292 | case var f when f.StartsWith(_startsWithtoken): 293 | return Constants.STARTS_WITH_FN; 294 | case var f when f.EndsWith(_endsWithToken): 295 | return Constants.ENDS_WITH_FN; 296 | default: 297 | return Constants.CONTAINS_FN; 298 | } 299 | } 300 | 301 | private string RemoveFilterTokens(string filter) 302 | { 303 | string untoken = filter; 304 | 305 | if(untoken.StartsWith(_startsWithtoken)) 306 | { 307 | untoken = untoken.Remove(0,_startsWithtoken.Length); 308 | } 309 | 310 | if(untoken.EndsWith(_endsWithToken)) 311 | { 312 | untoken = untoken.Remove(filter.LastIndexOf(_endsWithToken),_endsWithToken.Length); 313 | } 314 | 315 | return untoken; 316 | } 317 | 318 | /// 319 | /// Generate a lamda expression based on a search filter for all mapped columns 320 | /// 321 | private Expression> GenerateEntityFilter() 322 | { 323 | 324 | var paramExpression = Expression.Parameter(_type, "val"); 325 | 326 | string filter = _config[Constants.SEARCH_KEY]; 327 | string globalFilterFn = null; 328 | ConstantExpression globalFilterConst = null; 329 | Expression filterExpr = null; 330 | if(!string.IsNullOrWhiteSpace(filter)) 331 | { 332 | globalFilterFn = GetFilterFn(filter); 333 | filter = RemoveFilterTokens(filter); 334 | globalFilterConst = Expression.Constant(filter.ToLower()); 335 | } 336 | 337 | List individualConditions = new List(); 338 | var modifier = new ModifyParam(paramExpression); //map user supplied converters using a visitor 339 | 340 | foreach (var propMap in _propertyMap.Where(m => m.Value.Searchable)) 341 | { 342 | var prop = propMap.Value.Property; 343 | var isString = prop.PropertyType == typeof(string); 344 | var hasCustomExpr = _converters.ContainsKey(prop.Name); 345 | string propFilterFn = null; 346 | 347 | if ( !prop.CanWrite || (!_convertable.Any(t => t == prop.PropertyType) && !hasCustomExpr && !_isEnumerableQuery )) 348 | { 349 | continue; 350 | } 351 | 352 | ConstantExpression individualFilterConst = null; 353 | if(!string.IsNullOrWhiteSpace(propMap.Value.Filter)) 354 | { 355 | propFilterFn = GetFilterFn(propMap.Value.Filter); 356 | propMap.Value.Filter = RemoveFilterTokens(propMap.Value.Filter); 357 | individualFilterConst = Expression.Constant(propMap.Value.Filter.ToLower()); 358 | } 359 | 360 | Expression propExp = Expression.Property(paramExpression, prop); 361 | 362 | if(hasCustomExpr) 363 | { 364 | propExp = modifier.Visit( _converters[prop.Name]); 365 | } 366 | else if (!isString) 367 | { 368 | var toString = prop.PropertyType.GetMethod("ToString", Type.EmptyTypes); 369 | 370 | propExp = Expression.Call(propExp, toString); 371 | 372 | } 373 | 374 | var toLower = Expression.Call(propExp,typeof(string).GetMethod("ToLower", Type.EmptyTypes)); 375 | 376 | if(globalFilterConst!=null) 377 | { 378 | Expression globalTest = Expression.Call(toLower, typeof(string).GetMethod(globalFilterFn, new[] { typeof(string) }), globalFilterConst); 379 | 380 | if(filterExpr == null) 381 | { 382 | filterExpr = globalTest; 383 | } 384 | else 385 | { 386 | filterExpr = Expression.OrElse(filterExpr,globalTest); 387 | } 388 | } 389 | 390 | if(individualFilterConst!=null) 391 | { 392 | individualConditions.Add(Expression.Call(toLower, typeof(string).GetMethod(propFilterFn, new[] { typeof(string) }), individualFilterConst)); 393 | 394 | } 395 | 396 | } 397 | 398 | 399 | foreach(var condition in individualConditions) 400 | { 401 | if(filterExpr == null) 402 | { 403 | filterExpr = condition; 404 | } 405 | else 406 | { 407 | filterExpr = Expression.AndAlso(filterExpr,condition); 408 | } 409 | } 410 | 411 | // return the expression as a lambda 412 | return Expression.Lambda>(filterExpr, paramExpression); 413 | 414 | } 415 | 416 | public class ModifyParam : ExpressionVisitor 417 | { 418 | private ParameterExpression _replace; 419 | 420 | public ModifyParam(ParameterExpression p) 421 | { 422 | _replace = p; 423 | } 424 | 425 | protected override Expression VisitParameter(ParameterExpression node) 426 | { 427 | return _replace; 428 | } 429 | 430 | } 431 | 432 | private class PropertyMap 433 | { 434 | public PropertyInfo Property { get; set; } 435 | public bool Orderable { get; set; } 436 | public bool Searchable { get; set; } 437 | public string Regex { get; set; } //Not yet implemented 438 | public string Filter { get; set; } 439 | } 440 | 441 | 442 | 443 | } 444 | public class Results 445 | { 446 | public int draw { get; set; } 447 | public int recordsTotal { get; set; } 448 | public int recordsFiltered { get; set; } 449 | public List data { get; set; } 450 | 451 | } 452 | 453 | public class Constants 454 | { 455 | public const string COLUMN_PROPERTY_PATTERN = @"columns\[(\d+)\]\[data\]"; 456 | public const string ORDER_PATTERN = @"order\[(\d+)\]\[column\]"; 457 | 458 | public const string DISPLAY_START = "start"; 459 | public const string DISPLAY_LENGTH = "length"; 460 | public const string DRAW = "draw"; 461 | public const string ASCENDING_SORT = "asc"; 462 | public const string SEARCH_KEY = "search[value]"; 463 | public const string SEARCH_REGEX_KEY = "search[regex]"; 464 | 465 | public const string DATA_PROPERTY_FORMAT = "columns[{0}][data]"; 466 | public const string SEARCHABLE_PROPERTY_FORMAT = "columns[{0}][searchable]"; 467 | public const string ORDERABLE_PROPERTY_FORMAT = "columns[{0}][orderable]"; 468 | public const string SEARCH_VALUE_PROPERTY_FORMAT = "columns[{0}][search][value]"; 469 | public const string SEARCH_REGEX_PROPERTY_FORMAT = "columns[{0}][search][regex]"; 470 | public const string ORDER_COLUMN_FORMAT = "order[{0}][column]"; 471 | public const string ORDER_DIRECTION_FORMAT = "order[{0}][dir]"; 472 | public const string ORDERING_ENABLED = "ordering"; 473 | 474 | public const string CONTAINS_FN = "Contains"; 475 | public const string STARTS_WITH_FN = "StartsWith"; 476 | public const string ENDS_WITH_FN = "EndsWith"; 477 | 478 | public const string DEFAULT_STARTS_WITH_TOKEN = "*|"; 479 | public const string DEFAULT_ENDS_WITH_TOKEN = "|*"; 480 | 481 | public static string GetKey(string format,string index) 482 | { 483 | return String.Format(format, index); 484 | } 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/DatatablesParser/DatatablesParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | DatatablesParser 6 | DatatablesParser-core 7 | 1.5.0 8 | false 9 | C# Datatables Datatables.net javascript parser json Linq entity framework asp.net mvc grid table database query builder core 10 | https://github.com/garvincasimir/csharp-datatables-parser 11 | C# DataTables.net Parser Core 12 | Garvin Casimir 13 | 14 | A C# Server side component for the popuplar jQuery datatables plugin http://datatables.net/ suitable for asp.net core and other netstandard frameworks 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using websample.Models; 8 | using DataTablesParser; 9 | 10 | namespace websample.Controllers 11 | { 12 | public class HomeController : Controller 13 | { 14 | private readonly PersonContext _context; 15 | 16 | public HomeController(PersonContext context) 17 | { 18 | _context = context; 19 | } 20 | public IActionResult Index() 21 | { 22 | return View(); 23 | } 24 | 25 | public IActionResult About() 26 | { 27 | ViewData["Message"] = "Your application description page."; 28 | 29 | return View(); 30 | } 31 | 32 | public IActionResult Data() 33 | { 34 | var parser = new Parser(Request.Form, _context.People); 35 | 36 | return Json(parser.Parse()); 37 | } 38 | 39 | public IActionResult Error() 40 | { 41 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace websample.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /src/aspnet-core-sample/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System; 6 | 7 | namespace websample.Models 8 | { 9 | 10 | public class PersonContext : DbContext 11 | { 12 | public DbSet People { get; set; } 13 | 14 | public PersonContext(DbContextOptions options) 15 | : base(options) 16 | { } 17 | 18 | public PersonContext(){} 19 | } 20 | 21 | 22 | public class Person 23 | { 24 | [Key] 25 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 26 | public int Id { get; set; } 27 | public string FirstName { get; set; } 28 | public string LastName { get; set; } 29 | public DateTime BirthDate { get; set; } 30 | public decimal Weight { get; set; } 31 | public decimal Height { get; set; } 32 | public int Children { get; set; } 33 | 34 | [NotMapped] 35 | public string BirthDateFormatted 36 | { 37 | get 38 | { 39 | return string.Format("{0:M/d/yyyy}", BirthDate); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/aspnet-core-sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace websample 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using websample.Models; 10 | using Microsoft.EntityFrameworkCore; 11 | using Newtonsoft.Json.Serialization; 12 | 13 | namespace websample 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public IServiceProvider ConfigureServices(IServiceCollection services) 26 | { 27 | 28 | services.AddDbContext(options => options.UseInMemoryDatabase("aspnet-core-websample")); 29 | 30 | services.AddMvc() 31 | .AddJsonOptions(options => 32 | { 33 | options.SerializerSettings.ContractResolver = new DefaultContractResolver(); 34 | }); 35 | 36 | return services.BuildServiceProvider(); 37 | } 38 | 39 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 40 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider) 41 | { 42 | if (env.IsDevelopment()) 43 | { 44 | app.UseDeveloperExceptionPage(); 45 | } 46 | else 47 | { 48 | app.UseExceptionHandler("/Home/Error"); 49 | } 50 | 51 | 52 | var ctx = serviceProvider.GetService(); 53 | SeedSampleData(ctx); 54 | 55 | 56 | app.UseStaticFiles(); 57 | 58 | app.UseMvc(routes => 59 | { 60 | routes.MapRoute( 61 | name: "default", 62 | template: "{controller=Home}/{action=Index}/{id?}"); 63 | }); 64 | } 65 | 66 | private void SeedSampleData(PersonContext context) 67 | { 68 | var people = new List 69 | { 70 | new Person 71 | { 72 | FirstName = "James", 73 | LastName = "Jamie", 74 | BirthDate = DateTime.Parse("5/3/1960"), 75 | Children = 5, 76 | Height = 5.4M, 77 | Weight = 250M 78 | }, 79 | new Person 80 | { 81 | FirstName = "Tony", 82 | LastName = "Tonia", 83 | BirthDate = DateTime.Parse("7/3/1961"), 84 | Children = 3, 85 | Height = 4.4M, 86 | Weight = 150M 87 | }, 88 | new Person 89 | { 90 | FirstName = "Bandy", 91 | LastName = "Momin", 92 | BirthDate = DateTime.Parse("8/3/1970"), 93 | Children = 1, 94 | Height = 5.4M, 95 | Weight = 250M 96 | }, 97 | new Person 98 | { 99 | FirstName = "Tannie", 100 | LastName = "Tanner", 101 | BirthDate = DateTime.Parse("2/3/1950"), 102 | Children = 0, 103 | Height = 6.4M, 104 | Weight = 350M 105 | }, 106 | new Person 107 | { 108 | FirstName = "Cromie", 109 | LastName = "Crammer", 110 | BirthDate = DateTime.Parse("9/3/1953"), 111 | Children = 15, 112 | Height = 6.2M, 113 | Weight = 120M 114 | }, 115 | new Person 116 | { 117 | FirstName = "Xorie", 118 | LastName = "Zera", 119 | BirthDate = DateTime.Parse("10/3/1974"), 120 | Children = 2, 121 | Height = 5.9M, 122 | Weight = 175M 123 | } 124 | }; 125 | 126 | context.AddRange(people); 127 | context.SaveChanges(); 128 | 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["title"] = "People Table"; 3 | } 4 | 5 |

Index

6 |
7 | 8 | @section Scripts 9 | { 10 | 33 | 34 | 35 | } -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 22 |

23 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - websample 7 | 8 | 9 | 10 | 11 | 12 | 32 |
33 | @RenderBody() 34 |
35 |
36 |

© 2017 - websample

37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | @RenderSection("Scripts", required: false) 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using websample 2 | @using websample.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/aspnet-core-sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | true 6 | $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; 7 | aspnet-websample-9D568A68-34FE-46A1-9E3E-D339DAE3E740 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | run_cmd="dotnet run --server.urls http://*:80" 5 | 6 | exec $run_cmd -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Set widths on the form inputs since otherwise they're 100% wide */ 14 | input, 15 | select, 16 | textarea { 17 | max-width: 280px; 18 | } 19 | 20 | /* Carousel */ 21 | .carousel-caption p { 22 | font-size: 20px; 23 | line-height: 1.4; 24 | } 25 | 26 | /* Make .svg files in the carousel display properly in older browsers */ 27 | .carousel-inner .item img[src$=".svg"] { 28 | width: 100%; 29 | } 30 | 31 | /* Hide/rearrange for smaller screens */ 32 | @media screen and (max-width: 767px) { 33 | /* Hide captions */ 34 | .carousel-caption { 35 | display: none; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}input,select,textarea{max-width:280px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}@media screen and (max-width:767px){.carousel-caption{display:none}} 2 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garvincasimir/csharp-datatables-parser/879da8acaa5597486162fef5fe10f0ef7ae8ad7e/src/aspnet-core-sample/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/images/banner4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/aspnet-core-sample/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your JavaScript code. 2 | -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/DatatablesParser.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | DatatablesParser.Tests 5 | DatatablesParser.Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/EFlogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.IO; 4 | 5 | namespace DataTablesParser.Tests 6 | { 7 | public class EFlogger : ILoggerProvider 8 | { 9 | public ILogger CreateLogger(string categoryName) 10 | { 11 | return new MyLogger(); 12 | } 13 | 14 | public void Dispose() 15 | { } 16 | 17 | private class MyLogger : ILogger 18 | { 19 | public bool IsEnabled(LogLevel logLevel) 20 | { 21 | return true; 22 | } 23 | 24 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 25 | { 26 | 27 | Console.WriteLine(formatter(state, exception)); 28 | } 29 | 30 | public IDisposable BeginScope(TState state) 31 | { 32 | return null; 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/MssqlEntityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using DataTablesParser; 4 | using System.Linq; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace DataTablesParser.Tests 8 | { 9 | 10 | 11 | public class MssqlEntityTests 12 | { 13 | 14 | [Fact] 15 | public void TotalRecordsTest() 16 | { 17 | var context = TestHelper.GetMssqlContext(); 18 | 19 | var p = TestHelper.CreateParams(); 20 | 21 | var parser = new Parser(p, context.People); 22 | 23 | Console.WriteLine("Mssql - Total People TotalRecordsTest: {0}",context.People.Count()); 24 | 25 | Assert.Equal(context.People.Count(),parser.Parse().recordsTotal); 26 | 27 | } 28 | 29 | [Fact] 30 | public void TotalResultsTest() 31 | { 32 | var context = TestHelper.GetMssqlContext(); 33 | 34 | var p = TestHelper.CreateParams(); 35 | 36 | var resultLength = 3; 37 | 38 | //override display length 39 | p[Constants.DISPLAY_LENGTH] = new StringValues(Convert.ToString(resultLength)); 40 | 41 | var parser = new Parser(p, context.People); 42 | 43 | Console.WriteLine("Mssql - Total People TotalResultsTest: {0}",context.People.Count()); 44 | 45 | Assert.Equal(resultLength, parser.Parse().data.Count); 46 | 47 | } 48 | 49 | [Fact] 50 | public void TotalDisplayTest() 51 | { 52 | var context = TestHelper.GetMssqlContext(); 53 | var p = TestHelper.CreateParams(); 54 | var displayLength = 1; 55 | 56 | 57 | //Set filter parameter 58 | p[Constants.SEARCH_KEY] = new StringValues("Cromie"); 59 | 60 | var parser = new Parser(p, context.People); 61 | 62 | Console.WriteLine("Mssql - Total People TotalDisplayTest: {0}",context.People.Count()); 63 | 64 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 65 | 66 | } 67 | 68 | [Fact] 69 | public void TotalDisplayIndividualTest() 70 | { 71 | var context = TestHelper.GetMssqlContext(); 72 | var p = TestHelper.CreateParams(); 73 | var displayLength = 1; 74 | 75 | 76 | //Set filter parameter 77 | p[Constants.SEARCH_KEY] = new StringValues("a"); 78 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 79 | 80 | var parser = new Parser(p, context.People); 81 | 82 | Console.WriteLine("Mssql - Total People TotalDisplayIndividualTest: {0}",context.People.Count()); 83 | 84 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 85 | 86 | } 87 | 88 | [Fact] 89 | public void TotalDisplayIndividualMutiTest() 90 | { 91 | var context = TestHelper.GetInMemoryContext(); 92 | var p = TestHelper.CreateParams(); 93 | var displayLength = 1; 94 | 95 | 96 | //Set filter parameter 97 | p[Constants.SEARCH_KEY] = new StringValues("a"); 98 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "0")] = "omie"; 99 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 100 | 101 | var parser = new Parser(p, context.People); 102 | 103 | Console.WriteLine("Mssql - Total People TotalDisplayIndividualMutiTest: {0}",context.People.Count()); 104 | 105 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 106 | 107 | } 108 | 109 | [Fact] 110 | public void ResultsWhenSearchInNullColumnTest() 111 | { 112 | var context = TestHelper.GetMssqlContext(); 113 | var p = TestHelper.CreateParams(); 114 | var displayLength = 1; 115 | 116 | 117 | //Set filter parameter 118 | p[Constants.SEARCH_KEY] = new StringValues("Xorie"); 119 | 120 | var parser = new Parser(p, context.People); 121 | 122 | var result = parser.Parse().recordsFiltered; 123 | 124 | Console.WriteLine("MSSQL - Search one row whe some columns are null: {0}", result); 125 | 126 | Assert.Equal(displayLength, result); 127 | 128 | } 129 | 130 | 131 | } 132 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/MysqlEntityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using DataTablesParser; 4 | using System.Linq; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace DataTablesParser.Tests 8 | { 9 | 10 | public class MysqlEntityTests 11 | { 12 | 13 | [Fact] 14 | public void TotalRecordsTest() 15 | { 16 | var context = TestHelper.GetMysqlContext(); 17 | 18 | var p = TestHelper.CreateParams(); 19 | 20 | var parser = new Parser(p, context.People); 21 | 22 | Console.WriteLine("Mysql - Total People TotalRecordsTest: {0}",context.People.Count()); 23 | 24 | Assert.Equal(context.People.Count(),parser.Parse().recordsTotal); 25 | 26 | } 27 | 28 | [Fact] 29 | public void TotalResultsTest() 30 | { 31 | var context = TestHelper.GetMysqlContext(); 32 | 33 | var p = TestHelper.CreateParams(); 34 | 35 | var resultLength = 3; 36 | 37 | //override display length 38 | p[Constants.DISPLAY_LENGTH] = new StringValues(Convert.ToString(resultLength)); 39 | 40 | var parser = new Parser(p, context.People); 41 | 42 | Console.WriteLine("Mysql - Total People TotalResultsTest: {0}",context.People.Count()); 43 | 44 | Assert.Equal(resultLength, parser.Parse().data.Count); 45 | 46 | } 47 | 48 | [Fact] 49 | public void TotalDisplayTest() 50 | { 51 | var context = TestHelper.GetMysqlContext(); 52 | var p = TestHelper.CreateParams(); 53 | var displayLength = 1; 54 | 55 | 56 | //Set filter parameter 57 | p[Constants.SEARCH_KEY] = new StringValues("Cromie"); 58 | 59 | var parser = new Parser(p, context.People); 60 | 61 | Console.WriteLine("Mysql - Total People TotalDisplayTest: {0}",context.People.Count()); 62 | 63 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 64 | 65 | } 66 | 67 | 68 | [Fact] 69 | public void TotalDisplayIndividualTest() 70 | { 71 | var context = TestHelper.GetMysqlContext(); 72 | var p = TestHelper.CreateParams(); 73 | var displayLength = 1; 74 | 75 | 76 | //Set filter parameter 77 | p[Constants.SEARCH_KEY] = new StringValues("a"); 78 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 79 | 80 | var parser = new Parser(p, context.People); 81 | 82 | Console.WriteLine("MySql - Total People TotalDisplayIndividualTest: {0}",context.People.Count()); 83 | 84 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 85 | 86 | } 87 | 88 | [Fact] 89 | public void TotalDisplayIndividualMutiTest() 90 | { 91 | var context = TestHelper.GetMysqlContext(); 92 | var p = TestHelper.CreateParams(); 93 | var displayLength = 1; 94 | 95 | 96 | //Set filter parameter 97 | p[Constants.SEARCH_KEY] = new StringValues("a"); 98 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "0")] = "omie"; 99 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 100 | 101 | var parser = new Parser(p, context.People); 102 | 103 | Console.WriteLine("MySql - Total People TotalDisplayIndividualMutiTest: {0}",context.People.Count()); 104 | 105 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 106 | 107 | } 108 | 109 | [Fact] 110 | public void ResultsWhenSearchInNullColumnTest() 111 | { 112 | var context = TestHelper.GetMysqlContext(); 113 | var p = TestHelper.CreateParams(); 114 | var displayLength = 1; 115 | 116 | 117 | //Set filter parameter 118 | p[Constants.SEARCH_KEY] = new StringValues("Xorie"); 119 | 120 | var parser = new Parser(p, context.People); 121 | 122 | var result = parser.Parse().recordsFiltered; 123 | 124 | Console.WriteLine("MySql - Search one row whe some columns are null: {0}", result); 125 | 126 | Assert.Equal(displayLength, result); 127 | 128 | } 129 | 130 | [Fact] 131 | public void AddCustomFilterTest() 132 | { 133 | var context = TestHelper.GetMysqlContext(); 134 | var p = TestHelper.CreateParams(); 135 | var displayLength = 2; // James and Tony 136 | 137 | 138 | var parser = new Parser(p, context.People); // p is empty, all rows 139 | 140 | var minDate = DateTime.Parse("1960-01-01"); 141 | var maxDate = DateTime.Parse("1970-01-01"); 142 | parser.AddCustomFilter(x => x.BirthDate >= minDate); 143 | parser.AddCustomFilter(x => x.BirthDate < maxDate); 144 | 145 | var result = parser.Parse().recordsFiltered; 146 | 147 | Console.WriteLine("MySql - Search only born between 1960 and 1970: {0}", result); 148 | 149 | Assert.Equal(displayLength, result); 150 | 151 | } 152 | 153 | } 154 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/ParameterTests.cs: -------------------------------------------------------------------------------- 1 | using DataTablesParser; 2 | using Xunit; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace DataTablesParser.Tests 6 | { 7 | 8 | public class ParameterTests 9 | { 10 | [Fact] 11 | public void TestColumnPropertyPattern() 12 | { 13 | var key = "columns[0][data]"; 14 | 15 | var result = Regex.IsMatch(key, Constants.COLUMN_PROPERTY_PATTERN); 16 | 17 | Assert.True(result); 18 | 19 | } 20 | 21 | [Fact] 22 | public void TestOrderPattern() 23 | { 24 | var key = "order[0][column]"; 25 | 26 | var result = Regex.IsMatch(key, Constants.ORDER_PATTERN); 27 | 28 | Assert.True(result); 29 | 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace DataTablesParser.Tests 5 | { 6 | public class Person 7 | { 8 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 9 | public int Id { get; set; } 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public DateTime BirthDate { get; set; } 13 | public decimal Weight { get; set; } 14 | public decimal height { get; set; } 15 | public int Children { get; set; } 16 | public long TotalRedBloodCells { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/PersonContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | 4 | namespace DataTablesParser.Tests 5 | { 6 | public class PersonContext : DbContext 7 | { 8 | public PersonContext(){ } 9 | 10 | public PersonContext(DbContextOptions options) 11 | : base(options){ } 12 | 13 | public DbSet People { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/PgsqlEntityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using DataTablesParser; 4 | using System.Linq; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace DataTablesParser.Tests 8 | { 9 | 10 | public class PgsqlEntityTests 11 | { 12 | 13 | [Fact] 14 | public void TotalRecordsTest() 15 | { 16 | var context = TestHelper.GetPgsqlContext(); 17 | 18 | var p = TestHelper.CreateParams(); 19 | 20 | var parser = new Parser(p, context.People); 21 | 22 | Console.WriteLine("Pgsql - Total People TotalRecordsTest: {0}",context.People.Count()); 23 | 24 | Assert.Equal(context.People.Count(),parser.Parse().recordsTotal); 25 | 26 | } 27 | 28 | [Fact] 29 | public void TotalResultsTest() 30 | { 31 | var context = TestHelper.GetPgsqlContext(); 32 | 33 | var p = TestHelper.CreateParams(); 34 | 35 | var resultLength = 3; 36 | 37 | //override display length 38 | p[Constants.DISPLAY_LENGTH] = new StringValues(Convert.ToString(resultLength)); 39 | 40 | var parser = new Parser(p, context.People); 41 | 42 | Console.WriteLine("Pgsql - Total People TotalResultsTest: {0}",context.People.Count()); 43 | 44 | Assert.Equal(resultLength, parser.Parse().data.Count); 45 | 46 | } 47 | 48 | [Fact] 49 | public void TotalDisplayTest() 50 | { 51 | var context = TestHelper.GetPgsqlContext(); 52 | var p = TestHelper.CreateParams(); 53 | var displayLength = 1; 54 | 55 | 56 | //Set filter parameter 57 | p[Constants.SEARCH_KEY] = new StringValues("Cromie"); 58 | 59 | var parser = new Parser(p, context.People); 60 | 61 | Console.WriteLine("Pgsql - Total People TotalDisplayTest: {0}",context.People.Count()); 62 | 63 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 64 | 65 | } 66 | 67 | [Fact] 68 | public void TotalDisplayIndividualTest() 69 | { 70 | var context = TestHelper.GetPgsqlContext(); 71 | var p = TestHelper.CreateParams(); 72 | var displayLength = 1; 73 | 74 | 75 | //Set filter parameter 76 | p[Constants.SEARCH_KEY] = new StringValues("a"); 77 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 78 | 79 | var parser = new Parser(p, context.People); 80 | 81 | Console.WriteLine("Pgsql - Total People TotalDisplayIndividualTest: {0}",context.People.Count()); 82 | 83 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 84 | 85 | } 86 | 87 | 88 | [Fact] 89 | public void TotalDisplayIndividualMutiTest() 90 | { 91 | var context = TestHelper.GetInMemoryContext(); 92 | var p = TestHelper.CreateParams(); 93 | var displayLength = 1; 94 | 95 | 96 | //Set filter parameter 97 | p[Constants.SEARCH_KEY] = new StringValues("a"); 98 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "0")] = "omie"; 99 | p[Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1")] = "mmer"; 100 | 101 | var parser = new Parser(p, context.People); 102 | 103 | Console.WriteLine("Pgsql - Total People TotalDisplayIndividualMutiTest: {0}",context.People.Count()); 104 | 105 | Assert.Equal(displayLength, parser.Parse().recordsFiltered); 106 | 107 | } 108 | 109 | [Fact] 110 | public void ResultsWhenSearchInNullColumnTest() 111 | { 112 | var context = TestHelper.GetPgsqlContext(); 113 | var p = TestHelper.CreateParams(); 114 | var displayLength = 1; 115 | 116 | 117 | //Set filter parameter 118 | p[Constants.SEARCH_KEY] = new StringValues("Xorie"); 119 | 120 | var parser = new Parser(p, context.People); 121 | 122 | var result = parser.Parse().recordsFiltered; 123 | 124 | Console.WriteLine("PgSQL - Search one row whe some columns are null: {0}", result); 125 | 126 | Assert.Equal(displayLength, result); 127 | 128 | } 129 | 130 | 131 | } 132 | } -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Primitives; 4 | using System.Linq; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace DataTablesParser.Tests 10 | { 11 | public class TestHelper 12 | { 13 | private static Dictionary Params = new Dictionary(); 14 | private static List people = new List 15 | { 16 | new Person 17 | { 18 | FirstName = "James", 19 | LastName = "Jamie", 20 | BirthDate = DateTime.Parse("5/3/1960"), 21 | Children = 5, 22 | height = 5.4M, 23 | Weight = 250M 24 | }, 25 | new Person 26 | { 27 | FirstName = "Tony", 28 | LastName = "Tonia", 29 | BirthDate = DateTime.Parse("7/3/1961"), 30 | Children = 3, 31 | height = 4.4M, 32 | Weight = 150M 33 | }, 34 | new Person 35 | { 36 | FirstName = "Bandy", 37 | LastName = "Momin", 38 | BirthDate = DateTime.Parse("8/3/1970"), 39 | Children = 1, 40 | height = 5.4M, 41 | Weight = 250M 42 | }, 43 | new Person 44 | { 45 | FirstName = "Tannie", 46 | LastName = "Tanner", 47 | BirthDate = DateTime.Parse("2/3/1950"), 48 | Children = 0, 49 | height = 6.4M, 50 | Weight = 350M 51 | }, 52 | new Person 53 | { 54 | FirstName = "Cromie", 55 | LastName = "Crammer", 56 | BirthDate = DateTime.Parse("9/3/1953"), 57 | Children = 15, 58 | height = 6.2M, 59 | Weight = 120M 60 | }, 61 | new Person 62 | { 63 | FirstName = "Xorie", 64 | LastName = null, // "Zera", for search in null column test 65 | BirthDate = DateTime.Parse("10/3/1974"), 66 | Children = 2, 67 | height = 5.9M, 68 | Weight = 175M 69 | } 70 | }; 71 | 72 | 73 | static TestHelper() 74 | { 75 | Add(Constants.DRAW, "1"); 76 | Add(Constants.DISPLAY_START, "0"); 77 | Add(Constants.DISPLAY_LENGTH, "10"); 78 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "0"), "FirstName"); 79 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "0"), "true"); 80 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "0"), ""); 81 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "0"), "false"); 82 | 83 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "1"), "LastName"); 84 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "1"), "true"); 85 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "1"), ""); 86 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "1"), "false"); 87 | 88 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "2"), "BirthDate"); 89 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "2"), "true"); 90 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "2"), ""); 91 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "2"), "false"); 92 | 93 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "3"), "Weight"); 94 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "3"), "true"); 95 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "3"), ""); 96 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "3"), "false"); 97 | 98 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "4"), "Height"); 99 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "4"), "true"); 100 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "4"), ""); 101 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "4"), "false"); 102 | 103 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "5"), "Children"); 104 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "5"), "true"); 105 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "5"), ""); 106 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "5"), "false"); 107 | 108 | Add(Constants.GetKey(Constants.DATA_PROPERTY_FORMAT, "6"), "TotalRedBloodCells"); 109 | Add(Constants.GetKey(Constants.SEARCHABLE_PROPERTY_FORMAT, "6"), "true"); 110 | Add(Constants.GetKey(Constants.SEARCH_VALUE_PROPERTY_FORMAT, "6"), ""); 111 | Add(Constants.GetKey(Constants.SEARCH_REGEX_PROPERTY_FORMAT, "6"), "false"); 112 | 113 | Add(Constants.SEARCH_KEY, ""); 114 | Add(Constants.SEARCH_REGEX_KEY, "false"); 115 | 116 | Add(Constants.GetKey(Constants.ORDER_COLUMN_FORMAT, "0"), "0"); 117 | Add(Constants.GetKey(Constants.ORDER_DIRECTION_FORMAT, "0"), "0"); 118 | 119 | } 120 | 121 | public static Dictionary CreateParams() 122 | { 123 | return Params.ToDictionary(k => k.Key,v => v.Value); 124 | } 125 | 126 | public static IEnumerable CreateData() 127 | { 128 | return from p in people select new Person 129 | { 130 | FirstName = p.FirstName, 131 | LastName = p.LastName, 132 | BirthDate = p.BirthDate, 133 | Children = p.Children, 134 | height = p.height, 135 | Weight = p.Weight, 136 | TotalRedBloodCells = p.TotalRedBloodCells 137 | }; 138 | 139 | } 140 | 141 | private static void Add(string key,string value) 142 | { 143 | 144 | Params.Add(key,new StringValues(value)); 145 | } 146 | public static readonly ILoggerFactory consoleLogger 147 | = new LoggerFactory(new[] { 148 | new EFlogger() 149 | }); 150 | public static PersonContext GetInMemoryContext() 151 | { 152 | 153 | var serviceProvider = new ServiceCollection() 154 | .AddEntityFrameworkInMemoryDatabase() 155 | .AddSingleton(consoleLogger) 156 | .AddLogging() 157 | .BuildServiceProvider(); 158 | 159 | 160 | var builder = new DbContextOptionsBuilder(); 161 | builder.UseInMemoryDatabase("testdb") 162 | .UseInternalServiceProvider(serviceProvider); 163 | 164 | 165 | var context = new PersonContext(builder.Options); 166 | 167 | context.People.AddRange(CreateData()); 168 | context.SaveChanges(); 169 | return context; 170 | 171 | } 172 | 173 | public static PersonContext GetMysqlContext() 174 | { 175 | 176 | var serviceProvider = new ServiceCollection() 177 | .AddEntityFrameworkMySql() 178 | .AddSingleton(consoleLogger) 179 | .AddLogging() 180 | .BuildServiceProvider(); 181 | 182 | var builder = new DbContextOptionsBuilder(); 183 | builder.UseMySql(@"server=mysql;database=dotnettest;user=tester;password=Rea11ytrong_3") 184 | .UseInternalServiceProvider(serviceProvider); 185 | 186 | 187 | var context = new PersonContext(builder.Options); 188 | 189 | context.Database.EnsureCreated(); 190 | context.Database.ExecuteSqlRaw("truncate table People;"); 191 | 192 | context.People.AddRange(CreateData()); 193 | context.SaveChanges(); 194 | 195 | return context; 196 | 197 | } 198 | 199 | public static PersonContext GetPgsqlContext() 200 | { 201 | 202 | var serviceProvider = new ServiceCollection() 203 | .AddEntityFrameworkNpgsql() 204 | .AddSingleton(consoleLogger) 205 | .AddLogging() 206 | .BuildServiceProvider(); 207 | 208 | var builder = new DbContextOptionsBuilder(); 209 | builder.UseNpgsql(@"Host=pgsql;Database=dotnettest;User ID=tester;Password=Rea11ytrong_3") 210 | .UseInternalServiceProvider(serviceProvider); 211 | 212 | 213 | var context = new PersonContext(builder.Options); 214 | 215 | context.Database.EnsureCreated(); 216 | context.Database.ExecuteSqlRaw("truncate table public.\"People\";"); 217 | 218 | context.People.AddRange(CreateData()); 219 | context.SaveChanges(); 220 | 221 | return context; 222 | 223 | } 224 | 225 | 226 | public static PersonContext GetMssqlContext() 227 | { 228 | 229 | var serviceProvider = new ServiceCollection() 230 | .AddEntityFrameworkSqlServer() 231 | .AddSingleton(consoleLogger) 232 | .AddLogging() 233 | .BuildServiceProvider(); 234 | 235 | var builder = new DbContextOptionsBuilder(); 236 | builder.UseSqlServer(@"Data Source=mssql;Initial Catalog=TestNetCoreEF;user id=sa;password=Rea11ytrong_3") 237 | .UseInternalServiceProvider(serviceProvider); 238 | 239 | 240 | 241 | var context = new PersonContext(builder.Options); 242 | context.Database.EnsureCreated(); 243 | context.Database.ExecuteSqlRaw("truncate table People;"); 244 | 245 | context.People.AddRange(CreateData()); 246 | context.SaveChanges(); 247 | return context; 248 | 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /test/DatatablesParser.Tests/test-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Restore and build while db servers are starting" 4 | 5 | dotnet restore 6 | dotnet build 7 | echo "Testing connections to test db servers" 8 | while ! ( nc -w 1 mssql 1433 &> /dev/null && nc -w 1 mysql 3306 &> /dev/null && nc -w 1 pgsql 5432 &> /dev/null) ; do 9 | sleep 3; 10 | echo "Test db servers not ready. Trying again" 11 | done 12 | echo "Test DB Servers started" 13 | dotnet test --------------------------------------------------------------------------------