From 6082c8513e8efc6b0a6f4c840b6f702c90023b88 Mon Sep 17 00:00:00 2001 From: David Moreira Date: Thu, 19 Dec 2024 11:19:47 +0000 Subject: [PATCH] DataGrid: add ExpressionCompiler utility (#5872) * DataGrid ExpressionCompiler ongoing * ExpressionCompiler | Segregate into Sort | Search | Paging | Improve DataGridPage filter example * Add Expression support for less than or greater than * ExpressionCompiler | Between Support * Docs and Release notes * Add DataGrid ExpressionCompiler support for DataGridReadDataEventArgs * Format fix * Fix grammar --------- Co-authored-by: Mladen Macanovic --- .../Pages/Tests/DataGrid/DataGridPage.razor | 9 +- .../Tests/DataGrid/DataGridPage.razor.cs | 86 +- .../Models/Snippets.generated.cs | 65 ++ .../DataGrid/BindingData/LargeDataPage.razor | 15 + ...argeDataExpressionCompilerExampleCode.html | 69 ++ ...idLargeDataExpressionCompilerExample.razor | 65 ++ .../News/2025-05-01-release-notes-200.razor | 12 + .../Blazorise.DataGrid/DataGridColumn.cs | 3 +- .../Models/DataGridColumnInfo.cs | 9 +- .../Utils/ExpressionCompiler.cs | 803 ++++++++++++++++++ .../Utils/FunctionCompiler.cs | 162 +--- 11 files changed, 1057 insertions(+), 241 deletions(-) create mode 100644 Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Code/DataGridLargeDataExpressionCompilerExampleCode.html create mode 100644 Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Examples/DataGridLargeDataExpressionCompilerExample.razor create mode 100644 Source/Extensions/Blazorise.DataGrid/Utils/ExpressionCompiler.cs diff --git a/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor b/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor index b6096b0088..05fa91ba51 100644 --- a/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor +++ b/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor @@ -244,8 +244,8 @@ RowUpdating="@OnRowUpdating" RowRemoving="@OnRowRemoving" UseInternalEditing="false" - @bind-SelectedRow="@selectedEmployee" - @bind-SelectedRows="@selectedEmployees" + @bind-SelectedRow="@selectedEmployee" + @bind-SelectedRows="@selectedEmployees" NewItemDefaultSetter="@OnEmployeeNewItemDefaultSetter" DetailRowTrigger="@((e)=> e.Item.Salaries?.Count > 0 && e.Item.Id == selectedEmployee?.Id)" Striped @@ -254,7 +254,7 @@ Responsive ValidationsSummaryLabel="Following error occurs..." CustomFilter="@OnCustomFilter" - PageSize="5" + @bind-PageSize="pageSize" CurrentPage="currentPage" PageChanged="(e) => currentPage = e.Page" FilteredDataChanged="@OnFilteredDataChanged" @@ -354,8 +354,7 @@ - - + diff --git a/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor.cs b/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor.cs index a919ed91d4..18252aa769 100644 --- a/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor.cs +++ b/Demos/Blazorise.Demo/Pages/Tests/DataGrid/DataGridPage.razor.cs @@ -28,6 +28,7 @@ public partial class DataGridPage private DataGrid dataGrid; public int currentPage { get; set; } = 1; + public int pageSize { get; set; } = 5; private bool editable = true; private bool fixedHeader = false; @@ -203,73 +204,22 @@ private async Task OnReadData( DataGridReadDataEventArgs e ) { if ( !e.CancellationToken.IsCancellationRequested ) { - List response = null; + var query = dataModels.AsQueryable().ApplyDataGridSort( e ).ApplyDataGridSearch( e ); - var filteredData = await FilterData( e.Columns ); + if ( dataGrid.CustomFilter is not null ) + query = query.Where( item => item != null && dataGrid.CustomFilter( item ) ); - // this can be call to anything, in this case we're calling a fictional api - if ( e.ReadDataMode is DataGridReadDataMode.Virtualize ) - response = filteredData.Skip( e.VirtualizeOffset ).Take( e.VirtualizeCount ).ToList(); - else if ( e.ReadDataMode is DataGridReadDataMode.Paging ) - response = filteredData.Skip( ( e.Page - 1 ) * e.PageSize ).Take( e.PageSize ).ToList(); - else - throw new Exception( "Unhandled ReadDataMode" ); + var response = new List(); + response = query.ApplyDataGridPaging( e ).ToList(); await Task.Delay( random.Next( 100 ) ); - if ( !e.CancellationToken.IsCancellationRequested ) - { - totalEmployees = filteredData.Count; - employeeList = new List( response ); // an actual data for the current page - } - } - } - /// - /// Simple demo purpose example filter - /// - /// - /// - public Task> FilterData( IEnumerable dataGridColumns ) - { - var filteredData = dataModels.ToList(); - var sortByColumns = dataGridColumns.Where( x => x.SortDirection != SortDirection.Default ); - var firstSort = true; - if ( sortByColumns?.Any() ?? false ) - { - IOrderedEnumerable sortedCols = null; - foreach ( var sortByColumn in sortByColumns.OrderBy( x => x.SortIndex ) ) + if ( !e.CancellationToken.IsCancellationRequested ) { - var valueGetter = FunctionCompiler.CreateValueGetter( sortByColumn.SortField ); - - if ( firstSort ) - { - if ( sortByColumn.SortDirection == SortDirection.Ascending ) - sortedCols = dataModels.OrderBy( x => valueGetter( x ) ); - else - sortedCols = dataModels.OrderByDescending( x => valueGetter( x ) ); - - firstSort = false; - } - else - { - if ( sortByColumn.SortDirection == SortDirection.Ascending ) - sortedCols = sortedCols.ThenBy( x => valueGetter( x ) ); - else - sortedCols = sortedCols.ThenByDescending( x => valueGetter( x ) ); - } + totalEmployees = query.Count(); + employeeList = response; } - filteredData = sortedCols.ToList(); - } - - if ( dataGrid.CustomFilter != null ) - filteredData = filteredData.Where( item => item != null && dataGrid.CustomFilter( item ) ).ToList(); - - foreach ( var column in dataGridColumns.Where( x => !string.IsNullOrWhiteSpace( x.SearchValue?.ToString() ) ) ) - { - var valueGetter = FunctionCompiler.CreateValueGetter( column.Field ); - filteredData = filteredData.Where( x => valueGetter( x )?.ToString().IndexOf( column.SearchValue.ToString(), StringComparison.OrdinalIgnoreCase ) >= 0 ).ToList(); } - return Task.FromResult( filteredData ); } private Task Reset() @@ -301,23 +251,5 @@ private void OnSortChanged( DataGridSortChangedEventArgs eventArgs ) Console.WriteLine( $"Sort changed > Field: {eventArgs.ColumnFieldName}{sort}; Direction: {eventArgs.SortDirection};" ); } - private string TitleFromGender( string gender ) - { - return gender switch - { - "M" => "Mr.", - "F" => "Mrs.", - _ => string.Empty, - }; - } - - private string TitleToName( string title, object name ) - { - if ( string.IsNullOrEmpty( title ) ) - return $"{name}"; - - return $"{title} {name}"; - } - #endregion } \ No newline at end of file diff --git a/Documentation/Blazorise.Docs/Models/Snippets.generated.cs b/Documentation/Blazorise.Docs/Models/Snippets.generated.cs index 7fbbeceddb..62f72fe135 100644 --- a/Documentation/Blazorise.Docs/Models/Snippets.generated.cs +++ b/Documentation/Blazorise.Docs/Models/Snippets.generated.cs @@ -8501,6 +8501,71 @@ private async Task OnReadData( DataGridReadDataEventArgs e ) } }"; + public const string DataGridLargeDataExpressionCompilerExample = @"@using Blazorise.DataGrid.Extensions; +@using Blazorise.DataGrid.Utils + + + + + + ((Gender)x).Code"" TextField=""(x) => ((Gender)x).Description"" /> + + + + +@code { + [Inject] public EmployeeData EmployeeData { get; set; } + private DataGrid dataGridRef; + private List employeeListSource; + private List employeeList; + + protected override async Task OnInitializedAsync() + { + employeeListSource = await EmployeeData.GetDataAsync(); + await base.OnInitializedAsync(); + } + + private int totalEmployees; + + private async Task OnReadData(DataGridReadDataEventArgs e) + { + + if (!e.CancellationToken.IsCancellationRequested) + { + var query = employeeListSource.AsQueryable().ApplyDataGridSort(e.Columns).ApplyDataGridSearch(e.Columns); + + if (dataGridRef.CustomFilter is not null) + query = query.Where(item => item != null && dataGridRef.CustomFilter(item)); + + var response = new List(); + + if (e.ReadDataMode is DataGridReadDataMode.Virtualize) + response = query.ApplyDataGridPaging(e.VirtualizeOffset + 1, e.VirtualizeCount).ToList(); + else if (e.ReadDataMode is DataGridReadDataMode.Paging) + response = query.ApplyDataGridPaging(e.Page, e.PageSize).ToList(); + else + throw new Exception(""Unhandled ReadDataMode""); + + await Task.Delay(Random.Shared.Next(100)); + + if (!e.CancellationToken.IsCancellationRequested) + { + totalEmployees = query.Count(); + employeeList = response; + } + } + } +}"; + public const string DataGridLoadingEmptyTemplateExample = @" + + + + This utility enables you to compile expressions and use them in conjunction with the DataGrid ReadData() to build an IQueryable for querying your data. + + + The ExpressionCompiler can work alongside the DataGridColumnInfo collection provided by ReadData to create expression-based queries (IQueryables) that can be utilized in ORMs like Entity Framework. + + + + + + + + \ No newline at end of file diff --git a/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Code/DataGridLargeDataExpressionCompilerExampleCode.html b/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Code/DataGridLargeDataExpressionCompilerExampleCode.html new file mode 100644 index 0000000000..4c3c554766 --- /dev/null +++ b/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Code/DataGridLargeDataExpressionCompilerExampleCode.html @@ -0,0 +1,69 @@ +
+
+@using Blazorise.DataGrid.Extensions;
+@using Blazorise.DataGrid.Utils
+
+<DataGrid @ref=dataGridRef
+          TItem="Employee"
+          Data="@employeeList"
+          ReadData="@OnReadData"
+          TotalItems="@totalEmployees"
+          PageSize="10"
+          ShowPager
+          Responsive
+          Filterable
+          FilterMode="DataGridFilterMode.Menu">
+    <DataGridCommandColumn />
+    <DataGridColumn Field="@nameof(Employee.FirstName)" Caption="First Name" Editable />
+    <DataGridColumn Field="@nameof(Employee.LastName)" Caption="Last Name" Editable />
+    <DataGridSelectColumn TItem="Employee" Field="@nameof( Employee.Gender )" Caption="Gender" Editable Data="EmployeeData.Genders" ValueField="(x) => ((Gender)x).Code" TextField="(x) => ((Gender)x).Description" />
+    <DataGridNumericColumn Field="@nameof( Employee.Childrens )" Caption="Childrens" Editable />
+    <DataGridDateColumn Field="@nameof( Employee.DateOfBirth )" DisplayFormat="{0:dd.MM.yyyy}" Caption="Date Of Birth" Editable />
+</DataGrid>
+
+
+@code {
+    [Inject] public EmployeeData EmployeeData { get; set; }
+    private DataGrid<Employee> dataGridRef;
+    private List<Employee> employeeListSource;
+    private List<Employee> employeeList;
+
+    protected override async Task OnInitializedAsync()
+    {
+        employeeListSource = await EmployeeData.GetDataAsync();
+        await base.OnInitializedAsync();
+    }
+
+    private int totalEmployees;
+
+    private async Task OnReadData(DataGridReadDataEventArgs<Employee> e)
+    {
+
+        if (!e.CancellationToken.IsCancellationRequested)
+        {
+            var query = employeeListSource.AsQueryable().ApplyDataGridSort(e.Columns).ApplyDataGridSearch(e.Columns);
+
+            if (dataGridRef.CustomFilter is not null)
+                query = query.Where(item => item != null && dataGridRef.CustomFilter(item));
+
+            var response = new List<Employee>();
+
+            if (e.ReadDataMode is DataGridReadDataMode.Virtualize)
+                response = query.ApplyDataGridPaging(e.VirtualizeOffset + 1, e.VirtualizeCount).ToList();
+            else if (e.ReadDataMode is DataGridReadDataMode.Paging)
+                response = query.ApplyDataGridPaging(e.Page, e.PageSize).ToList();
+            else
+                throw new Exception("Unhandled ReadDataMode");
+
+            await Task.Delay(Random.Shared.Next(100));
+
+            if (!e.CancellationToken.IsCancellationRequested)
+            {
+                totalEmployees = query.Count();
+                employeeList = response;
+            }
+        }
+    }
+}
+
+
diff --git a/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Examples/DataGridLargeDataExpressionCompilerExample.razor b/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Examples/DataGridLargeDataExpressionCompilerExample.razor new file mode 100644 index 0000000000..f76a7c0ea0 --- /dev/null +++ b/Documentation/Blazorise.Docs/Pages/Docs/Extensions/DataGrid/Examples/DataGridLargeDataExpressionCompilerExample.razor @@ -0,0 +1,65 @@ +@using Blazorise.DataGrid.Extensions; +@using Blazorise.DataGrid.Utils +@namespace Blazorise.Docs.Docs.Examples + + + + + + + + + + +@code { + [Inject] public EmployeeData EmployeeData { get; set; } + private DataGrid dataGridRef; + private List employeeListSource; + private List employeeList; + + protected override async Task OnInitializedAsync() + { + employeeListSource = await EmployeeData.GetDataAsync(); + await base.OnInitializedAsync(); + } + + private int totalEmployees; + + private async Task OnReadData(DataGridReadDataEventArgs e) + { + + if (!e.CancellationToken.IsCancellationRequested) + { + var query = employeeListSource.AsQueryable().ApplyDataGridSort(e.Columns).ApplyDataGridSearch(e.Columns); + + if (dataGridRef.CustomFilter is not null) + query = query.Where(item => item != null && dataGridRef.CustomFilter(item)); + + var response = new List(); + + if (e.ReadDataMode is DataGridReadDataMode.Virtualize) + response = query.ApplyDataGridPaging(e.VirtualizeOffset + 1, e.VirtualizeCount).ToList(); + else if (e.ReadDataMode is DataGridReadDataMode.Paging) + response = query.ApplyDataGridPaging(e.Page, e.PageSize).ToList(); + else + throw new Exception("Unhandled ReadDataMode"); + + await Task.Delay(Random.Shared.Next(100)); + + if (!e.CancellationToken.IsCancellationRequested) + { + totalEmployees = query.Count(); + employeeList = response; + } + } + } +} \ No newline at end of file diff --git a/Documentation/Blazorise.Docs/Pages/News/2025-05-01-release-notes-200.razor b/Documentation/Blazorise.Docs/Pages/News/2025-05-01-release-notes-200.razor index a25f862da2..10a7cdcf5b 100644 --- a/Documentation/Blazorise.Docs/Pages/News/2025-05-01-release-notes-200.razor +++ b/Documentation/Blazorise.Docs/Pages/News/2025-05-01-release-notes-200.razor @@ -92,6 +92,18 @@ You may now allow multiple values to be selected in the DataGridSelectColumn. Set the new Multiple parameter to true to enable this feature. Please bind the corresponding array to successfully bind the multiple values. + + ExpressionCompiler + + + + A new DataGrid utility, the ExpressionCompiler was introduced. + This utility allows you to compile expressions and use them in the DataGrid. + The ExpressionCompiler can be used together with the DataGridColumnInfo collection provided by the ReadData in order to create Expression based queries (IQueryables) that can in turn be used in ORMs like Entity Framework. + + Visit DataGrid Binding Large Data for more information. + + Final Notes diff --git a/Source/Extensions/Blazorise.DataGrid/DataGridColumn.cs b/Source/Extensions/Blazorise.DataGrid/DataGridColumn.cs index 525f6f405b..15d5c6cf1f 100644 --- a/Source/Extensions/Blazorise.DataGrid/DataGridColumn.cs +++ b/Source/Extensions/Blazorise.DataGrid/DataGridColumn.cs @@ -69,7 +69,8 @@ internal DataGridColumnInfo ToColumnInfo( IList> sortByCol sortByColumns?.FirstOrDefault( sortCol => sortCol.IsEqual( this ) )?.SortOrder ?? -1, ColumnType, GetFieldToSort(), - GetFilterMethod() ?? GetDataGridFilterMethodAsColumn() ); + GetFilterMethod() ?? GetDataGridFilterMethodAsColumn(), + GetValueType( default ) ); } private Func ExpandoObjectTypeGetter() diff --git a/Source/Extensions/Blazorise.DataGrid/Models/DataGridColumnInfo.cs b/Source/Extensions/Blazorise.DataGrid/Models/DataGridColumnInfo.cs index 2412553a4f..2aa58e8071 100644 --- a/Source/Extensions/Blazorise.DataGrid/Models/DataGridColumnInfo.cs +++ b/Source/Extensions/Blazorise.DataGrid/Models/DataGridColumnInfo.cs @@ -19,7 +19,8 @@ public class DataGridColumnInfo /// Current column type. /// Sort field name. /// Filter method. - public DataGridColumnInfo( string field, object searchValue, SortDirection sortDirection, int sortIndex, DataGridColumnType columnType, string sortField, DataGridColumnFilterMethod? filterMethod ) + /// Value type. + public DataGridColumnInfo( string field, object searchValue, SortDirection sortDirection, int sortIndex, DataGridColumnType columnType, string sortField, DataGridColumnFilterMethod? filterMethod, Type valueType ) { Field = field; SearchValue = searchValue; @@ -28,6 +29,7 @@ public DataGridColumnInfo( string field, object searchValue, SortDirection sortD ColumnType = columnType; SortField = sortField; FilterMethod = filterMethod; + ValueType = valueType; } /// @@ -64,4 +66,9 @@ public DataGridColumnInfo( string field, object searchValue, SortDirection sortD /// Gets the column filter method. /// public DataGridColumnFilterMethod? FilterMethod { get; } + + /// + /// Gets the column determined value type. + /// + public Type ValueType { get; } } \ No newline at end of file diff --git a/Source/Extensions/Blazorise.DataGrid/Utils/ExpressionCompiler.cs b/Source/Extensions/Blazorise.DataGrid/Utils/ExpressionCompiler.cs new file mode 100644 index 0000000000..b452bc321b --- /dev/null +++ b/Source/Extensions/Blazorise.DataGrid/Utils/ExpressionCompiler.cs @@ -0,0 +1,803 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Blazorise.Extensions; +using Microsoft.AspNetCore.Components.Forms; + +namespace Blazorise.DataGrid.Utils; +public static class ExpressionCompiler +{ + /// + /// Applies all the DataGrid filters to the queryable data. + /// + /// The TItem + /// The Data to be queried + /// The DataGrid Columns Info + /// Optionally provide the page number + /// Optionally provide the page size + /// + public static IQueryable ApplyDataGridFilters( this IQueryable data, IEnumerable dataGridColumns, int page = 0, int pageSize = 0 ) + { + return data.ApplyDataGridSort( dataGridColumns ).ApplyDataGridSearch( dataGridColumns ).ApplyDataGridPaging( page, pageSize ); + } + + /// + /// Applies the search filter to the queryable data. + /// + /// + /// The Data to be queried + /// The DataGrid Columns Info + /// + public static IQueryable ApplyDataGridSearch( this IQueryable data, IEnumerable dataGridColumns ) + { + if ( dataGridColumns.IsNullOrEmpty() ) + return data; + + foreach ( var column in dataGridColumns.Where( x => !string.IsNullOrWhiteSpace( x.SearchValue?.ToString() ) ) ) + { + var filterMethod = column.FilterMethod ?? DataGridColumnFilterMethod.Contains; + + switch ( filterMethod ) + { + case DataGridColumnFilterMethod.Contains: + data = data.Where( GetWhereContainsExpression( column.Field, column.SearchValue.ToString() ) ); + break; + case DataGridColumnFilterMethod.StartsWith: + data = data.Where( GetWhereStartsWithExpression( column.Field, column.SearchValue.ToString() ) ); + break; + case DataGridColumnFilterMethod.EndsWith: + data = data.Where( GetWhereEndsWithExpression( column.Field, column.SearchValue.ToString() ) ); + break; + case DataGridColumnFilterMethod.Equals: + data = data.Where( GetWhereEqualsExpression( column.Field, column.SearchValue.ToString() ) ); + break; + case DataGridColumnFilterMethod.NotEquals: + data = data.Where( GetWhereNotEqualsExpression( column.Field, column.SearchValue.ToString() ) ); + break; + case DataGridColumnFilterMethod.LessThan: + if ( column.SearchValue is null ) + break; + + if ( column.ColumnType == DataGridColumnType.Numeric ) + { + if ( column.ValueType == typeof( decimal ) || column.ValueType == typeof( decimal? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, decimal.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( double ) || column.ValueType == typeof( double? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, double.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( float ) || column.ValueType == typeof( float? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, float.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( int ) || column.ValueType == typeof( int? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, int.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( short ) || column.ValueType == typeof( short? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, short.Parse( column.SearchValue.ToString() ) ) ); + } + else if ( column.ColumnType == DataGridColumnType.Date ) + { + if ( column.ValueType == typeof( DateTime ) || column.ValueType == typeof( DateTime? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, DateTime.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateTimeOffset ) || column.ValueType == typeof( DateTimeOffset? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, DateTimeOffset.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateOnly ) || column.ValueType == typeof( DateOnly? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, DateOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeOnly ) || column.ValueType == typeof( TimeOnly? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, TimeOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeSpan ) || column.ValueType == typeof( TimeSpan? ) ) + data = data.Where( GetWhereLessThanExpression( column.Field, TimeSpan.Parse( column.SearchValue.ToString() ) ) ); + } + break; + case DataGridColumnFilterMethod.LessThanOrEqual: + if ( column.SearchValue is null ) + break; + + if ( column.ColumnType == DataGridColumnType.Numeric ) + { + if ( column.ValueType == typeof( decimal ) || column.ValueType == typeof( decimal? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, decimal.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( double ) || column.ValueType == typeof( double? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, double.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( float ) || column.ValueType == typeof( float? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, float.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( int ) || column.ValueType == typeof( int? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, int.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( short ) || column.ValueType == typeof( short? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, short.Parse( column.SearchValue.ToString() ) ) ); + } + else if ( column.ColumnType == DataGridColumnType.Date ) + { + if ( column.ValueType == typeof( DateTime ) || column.ValueType == typeof( DateTime? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, DateTime.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateTimeOffset ) || column.ValueType == typeof( DateTimeOffset? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, DateTimeOffset.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateOnly ) || column.ValueType == typeof( DateOnly? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, DateOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeOnly ) || column.ValueType == typeof( TimeOnly? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, TimeOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeSpan ) || column.ValueType == typeof( TimeSpan? ) ) + data = data.Where( GetWhereLessThanOrEqualExpression( column.Field, TimeSpan.Parse( column.SearchValue.ToString() ) ) ); + } + break; + case DataGridColumnFilterMethod.GreaterThan: + if ( column.SearchValue is null ) + break; + + if ( column.ColumnType == DataGridColumnType.Numeric ) + { + if ( column.ValueType == typeof( decimal ) || column.ValueType == typeof( decimal? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, decimal.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( double ) || column.ValueType == typeof( double? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, double.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( float ) || column.ValueType == typeof( float? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, float.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( int ) || column.ValueType == typeof( int? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, int.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( short ) || column.ValueType == typeof( short? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, short.Parse( column.SearchValue.ToString() ) ) ); + } + else if ( column.ColumnType == DataGridColumnType.Date ) + { + if ( column.ValueType == typeof( DateTime ) || column.ValueType == typeof( DateTime? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, DateTime.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateTimeOffset ) || column.ValueType == typeof( DateTimeOffset? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, DateTimeOffset.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateOnly ) || column.ValueType == typeof( DateOnly? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, DateOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeOnly ) || column.ValueType == typeof( TimeOnly? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, TimeOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeSpan ) || column.ValueType == typeof( TimeSpan? ) ) + data = data.Where( GetWhereGreaterThanExpression( column.Field, TimeSpan.Parse( column.SearchValue.ToString() ) ) ); + } + break; + case DataGridColumnFilterMethod.GreaterThanOrEqual: + if ( column.SearchValue is null ) + break; + + if ( column.ColumnType == DataGridColumnType.Numeric ) + { + if ( column.ValueType == typeof( decimal ) || column.ValueType == typeof( decimal? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, decimal.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( double ) || column.ValueType == typeof( double? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, double.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( float ) || column.ValueType == typeof( float? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, float.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( int ) || column.ValueType == typeof( int? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, int.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( short ) || column.ValueType == typeof( short? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, short.Parse( column.SearchValue.ToString() ) ) ); + } + else if ( column.ColumnType == DataGridColumnType.Date ) + { + if ( column.ValueType == typeof( DateTime ) || column.ValueType == typeof( DateTime? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, DateTime.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateTimeOffset ) || column.ValueType == typeof( DateTimeOffset? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, DateTimeOffset.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( DateOnly ) || column.ValueType == typeof( DateOnly? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, DateOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeOnly ) || column.ValueType == typeof( TimeOnly? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, TimeOnly.Parse( column.SearchValue.ToString() ) ) ); + + if ( column.ValueType == typeof( TimeSpan ) || column.ValueType == typeof( TimeSpan? ) ) + data = data.Where( GetWhereGreaterThanOrEqualExpression( column.Field, TimeSpan.Parse( column.SearchValue.ToString() ) ) ); + } + break; + case DataGridColumnFilterMethod.Between: + + if ( column.SearchValue is not object[] rangeSearchValues || rangeSearchValues.Length < 2 ) + break; + + var stringSearchValue1 = rangeSearchValues[0]?.ToString(); + var stringSearchValue2 = rangeSearchValues[1]?.ToString(); + + if ( stringSearchValue1 is null || stringSearchValue2 is null ) + break; + + if ( column.ColumnType == DataGridColumnType.Numeric ) + { + if ( column.ValueType == typeof( decimal ) || column.ValueType == typeof( decimal? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, decimal.Parse( stringSearchValue1 ), decimal.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( double ) || column.ValueType == typeof( double? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, double.Parse( stringSearchValue1 ), double.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( float ) || column.ValueType == typeof( float? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, float.Parse( stringSearchValue1 ), float.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( int ) || column.ValueType == typeof( int? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, int.Parse( stringSearchValue1 ), int.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( short ) || column.ValueType == typeof( short? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, short.Parse( stringSearchValue1 ), short.Parse( stringSearchValue2 ) ) ); + } + else if ( column.ColumnType == DataGridColumnType.Date ) + { + if ( column.ValueType == typeof( DateTime ) || column.ValueType == typeof( DateTime? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, DateTime.Parse( stringSearchValue1 ), DateTime.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( DateTimeOffset ) || column.ValueType == typeof( DateTimeOffset? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, DateTimeOffset.Parse( stringSearchValue1 ), DateTimeOffset.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( DateOnly ) || column.ValueType == typeof( DateOnly? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, DateOnly.Parse( stringSearchValue1 ), DateOnly.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( TimeOnly ) || column.ValueType == typeof( TimeOnly? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, TimeOnly.Parse( stringSearchValue1 ), TimeOnly.Parse( stringSearchValue2 ) ) ); + + if ( column.ValueType == typeof( TimeSpan ) || column.ValueType == typeof( TimeSpan? ) ) + data = data.Where( GetWhereBetweenExpression( column.Field, TimeSpan.Parse( stringSearchValue1 ), TimeSpan.Parse( stringSearchValue2 ) ) ); + } + + break; + } + } + + return data; + } + + /// + /// Applies the sort filter to the queryable data. + /// + /// + /// The Data to be queried + /// The DataGrid Columns Info + /// + public static IQueryable ApplyDataGridSort( this IQueryable data, IEnumerable dataGridColumns ) + { + if ( dataGridColumns.IsNullOrEmpty() ) + return data; + + var sortByColumns = dataGridColumns.Where( x => x.SortDirection != SortDirection.Default ); + var firstSort = true; + if ( sortByColumns.Any() ) + { + + foreach ( var sortByColumn in sortByColumns.OrderBy( x => x.SortIndex ) ) + { + var valueGetterExpression = string.IsNullOrWhiteSpace( sortByColumn.SortField ) ? CreateValueGetterExpression( sortByColumn.Field ) : CreateValueGetterExpression( sortByColumn.SortField ); + + if ( firstSort ) + { + if ( sortByColumn.SortDirection == SortDirection.Ascending ) + data = data.OrderBy( valueGetterExpression ); + else + data = data.OrderByDescending( valueGetterExpression ); + + firstSort = false; + } + else + { + if ( sortByColumn.SortDirection == SortDirection.Ascending ) + data = ( data as IOrderedQueryable ).ThenBy( valueGetterExpression ); + else + data = ( data as IOrderedQueryable ).ThenByDescending( valueGetterExpression ); + } + } + } + return data; + } + + /// + /// Applies the paging filter to the queryable data. + /// + /// + /// The Data to be queried + /// The current page + /// The page size + /// + public static IQueryable ApplyDataGridPaging( this IQueryable data, int page, int pageSize ) + { + if ( page > 0 && pageSize > 0 ) + { + return data.Skip( ( page - 1 ) * pageSize ).Take( pageSize ); + } + + return data; + } + + /// + /// Applies all the DataGrid filters to the queryable data. + /// + /// The TItem + /// The Data to be queried + /// The DataGrid Read Data Event Arguments + /// + public static IQueryable ApplyDataGridFilters( this IQueryable data, DataGridReadDataEventArgs dataGridReadDataEventArgs ) + { + return data.ApplyDataGridSort( dataGridReadDataEventArgs ).ApplyDataGridSearch( dataGridReadDataEventArgs ).ApplyDataGridPaging( dataGridReadDataEventArgs ); + } + + /// + /// Applies the search filter to the queryable data. + /// + /// + /// The Data to be queried + /// The DataGrid Read Data Event Arguments + /// + public static IQueryable ApplyDataGridSearch( this IQueryable data, DataGridReadDataEventArgs dataGridReadDataEventArgs ) + { + return data.ApplyDataGridSearch( dataGridReadDataEventArgs.Columns ); + } + + /// + /// Applies the sort filter to the queryable data. + /// + /// + /// The Data to be queried + /// The DataGrid Read Data Event Arguments + /// + public static IQueryable ApplyDataGridSort( this IQueryable data, DataGridReadDataEventArgs dataGridReadDataEventArgs ) + { + return data.ApplyDataGridSort( dataGridReadDataEventArgs.Columns ); + } + + /// + /// Applies the paging filter to the queryable data. + /// + /// + /// The Data to be queried + /// The DataGrid Read Data Event Arguments + /// + public static IQueryable ApplyDataGridPaging( this IQueryable data, DataGridReadDataEventArgs dataGridReadDataEventArgs ) + { + if ( dataGridReadDataEventArgs.ReadDataMode is DataGridReadDataMode.Virtualize ) + data = data.ApplyDataGridPaging( dataGridReadDataEventArgs.VirtualizeOffset + 1, dataGridReadDataEventArgs.VirtualizeCount ); + else if ( dataGridReadDataEventArgs.ReadDataMode is DataGridReadDataMode.Paging ) + data = data.ApplyDataGridPaging( dataGridReadDataEventArgs.Page, dataGridReadDataEventArgs.PageSize ); + + return data; + } + + /// + /// Creates the lambda expression that is suitable for usage with Blazor . + /// + /// Type of model that contains the data-annotations. + /// Return type of validation field. + /// An actual instance of the validation model. + /// Field name to validate. + /// Expression compatible with parser. + public static Expression> CreateValidationGetterExpression( TItem item, string fieldName ) + { + var parameter = Expression.Parameter( typeof( TItem ), "item" ); + var property = ExpressionCompiler.GetPropertyOrFieldExpression( parameter, fieldName ); + var path = fieldName.Split( '.' ); + + //TODO : Couldn't this be done with an expression? + Func instanceGetter; + + if ( path.Length <= 1 ) + instanceGetter = ( item ) => item; + else + instanceGetter = FunctionCompiler.CreateValueGetter( string.Join( '.', path.Take( path.Length - 1 ) ) ); + + var convertExpression = Expression.MakeMemberAccess( Expression.Constant( instanceGetter( item ) ), property.Member ); + + return Expression.Lambda>( convertExpression ); + } + + /// + /// Builds an access expression for nested properties while checking for null values. + /// + /// Item that has the requested field name. + /// Item field name. + /// Returns the requested field if it exists. + public static Expression GetSafePropertyOrFieldExpression( Expression item, string propertyOrFieldName ) + { + if ( string.IsNullOrEmpty( propertyOrFieldName ) ) + throw new ArgumentException( $"{nameof( propertyOrFieldName )} is not specified." ); + + var parts = propertyOrFieldName.Split( new char[] { '.' }, 2 ); + + Expression field = null; + + MemberInfo memberInfo = GetSafeMember( item.Type, parts[0] ); + + if ( memberInfo is PropertyInfo propertyInfo ) + field = Expression.Property( item, propertyInfo ); + else if ( memberInfo is FieldInfo fieldInfo ) + field = Expression.Field( item, fieldInfo ); + + if ( field == null ) + throw new ArgumentException( $"Cannot detect the member of {item.Type}", propertyOrFieldName ); + + field = Expression.Condition( Expression.Equal( item, Expression.Default( item.Type ) ), + IsNullable( field.Type ) ? Expression.Constant( null, field.Type ) : Expression.Default( field.Type ), + field ); + + if ( parts.Length > 1 ) + field = GetSafePropertyOrFieldExpression( field, parts[1] ); + + return field; + } + + public static MemberExpression GetPropertyOrFieldExpression( Expression item, string propertyOrFieldName ) + { + if ( string.IsNullOrEmpty( propertyOrFieldName ) ) + throw new ArgumentException( $"{nameof( propertyOrFieldName )} is not specified." ); + + var parts = propertyOrFieldName.Split( new char[] { '.' }, 2 ); + + MemberExpression field = null; + + MemberInfo memberInfo = GetSafeMember( item.Type, parts[0] ); + + if ( memberInfo is PropertyInfo propertyInfo ) + field = Expression.Property( item, propertyInfo ); + else if ( memberInfo is FieldInfo fieldInfo ) + field = Expression.Field( item, fieldInfo ); + + if ( field == null ) + throw new ArgumentException( $"Cannot detect the member of {item.Type}", propertyOrFieldName ); + + if ( parts.Length > 1 ) + field = GetPropertyOrFieldExpression( field, parts[1] ); + + return field; + } + + public static Expression> CreateValueGetterExpression( string fieldName ) + { + var item = Expression.Parameter( typeof( TItem ), "item" ); + var property = GetSafePropertyOrFieldExpression( item, fieldName ); + return Expression.Lambda>( Expression.Convert( property, typeof( object ) ), item ); + } + + private static Expression ConvertToStringExpression( Expression property ) + { + Expression convert; + var propInfo = (PropertyInfo)( property as MemberExpression ).Member; + + if ( IsNullable( propInfo.PropertyType ) && ( propInfo.PropertyType != typeof( string ) ) ) + { + var hasValueExpression = Expression.Property( property, "HasValue" ); + var nullableValueExpression = Expression.Property( property, "Value" ); + + var trueExpression = Expression.Call( + typeof( Convert ).GetMethod( nameof( Convert.ToString ), new[] { propInfo.PropertyType.GenericTypeArguments[0] } ), + nullableValueExpression ); + + var falseExpression = Expression.Constant( "", typeof( string ) ); + convert = Expression.Condition( hasValueExpression, + trueExpression, + falseExpression ); + } + else if ( propInfo.PropertyType.IsEnum ) + { + var convertToInt = Expression.Convert( property, typeof( int ) ); + convert = Expression.Call( + typeof( Convert ).GetMethod( nameof( Convert.ToString ), new[] { typeof( int ) } ), + convertToInt ); + } + else + { + convert = Expression.Call( + typeof( Convert ).GetMethod( nameof( Convert.ToString ), new[] { propInfo.PropertyType } ), + property ); + } + + return convert; + } + + private static Expression ContainsExpression( Expression propertyExpression, string searchValue ) + { + Expression body = Expression.Call( + propertyExpression, + typeof( string ).GetMethod( nameof( string.Contains ), new[] { typeof( string ) } )!, + Expression.Constant( searchValue ) + ); + return body; + } + + private static Expression StartsWithExpression( Expression propertyExpression, string searchValue ) + { + Expression body = Expression.Call( + propertyExpression, + typeof( string ).GetMethod( nameof( string.StartsWith ), new[] { typeof( string ) } )!, + Expression.Constant( searchValue ) + ); + return body; + } + + private static Expression EndsWithExpression( Expression propertyExpression, string searchValue ) + { + Expression body = Expression.Call( + propertyExpression, + typeof( string ).GetMethod( nameof( string.EndsWith ), new[] { typeof( string ) } )!, + Expression.Constant( searchValue ) + ); + return body; + } + + private static Expression EqualsWithExpression( Expression propertyExpression, string searchValue ) + { + Expression body = Expression.Call( + propertyExpression, + typeof( string ).GetMethod( nameof( string.Equals ), new[] { typeof( string ) } )!, + Expression.Constant( searchValue ) + ); + return body; + } + + private static Expression NotEqualsWithExpression( Expression propertyExpression, string searchValue ) + { + return Expression.IsFalse( EqualsWithExpression( propertyExpression, searchValue ) ); + } + + /// + /// Builds a where expression. Where the source property contains the searchValue. + /// + /// + /// + /// + /// + public static Expression> GetWhereContainsExpression( + string sourceProperty, + string searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + Expression convertSourcePropertyExpression = ConvertToStringExpression( sourcePropertyExpression ); + Expression body = ContainsExpression( convertSourcePropertyExpression, searchValue ); + return Expression.Lambda>( body, sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property starts with the searchValue. + /// + /// + /// + /// + /// + public static Expression> GetWhereStartsWithExpression( + string sourceProperty, + string searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + Expression convertSourcePropertyExpression = ConvertToStringExpression( sourcePropertyExpression ); + Expression body = StartsWithExpression( convertSourcePropertyExpression, searchValue ); + return Expression.Lambda>( body, sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property ends with the searchValue. + /// + /// + /// + /// + /// + public static Expression> GetWhereEndsWithExpression( + string sourceProperty, + string searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + Expression convertSourcePropertyExpression = ConvertToStringExpression( sourcePropertyExpression ); + Expression body = EndsWithExpression( convertSourcePropertyExpression, searchValue ); + return Expression.Lambda>( body, sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property equals the searchValue. + /// + /// + /// + /// + /// + public static Expression> GetWhereEqualsExpression( + string sourceProperty, + string searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + Expression convertSourcePropertyExpression = ConvertToStringExpression( sourcePropertyExpression ); + Expression body = EqualsWithExpression( convertSourcePropertyExpression, searchValue ); + return Expression.Lambda>( body, sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property is not equal to the searchValue. + /// + /// + /// + /// + /// + public static Expression> GetWhereNotEqualsExpression( + string sourceProperty, + string searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + Expression convertSourcePropertyExpression = ConvertToStringExpression( sourcePropertyExpression ); + Expression body = NotEqualsWithExpression( convertSourcePropertyExpression, searchValue ); + return Expression.Lambda>( body, sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property is less than the searchValue. + /// + /// + /// + /// + /// + /// + public static Expression> GetWhereLessThanExpression( + string sourceProperty, + TValue searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + //Note : Another option, would be to not convert and assume the searchValue is of the same type as the source property? + var convertSourcePropertyExpression = Expression.Convert( sourcePropertyExpression, typeof( TValue ) ); + return Expression.Lambda>( Expression.LessThan( convertSourcePropertyExpression, Expression.Constant( searchValue ) ), sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property is less than or equal to the searchValue. + /// + /// + /// + /// + /// + /// + public static Expression> GetWhereLessThanOrEqualExpression( + string sourceProperty, + TValue searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + //Note : Another option, would be to not convert and assume the searchValue is of the same type as the source property? + var convertSourcePropertyExpression = Expression.Convert( sourcePropertyExpression, typeof( TValue ) ); + return Expression.Lambda>( Expression.LessThanOrEqual( convertSourcePropertyExpression, Expression.Constant( searchValue ) ), sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property greater than the searchValue. + /// + /// + /// + /// + /// + /// + public static Expression> GetWhereGreaterThanExpression( + string sourceProperty, + TValue searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + //Note : Another option, would be to not convert and assume the searchValue is of the same type as the source property? + var convertSourcePropertyExpression = Expression.Convert( sourcePropertyExpression, typeof( TValue ) ); + return Expression.Lambda>( Expression.GreaterThan( convertSourcePropertyExpression, Expression.Constant( searchValue ) ), sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property greater than or equal to the searchValue. + /// + /// + /// + /// + /// + /// + public static Expression> GetWhereGreaterThanOrEqualExpression( + string sourceProperty, + TValue searchValue ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + //Note : Another option, would be to not convert and assume the searchValue is of the same type as the source property? + var convertSourcePropertyExpression = Expression.Convert( sourcePropertyExpression, typeof( TValue ) ); + return Expression.Lambda>( Expression.GreaterThanOrEqual( convertSourcePropertyExpression, Expression.Constant( searchValue ) ), sourceParameterExpression ); + } + + /// + /// Builds a where expression. Where the source property is between two provided searchValues. + /// + /// + /// + /// + /// + /// + /// + public static Expression> GetWhereBetweenExpression( + string sourceProperty, + TValue searchValue1, + TValue searchValue2 ) + { + var sourceParameterExpression = GetParameterExpression(); + var sourcePropertyExpression = GetPropertyOrFieldExpression( sourceParameterExpression, sourceProperty ); + + //Note : Another option, would be to not convert and assume the searchValue is of the same type as the source property? + var convertSourcePropertyExpression = Expression.Convert( sourcePropertyExpression, typeof( TValue ) ); + + var value1GreaterThanOrEqualExpression = Expression.GreaterThanOrEqual( convertSourcePropertyExpression, Expression.Constant( searchValue1 ) ); + var value2LessThanOrEqualExpression = Expression.LessThanOrEqual( convertSourcePropertyExpression, Expression.Constant( searchValue2 ) ); + + return Expression.Lambda>( Expression.MakeBinary( ExpressionType.AndAlso, value1GreaterThanOrEqualExpression, value2LessThanOrEqualExpression ), sourceParameterExpression ); + } + + + /// + /// Checks if requested type can bu nullable. + /// + /// Object type. + /// + private static bool IsNullable( Type type ) + { + if ( type.IsClass ) + return true; + + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Nullable<> ); + } + + // inspired by: https://stackoverflow.com/questions/2496256/expression-tree-with-property-inheritance-causes-an-argument-exception + private static MemberInfo GetSafeMember( Type type, string fieldName ) + { + MemberInfo memberInfo = (MemberInfo)type.GetProperty( fieldName ) + ?? type.GetField( fieldName ); + + if ( memberInfo == null ) + { + var baseTypesAndInterfaces = new List(); + + if ( type.BaseType != null ) + { + baseTypesAndInterfaces.Add( type.BaseType ); + } + + baseTypesAndInterfaces.AddRange( type.GetInterfaces() ); + + foreach ( var baseType in baseTypesAndInterfaces ) + { + memberInfo = GetSafeMember( baseType, fieldName ); + + if ( memberInfo != null ) + break; + } + } + + return memberInfo; + } + + private static ParameterExpression GetParameterExpression() + => Expression.Parameter( typeof( TItem ), "item" ); + +} diff --git a/Source/Extensions/Blazorise.DataGrid/Utils/FunctionCompiler.cs b/Source/Extensions/Blazorise.DataGrid/Utils/FunctionCompiler.cs index e8b861706c..83a5c749df 100644 --- a/Source/Extensions/Blazorise.DataGrid/Utils/FunctionCompiler.cs +++ b/Source/Extensions/Blazorise.DataGrid/Utils/FunctionCompiler.cs @@ -16,143 +16,6 @@ public static Func CreateNewItem() return Expression.Lambda>( Expression.New( typeof( TItem ) ) ).Compile(); } - /// - /// Builds an access expression for nested properties while checking for null values. - /// - /// Item that has the requested field name. - /// Item field name. - /// Returns the requested field if it exists. - private static Expression GetSafePropertyOrField( Expression item, string propertyOrFieldName ) - { - if ( string.IsNullOrEmpty( propertyOrFieldName ) ) - throw new ArgumentException( $"{nameof( propertyOrFieldName )} is not specified." ); - - var parts = propertyOrFieldName.Split( new char[] { '.' }, 2 ); - - Expression field = null; - - MemberInfo memberInfo = GetSafeMember( item.Type, parts[0] ); - - if ( memberInfo is PropertyInfo propertyInfo ) - field = Expression.Property( item, propertyInfo ); - else if ( memberInfo is FieldInfo fieldInfo ) - field = Expression.Field( item, fieldInfo ); - - if ( field == null ) - throw new ArgumentException( $"Cannot detect the member of {item.Type}", propertyOrFieldName ); - - field = Expression.Condition( Expression.Equal( item, Expression.Default( item.Type ) ), - IsNullable( field.Type ) ? Expression.Constant( null, field.Type ) : Expression.Default( field.Type ), - field ); - - if ( parts.Length > 1 ) - field = GetSafePropertyOrField( field, parts[1] ); - - return field; - } - - private static MemberExpression GetPropertyOrField( Expression item, string propertyOrFieldName ) - { - if ( string.IsNullOrEmpty( propertyOrFieldName ) ) - throw new ArgumentException( $"{nameof( propertyOrFieldName )} is not specified." ); - - var parts = propertyOrFieldName.Split( new char[] { '.' }, 2 ); - - MemberExpression field = null; - - MemberInfo memberInfo = GetSafeMember( item.Type, parts[0] ); - - if ( memberInfo is PropertyInfo propertyInfo ) - field = Expression.Property( item, propertyInfo ); - else if ( memberInfo is FieldInfo fieldInfo ) - field = Expression.Field( item, fieldInfo ); - - if ( field == null ) - throw new ArgumentException( $"Cannot detect the member of {item.Type}", propertyOrFieldName ); - - if ( parts.Length > 1 ) - field = GetPropertyOrField( field, parts[1] ); - - // if the value type cannot be null there's no reason to check it for null - if ( !IsNullable( field.Type ) ) - return field; - - return field; - } - - // inspired by: https://stackoverflow.com/questions/2496256/expression-tree-with-property-inheritance-causes-an-argument-exception - private static MemberInfo GetSafeMember( Type type, string fieldName ) - { - MemberInfo memberInfo = (MemberInfo)type.GetProperty( fieldName ) - ?? type.GetField( fieldName ); - - if ( memberInfo == null ) - { - var baseTypesAndInterfaces = new List(); - - if ( type.BaseType != null ) - { - baseTypesAndInterfaces.Add( type.BaseType ); - } - - baseTypesAndInterfaces.AddRange( type.GetInterfaces() ); - - foreach ( var baseType in baseTypesAndInterfaces ) - { - memberInfo = GetSafeMember( baseType, fieldName ); - - if ( memberInfo != null ) - break; - } - } - - return memberInfo; - } - - /// - /// Checks if requested type can bu nullable. - /// - /// Object type. - /// - private static bool IsNullable( Type type ) - { - if ( type.IsClass ) - return true; - - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Nullable<> ); - } - - /// - /// Builds an access expression for nested properties or fields. - /// - /// Item that has the requested field name. - /// Item field name. - /// Returns the requested field if it exists. - private static Expression GetField( Expression item, string propertyOrFieldName ) - { - if ( string.IsNullOrEmpty( propertyOrFieldName ) ) - throw new ArgumentException( $"{nameof( propertyOrFieldName )} is not specified." ); - - var parts = propertyOrFieldName.Split( new char[] { '.' }, 2 ); - - Expression subPropertyOrField = null; - - MemberInfo memberInfo = GetSafeMember( item.Type, parts[0] ); - - if ( memberInfo is PropertyInfo propertyInfo ) - subPropertyOrField = Expression.Property( item, propertyInfo ); - else if ( memberInfo is FieldInfo fieldInfo ) - subPropertyOrField = Expression.Field( item, fieldInfo ); - - if ( subPropertyOrField == null ) - throw new ArgumentException( $"Cannot detect the member of {item.Type}", propertyOrFieldName ); - - if ( parts.Length > 1 ) - subPropertyOrField = GetField( subPropertyOrField, parts[1] ); - - return subPropertyOrField; - } - /// /// Creates the lambda expression that is suitable for usage with Blazor . /// @@ -163,40 +26,25 @@ private static Expression GetField( Expression item, string propertyOrFieldName /// Expression compatible with parser. public static Expression> CreateValidationExpressionGetter( TItem item, string fieldName ) { - var parameter = Expression.Parameter( typeof( TItem ), "item" ); - var property = GetPropertyOrField( parameter, fieldName ); - var path = fieldName.Split( '.' ); - - Func instanceGetter; - - if ( path.Length <= 1 ) - instanceGetter = ( item ) => item; - else - instanceGetter = CreateValueGetter( string.Join( '.', path.Take( path.Length - 1 ) ) ); - - var convertExpression = Expression.MakeMemberAccess( Expression.Constant( instanceGetter( item ) ), property.Member ); - - return Expression.Lambda>( convertExpression ); + return ExpressionCompiler.CreateValidationGetterExpression( item, fieldName ); } public static Func CreateValueGetter( string fieldName ) { - var item = Expression.Parameter( typeof( TItem ), "item" ); - var property = GetSafePropertyOrField( item, fieldName ); - return Expression.Lambda>( Expression.Convert( property, typeof( object ) ), item ).Compile(); + return ExpressionCompiler.CreateValueGetterExpression( fieldName ).Compile(); } public static Func CreateValueTypeGetter( string fieldName ) { var item = Expression.Parameter( typeof( TItem ), "item" ); - var property = GetField( item, fieldName ); + var property = ExpressionCompiler.GetPropertyOrFieldExpression( item, fieldName ); return Expression.Lambda>( Expression.Constant( property.Type ), item ).Compile(); } public static Func CreateDefaultValueByType( string fieldName ) { var item = Expression.Parameter( typeof( TItem ) ); - var property = GetField( item, fieldName ); + var property = ExpressionCompiler.GetPropertyOrFieldExpression( item, fieldName ); return Expression.Lambda>( Expression.Convert( Expression.Default( property.Type ), typeof( object ) ) ).Compile(); } @@ -207,7 +55,7 @@ public static Action CreateValueSetter( string fieldName ) // There's ne safe field setter because that should be a developer responsibility // to don't allow for null nested fields. - var field = GetField( item, fieldName ); + var field = ExpressionCompiler.GetPropertyOrFieldExpression( item, fieldName ); return Expression.Lambda>( Expression.Assign( field, Expression.Convert( value, field.Type ) ), item, value ).Compile(); } } \ No newline at end of file