enisn / AutoFilterer

AutoFilterer is a mini filtering framework library for dotnet. The main purpose of the library is to generate LINQ expressions for Entities over DTOs automatically. The first aim is to be compatible with Open API 3.0 Specifications
MIT License
458 stars 37 forks source link

Enhancement request: Support Generics for FilteringOptionsBaseAttribute in Custom Expression for a Property #64

Open neozhu opened 1 year ago

neozhu commented 1 year ago

I have an idea related to the "Custom Expression for a Property" feature mentioned in your documentation. My idea is to make the FilteringOptionsBaseAttribute support generics, which would make writing LINQ expressions easier. Do you think this is possible?

For reference, here is another project that might be helpful: https://github.com/ClaveConsulting/Expressionify https://github.com/lukemcgregor/LinqExpander

Thank you for considering my suggestion.

enisn commented 1 year ago

We can create some adapter packages for those libraries so they can be used with autofilterer without a pain

neozhu commented 1 year ago

thank you

enisn commented 1 year ago

Keep this issue open until the development is completed for Generics support :)

neozhu commented 1 year ago

Hi @enisn ,

Thank you very much for your attention to this topic.

I have an idea and I'm wondering if it's possible to implement it in your project?

Let me first share my code snippet. Once you see it, you'll understand. I believe it's a way to save a lot of work and make the code more concise.

I plan to do three fixed queries in advance, such as 1. all (unconditional), 2. query records created by me (CreateBy == UserId Of Login), and 3. query records created today (Created == Today)

public class ProductsWithPaginationQuery : PaginationFilterBase, ICacheableRequest<PaginatedData<ProductDto>>
{
    public string? Name { get; set; }
    public string? Brand { get; set; }

    public string? Unit { get; set; }
    public Range<decimal> Price { get; set; } = new();
    [CompareTo("Name", "Brand", "Description")] // <-- This filter will be applied to Name or Brand or Description.
    [StringFilterOptions(StringFilterOption.Contains)]
    public string? Keyword { get; set; }
    [CompareTo(typeof(SearchProductsWithListView), "Name")]
    public ProductListView ListView { get; set; } = ProductListView.All; //<-- When the user selects a different ListView,
                                                                         // a custom query expression is executed on the backend.
                                                                         // For example, if the user selects "My Products",
                                                                         // the query will be x => x.CreatedBy == CurrentUser.UserId
    public UserProfile? CurrentUser { get; set; } // <-- This CurrentUser property gets its value from the information of
                                                  // the currently logged in user
    public override string ToString()
    {
        return $"CurrentUser:{CurrentUser?.UserId},ListView:{ListView},Search:{Keyword},Name:{Name},Brand:{Brand},Unit:{Unit},MinPrice:{Price?.Min},MaxPrice:{Price?.Max},Sort:{Sort},SortBy:{SortBy},{Page},{PerPage}";
    }
    public string CacheKey => ProductCacheKey.GetPaginationCacheKey($"{this}");
    public MemoryCacheEntryOptions? Options => ProductCacheKey.MemoryCacheEntryOptions;
}

public enum ProductListView
{
    [Description("All")]
    All,
    [Description("My Products")]
    My,
    [Description("Created Toady")]
    CreatedToday,
}
public class SearchProductsWithListView : FilteringOptionsBaseAttribute
{
   public override Expression BuildExpression(Expression expressionBody, PropertyInfo targetProperty, PropertyInfo filterProperty, object value)
    {
        var today = DateTime.Now.Date;
        var start = Convert.ToDateTime(today.ToString("yyyy-MM-dd",CultureInfo.CurrentCulture) + " 00:00:00", CultureInfo.CurrentCulture);
        var end = Convert.ToDateTime(today.ToString("yyyy-MM-dd",CultureInfo.CurrentCulture) + " 23:59:59", CultureInfo.CurrentCulture);
        //var currentUser = filterProperty.CurrentUser;
        var listview = (ProductListView)value;
        return listview switch {
            ProductListView.All => expressionBody,
            //ProductListView.My=>  Expression.Equal(Expression.Property(expressionBody, "CreatedBy"),  Expression.Constant(currentUser?.UserId)),
            ProductListView.CreatedToday => Expression.GreaterThanOrEqual(Expression.Property(expressionBody, "Created"), 
                                                                          Expression.Constant(start, typeof(DateTime?)))
                                            .Combine(Expression.LessThanOrEqual(Expression.Property(expressionBody, "Created"), 
                                                     Expression.Constant(end, typeof(DateTime?))), 
                                                     CombineType.And),
            _=> expressionBody
        };
    }
}

/////  ProductListView.CreatedToday => Expression.GreaterThanOrEqual(Expression.Property(expressionBody, "Created"), 
//                                                                          Expression.Constant(start, typeof(DateTime?)))
//                                           .Combine(Expression.LessThanOrEqual(Expression.Property(expressionBody, "Created"), 
//                                                    Expression.Constant(end, typeof(DateTime?))), 
//                                                     CombineType.And),
// this Expression is  working.

However, the code snippet I shared is not yet compiling. I need your help to implement this feature. Do you have any good ideas on how to achieve this?

The problem I'm facing now is that I cannot get the property value of the current user of ProductsWithPaginationQuery in class SearchProductsWithListView : FilteringOptionsBaseAttribute{}, so I cannot build the query expression for the query "created by me".

65

neozhu commented 1 year ago

The following code is my own way to implement the query expression made by the ListView: this is my project :https://github.com/neozhu/CleanArchitectureWithBlazorServer

public class DocumentsWithPaginationQuery : PaginationFilter, ICacheableRequest<PaginatedData<DocumentDto>>
{
    public DocumentListView ListView { get; set; } = DocumentListView.All;
    public required UserProfile CurrentUser { get; set; }
    public override string ToString()
    {
        return $"CurrentUser:{CurrentUser?.UserId},ListView:{ListView},Search:{Keyword},OrderBy:{OrderBy} {SortDirection},{PageNumber},{PageSize}";
    }
    public string CacheKey => DocumentCacheKey.GetPaginationCacheKey($"{this}");
    public MemoryCacheEntryOptions? Options => DocumentCacheKey.MemoryCacheEntryOptions;

}
public class DocumentsQueryHandler : IRequestHandler<DocumentsWithPaginationQuery, PaginatedData<DocumentDto>>
{

    private readonly IApplicationDbContext _context;
    private readonly IMapper _mapper;

    public DocumentsQueryHandler(
        IApplicationDbContext context,
        IMapper mapper
        )
    {
        _context = context;
        _mapper = mapper;
    }
    public async Task<PaginatedData<DocumentDto>> Handle(DocumentsWithPaginationQuery request, CancellationToken cancellationToken)
    {
        var data = await _context.Documents
            .Specify(new DocumentsQuery(request))
            .OrderBy($"{request.OrderBy} {request.SortDirection}")
            .ProjectTo<DocumentDto>(_mapper.ConfigurationProvider)
            .PaginatedDataAsync(request.PageNumber, request.PageSize);

        return data;
    }

    internal class DocumentsQuery : Specification<Document>
    {
        public DocumentsQuery(DocumentsWithPaginationQuery request)
        {
            Criteria = request.ListView switch
            {
                DocumentListView.All => p => (p.CreatedBy == request.CurrentUser.UserId && p.IsPublic == false) || (p.IsPublic == true && p.TenantId == request.CurrentUser.TenantId),
                DocumentListView.My => p => (p.CreatedBy == request.CurrentUser.UserId && p.TenantId == request.CurrentUser.TenantId),
                DocumentListView.CreatedToday => p => p.Created.Value.Date == DateTime.Now.Date,
                _ => throw new NotImplementedException()
            };
            if (!string.IsNullOrEmpty(request.Keyword))
            {
                And(x => x.Title.Contains(request.Keyword) || x.Description.Contains(request.Keyword) || x.Content.Contains(request.Keyword));
            }
        }
    }

}
public enum DocumentListView
{
    [Description("All")]
    All,
    [Description("My Document")]
    My,
    [Description("Created Toady")]
    CreatedToday,
}
neozhu commented 1 year ago

@enisn Do you have a release plan for this?

neozhu commented 1 year ago

Hello @enisn,