Unplanned
Last Updated: 04 Sep 2026 12:28 by Frédéric.R
Peter
Created on: 28 Feb 2023 22:09
Category: UI for .NET MAUI
Type: Feature Request
8
Introduce EntityFrameworkCoreDataSource to .NET MAUI

Hi Team,

I would like to be able to have client-side filtering/sorting/grouping capabilities for large backend datasets that cannot be entirely loaded on the client.

As it stands now, the DataGrid (and other data components) can only operate on the data that is has in the local DataView. This means that I need to build a custom filtering solution that prefilters/presorts the backend data before paging and loading it into the DataGrid.

To accomplish the "full view", we need a data layer that understands both the UI as well as the backend. The Telerik UI for WPF product has an excellent solution for this, known as the WPF EntityFrameworkCoreDataSource - Overview - Telerik UI for WPF and the WPF DataServiceDataSource - Overview - Telerik UI for WPF.

If such a feature can be added to Telerik UI for MAUI, it would be an excellent bonus for the component suite in data heavy applications.

Thank you,

Peter

 

2 comments
Frédéric.R
Posted on: 04 Sep 2026 12:28
I developped this, can be enhanced/corrected and then included in telerik library ?


// ============================================================================
// File:        TelerikQueryExtensions.cs
// Namespace:   UNOG.Assets.MAUI.Utilities
// Summary:     Extension methods that translate Telerik RadDataGrid sort and
//              filter descriptor collections into equivalent LINQ expressions,
//              enabling them to be applied directly to IQueryable sources
//              (e.g. Entity Framework Core queries) for server-side paging,
//              sorting, and filtering.
// Remarks:     Used by DataViewModel.GetNextPageAsync() to translate
//              grid-driven SortDescriptorCollection / filter descriptors into
//              query expressions evaluated by the EF Core provider.
//
// Author: Frédéric Rybkowski
// ============================================================================

using System.Collections.ObjectModel;
using System.Linq.Expressions;
using Telerik.Maui.Controls.Data;


namespace xxx.Utilities
{
    /// <summary>
    /// Provides <see cref="IQueryable{T}"/> extension methods that translate Telerik
    /// <see cref="SortDescriptorCollection"/> and filter descriptor collections
    /// (as used by <c>RadDataGrid</c>) into LINQ sort/filter expressions that can be
    /// applied directly to an Entity Framework Core query.
    /// </summary>
    public static class TelerikQueryExtensions
    {

        public static IQueryable<T> ApplySorts<T>(this IQueryable<T> query, SortDescriptorCollection sortDescriptors)
        {
            if (sortDescriptors == null || sortDescriptors.Count == 0)
            {
                return query;
            }

            IOrderedQueryable<T> orderedQuery = null;

            foreach (var sortDescriptor in sortDescriptors)
            {

                if (sortDescriptor is PropertySortDescriptor propertySortDescriptor)
                {

                    var parameter = Expression.Parameter(typeof(T), "item");
                    var property = Expression.Property(parameter, propertySortDescriptor.PropertyName);
                    var lambda = Expression.Lambda(property, parameter);

                    string methodName = propertySortDescriptor.SortOrder == SortOrder.Ascending
                        ? (orderedQuery == null ? "OrderBy" : "ThenBy")
                        : (orderedQuery == null ? "OrderByDescending" : "ThenByDescending");

                    var method = typeof(Queryable).GetMethods()
                        .First(m => m.Name == methodName && m.GetParameters().Length == 2)
                        .MakeGenericMethod(typeof(T), property.Type);

                    orderedQuery = (IOrderedQueryable<T>)method.Invoke(null, new object[] { orderedQuery ?? query, lambda });
                }
            }

            return orderedQuery ?? query;
        }


        public static IQueryable<T> ApplyFilters<T>(this IQueryable<T> query, ObservableCollection<FilterDescriptorBase> filters, LogicalOperator mode = LogicalOperator.And)
        {
            if (filters == null || filters.Count == 0)
            {
                return query;
            }

            Expression<Func<T, bool>> rootCombinedFilter = ProcessFilters<T>(filters, mode);

            if (rootCombinedFilter != null)
            {
                query = query.Where(rootCombinedFilter);
            }

            return query;
        }



        public static Expression<Func<T, bool>> GetExpression<T>(CompositeFilterDescriptor filter) where T : class
        {
            if (filter != null)
                return ProcessFilters<T>(filter.Descriptors, filter.Operator);
            else
                return null;
        }

        public static IQueryable<T> ApplyFilters<T>(this IQueryable<T> query, CompositeFilterDescriptor filter)
        {

            Expression<Func<T, bool>> rootCombinedFilter = null;
            if (filter == null || filter.Descriptors.Count == 0)
            {
                return query;
            }
            else {
                rootCombinedFilter = ProcessFilters<T>(filter.Descriptors, filter.Operator);
            }

            if (rootCombinedFilter != null)
            {
                query = query.Where(rootCombinedFilter);
            }

            return query;
        }





        public static Expression<Func<T, bool>> ProcessFilters<T>(ObservableCollection<FilterDescriptorBase> filters, LogicalOperator mode)
        {
            Expression<Func<T, bool>> rootCombinedFilter = null;

            foreach (var filterDescriptor in filters)
            {
                if (filterDescriptor is CompositeFilterDescriptor compositeFilter)
                {
                    rootCombinedFilter = CombineFilters(rootCombinedFilter, ProcessFilters<T>(compositeFilter.Descriptors, compositeFilter.Operator), mode);
                }
                else if (filterDescriptor is TextFilterDescriptor textFilter)
                {
                    rootCombinedFilter = CombineFilters(rootCombinedFilter, BuildTextFilterExpression<T>(textFilter), mode);
                }
                else if (filterDescriptor is BooleanFilterDescriptor booleanFilter)
                {
                    rootCombinedFilter = CombineFilters(rootCombinedFilter, BuildBooleanFilterExpression<T>(booleanFilter), mode);
                }
                else if (filterDescriptor is NumericalFilterDescriptor numericalFilter)
                {
                    rootCombinedFilter = CombineFilters(rootCombinedFilter, BuildNumericalFilterExpression<T>(numericalFilter), mode);
                }
                else if (filterDescriptor is DateTimeFilterDescriptor dateFilter)
                {
                    rootCombinedFilter = CombineFilters(rootCombinedFilter, BuildDateFilterExpression<T>(dateFilter), mode);
                }
            }

            return rootCombinedFilter;
        }

        private static Expression<Func<T, bool>> CombineFilters<T>(Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2, LogicalOperator mode)
        {
            if (expr1 == null)
            {
                return expr2;
            }

            var parameter = Expression.Parameter(typeof(T));
            Expression body = null;

            if (mode == LogicalOperator.And)
            {
                body = Expression.AndAlso(
                  Expression.Invoke(expr1, parameter),
                  Expression.Invoke(expr2, parameter));
            }
            else
            {
                body = Expression.OrElse(
                   Expression.Invoke(expr1, parameter),
                   Expression.Invoke(expr2, parameter));
            }

            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }
        private static Expression<Func<T, bool>> BuildTextFilterExpression<T>(TextFilterDescriptor textFilter)
        {
            var parameter = Expression.Parameter(typeof(T), "item");
            Expression property = Expression.Property(parameter, textFilter.PropertyName);

            // Ensure property is string
            if (property.Type != typeof(string))
            {


                // Convert enum to string if necessary
                if (property.Type.IsEnum)
                {
                    var toStringMethod = property.Type.GetMethod("ToString", Type.EmptyTypes);
                    property = Expression.Call(property, toStringMethod);
                }
                else
                    return (item) => true; // Skip if property is not of type string
            }

            Expression valueExpression = textFilter.Value != null ? Expression.Constant(textFilter.Value.ToString(), typeof(string)): null;
            Expression body = null;

            // Case-insensitive if configured
            if (!textFilter.IsCaseSensitive)
            {
                var toLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
                Expression lowerProperty = Expression.Call(property, toLowerMethod);
                Expression lowerValue = Expression.Call(valueExpression, toLowerMethod);

                property = lowerProperty;
                valueExpression = lowerValue;
            }

            switch (textFilter.Operator)
            {
                case TextOperator.Contains:
                    body = Expression.Call(property, "Contains", null, valueExpression);
                    break;
                case TextOperator.StartsWith:
                    body = Expression.Call(property, "StartsWith", null, valueExpression);
                    break;
                case TextOperator.EndsWith:
                    body = Expression.Call(property, "EndsWith", null, valueExpression);
                    break;
                case TextOperator.EqualsTo:
                    body = Expression.Equal(property, valueExpression);
                    break;
                case TextOperator.IsEmpty:
                    body = Expression.Equal(property, Expression.Constant(string.Empty));
                    break;
                case TextOperator.IsNotEmpty:
                    body = Expression.NotEqual(property, Expression.Constant(string.Empty));
                    break;
                default:
                    return (item) => true; // Skip if operator is not supported
            }

            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }




        private static Expression<Func<T, bool>> BuildBooleanFilterExpression<T>(BooleanFilterDescriptor booleanFilter)
        {
            var parameter = Expression.Parameter(typeof(T), "item");
            var property = Expression.Property(parameter, booleanFilter.PropertyName);

            if (property.Type != typeof(bool))
            {
                return (item) => true; // Skip if property is not of type bool
            }

            var valueExpression = Expression.Constant(booleanFilter.Value, typeof(bool));
            Expression body = Expression.Equal(property, valueExpression);

            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        private static Expression<Func<T, bool>> BuildNumericalFilterExpression<T>(NumericalFilterDescriptor numericalFilter)
        {
            var parameter = Expression.Parameter(typeof(T), "item");
            var property = Expression.Property(parameter, numericalFilter.PropertyName);

            if (!IsNumericType(property.Type))
            {
                return (item) => true; // Skip if property is not of a numeric type
            }

            var valueExpression = Expression.Constant(Convert.ChangeType(numericalFilter.Value, property.Type), property.Type);
            Expression body = null;

            switch (numericalFilter.Operator)
            {
                case NumericalOperator.EqualsTo:
                    body = Expression.Equal(property, valueExpression);
                    break;
                case NumericalOperator.IsGreaterThan:
                    body = Expression.GreaterThan(property, valueExpression);
                    break;
                case NumericalOperator.IsGreaterThanOrEqualTo:
                    body = Expression.GreaterThanOrEqual(property, valueExpression);
                    break;
                case NumericalOperator.IsLessThan:
                    body = Expression.LessThan(property, valueExpression);
                    break;
                case NumericalOperator.IsLessThanOrEqualTo:
                    body = Expression.LessThanOrEqual(property, valueExpression);
                    break;
            }

            if (body == null) { return (item) => true; }

            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        private static Expression<Func<T, bool>> BuildDateFilterExpression<T>(DateTimeFilterDescriptor dateFilter)
        {
            var parameter = Expression.Parameter(typeof(T), "item");
            var property = Expression.Property(parameter, dateFilter.PropertyName);

            if (property.Type != typeof(DateTime))
            {
                return (item) => true; // Skip if property is not of type DateTime
            }

            var valueExpression = Expression.Constant(dateFilter.Value, typeof(DateTime));
            Expression body = null;

            switch (dateFilter.Operator)
            {
                case NumericalOperator.EqualsTo:
                    body = Expression.Equal(property, valueExpression);
                    break;
                case NumericalOperator.IsGreaterThan:
                    body = Expression.GreaterThan(property, valueExpression);
                    break;
                case NumericalOperator.IsGreaterThanOrEqualTo:
                    body = Expression.GreaterThanOrEqual(property, valueExpression);
                    break;
                case NumericalOperator.IsLessThan:
                    body = Expression.LessThan(property, valueExpression);
                    break;
                case NumericalOperator.IsLessThanOrEqualTo:
                    body = Expression.LessThanOrEqual(property, valueExpression);
                    break;
            }

            if (body == null) { return (item) => true; }

            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        private static bool IsNumericType(Type type)
        {
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Byte:
                case TypeCode.Decimal:
                case TypeCode.Double:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.SByte:
                case TypeCode.Single:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    return true;
                default:
                    return false;
            }
        }
    }

    public enum FilterMode
    {
        And,
        Or
    }
}





a part of my project that use the library :
private async Task<IEnumerable<R>> GetNextPageAsync<R>() where R : class

        {
            IEnumerable<R> nextPageOfData;

            using (var dbContext = DBContext())
            {
                try
                {
                    var query = dbContext.Set<R>().AsQueryable();

                    query = ApplyCustomFilters(query);
                    query = query.ApplyFilters(gridFilters);
                    query = query.ApplySorts(gridSorts);

                    // Compute the total matching row count once per filter/sort/search
                    // combination, so we know upfront (arithmetically) whether a next
                    // page exists instead of discovering it via a wasted, empty fetch.
                    if (totalMatchingCount < 0)
                        totalMatchingCount = await query.CountAsync();

                    nextPageOfData = await query.Skip(pageToGet * itemsPerPage)
                                                .Take(itemsPerPage)
                                                .ToListAsync();

                    if(nextPageOfData.Count() > 0)
                        pageToGet++;

                    // We know exactly how many rows match, so derive hasMoreData
                    // from the running total rather than waiting for a partial page.
                    hasMoreData = pageToGet * itemsPerPage < totalMatchingCount;
                }
                catch (Exception ex)
                {
                    System.Console.WriteLine($"Error fetching data: {ex.Message}");
                    nextPageOfData = new List<R>();

                    if (ex is DbException)
                    {
                        await dbContext.InitializeDatabaseAsync(true);
                    }
                }
            }

            return nextPageOfData;
        }




Patryk
Posted on: 02 Jul 2024 21:12