Skip to content

No server side filtering #1

Description

@candritzky

Dear Carl,

there's a problem with your current implementation: When using the "efcustomers" controller, the query first returns all records from the database and then filtering is done only in memory. This might be a no-go for most real-world applications.

But it can be fixed in AvnRepository (in QueryFilter.GetFilteredList) by changing the query expressions so that they are accepted by Entity Framework Core. The trick is to avoid reflection and build simple expressions that are supported EF Core (e. g. c => c.Name, String.StartsWith, ...).

Here's my patch for the StartsWith operator (as an example):

                else if (filterProperty.Operator == FilterOperator.StartsWith)
                    //if (filterProperty.CaseSensitive == false)
                    {
                        var arg = Expression.Parameter(typeof(TEntity), "c");
                        var getProp = Expression.MakeMemberAccess(arg, prop);
                        var startsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });

                        var targetValue = Expression.Constant(value);

                        MethodCallExpression propStartsWith;
                        if (filterProperty.CaseSensitive)
                        {
                            // Compare as-is for case-sensitive comparison
                            propStartsWith = Expression.Call(getProp, startsWith, targetValue);
                        }
                        else
                        {
                            // Convert ToLower to achieve case-insensitive comparison
                            // Please note that SQL Server is case-insenstive by default (depending on the configured COLLATION)
                            MethodInfo toLower = typeof(string).GetMethod("ToLower", Array.Empty<Type>());
                            MethodCallExpression propStartsWithLower = Expression.Call(getProp, toLower);

                            propStartsWith = Expression.Call(propStartsWithLower, startsWith, targetValue);
                        }

                        expression = Expression.Lambda<Func<TEntity, bool>>(propStartsWith, arg);
                        //expression = s => s.GetType().GetProperty(filterProperty.Name).GetValue(s).ToString().ToLower().StartsWith(value);
                    }
                    //else
                    //    expression = s => s.GetType().GetProperty(filterProperty.Name).GetValue(s).ToString().StartsWith(value);

A similar change is required for OrderByPropertyName. Here's my patch:

        // order by
        if (OrderByPropertyName != "")
        {
            PropertyInfo prop = typeof(TEntity).GetProperty(OrderByPropertyName);
            if (prop != null)
            {
                var arg = Expression.Parameter(typeof(TEntity), "c");
                var getProp = Expression.MakeMemberAccess(arg, prop);
                var orderByExpression = Expression.Lambda<Func<TEntity, string>>(getProp, arg);
                if (OrderByDescending)
                    query = query.OrderByDescending(orderByExpression);
                else
                    query = query.OrderBy(orderByExpression);
            }
        }

With that in place, the code in BlazorRepositoryDemo (in EFRepository.GetAsync) can be changed to this. Note that it no longer calls GetAllAsync and passes the IQueryable<TEntity> DbSet directly into the filter:

    public async Task<IEnumerable<TEntity>> GetAsync(QueryFilter<TEntity> Filter)
    {
        //var allitems = (await GetAllAsync()).ToList();
        //return await Task.FromResult(Filter.GetFilteredList(allitems));
        var filteredItems = Filter.GetFilteredList(dbSet);
        return await Task.FromResult(filteredItems);
    }

This will then give you real database server side filtering (and ordering). See here for the query log (taken from SQL Server Profiler):

SELECT [c].[Id], [c].[Email], [c].[Name]
FROM [Customer] AS [c]
WHERE LOWER([c].[Name]) LIKE N'j%'
ORDER BY [c].[Name] DESC

Note #1 Unfortunately this case-sensitivity because SQL server treats string comparisons as case-insensitive by default. But this can be changed on SQL database side by using an appropriate collation.

Note #2 The OrderBy expression is still not quite correct because it only works with string typed columns. I don't know yet how to call Expression.Lambda when some argument types (here: the last argument for the return type) are only known at runtime (here: prop.PropertyType).

Best regards,
Chris

PS: Sorry for not providing a clean PR, but the change affects two repos.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions