Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering capabilities #18

Open
murugaratham opened this issue Feb 18, 2022 · 13 comments
Open

Filtering capabilities #18

murugaratham opened this issue Feb 18, 2022 · 13 comments
Labels
enhancement New feature or request

Comments

@murugaratham
Copy link

It would be cool if there’s filtering & sorting

@csharpfritz
Copy link
Owner

This feels like something that should be handed off to a standard framework like GraphQL that this library enables.

@csharpfritz csharpfritz added the enhancement New feature or request label Feb 18, 2022
@ScottKane
Copy link
Contributor

I don't think this is something specific to GraphQl, would be just as nice to use something like the following so that repository/filtering kind of go hand in hand.

public class MyClass
{
    public string Name { get; set; }
    public string Description { get; set; }
}

public interface ISpecification<T> where T : class, IEntity
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    List<string> IncludeStrings { get; }
}

public abstract class FilterSpecification<T> : ISpecification<T> where T : class, IEntity
{
    public Expression<Func<T, bool>> Criteria { get; set; }
    public List<Expression<Func<T, object>>> Includes { get; } = new();
    public List<string> IncludeStrings { get; } = new();

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression) => Includes.Add(includeExpression);
    protected virtual void AddInclude(string includeString) => IncludeStrings.Add(includeString);
}

public class MyClassFilterSpecification : FilterSpecification<MyClass>
{
    public MyClassFilterSpecification(string searchString) // This could maybe be a MyClassFilterOptions object in the future but just a simple search string for now.
    {
        if (!string.IsNullOrEmpty(searchString))
            Criteria = p => p.Name.Contains(searchString) ||
                            p.Description.Contains(searchString);
        else
            Criteria = p => true;
    }
}

This would work well with a repository pattern, consider the following repository:

public interface IEntity { }
    
public interface IEntity<TId> : IEntity
{
    public TId Id { get; set; }
}

public interface IRepositoryAsync<T, in TId> where T : class, IEntity<TId>
{
    IQueryable<T> Entities { get; }
    Task<T> GetByIdAsync(TId id);
    Task<List<T>> GetAllAsync();
    Task<List<T>> GetPagedResponseAsync(int pageNumber, int pageSize);
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

public interface IUnitOfWork<TId> : IDisposable
{
    IRepositoryAsync<T, TId> Repository<T>() where T : IEntity<TId>;

    Task<int> Commit(CancellationToken cancellationToken);

    Task<int> CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys);

    Task Rollback();
}

public class UnitOfWork<TId> : IUnitOfWork<TId>
{
    private readonly IAppCache _cache;
    private readonly ApplicationContext _dbContext;
    private Hashtable _repositories;
    private bool _disposed;

    public UnitOfWork(
        ApplicationContext dbContext,
        IAppCache cache)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        _cache = cache;
    }

    public IRepositoryAsync<TEntity, TId> Repository<TEntity>() where TEntity : IEntity<TId>
    {
        _repositories ??= new Hashtable();

        var type = typeof(TEntity).Name;

        if (_repositories.ContainsKey(type)) return (IRepositoryAsync<TEntity, TId>) _repositories[type];
        var repositoryType = typeof(RepositoryAsync<,>);

        var repositoryInstance = Activator.CreateInstance(
            repositoryType.MakeGenericType(
                typeof(TEntity),
                typeof(TId)),
            _dbContext);

        _repositories.Add(
            type,
            repositoryInstance);

        return (IRepositoryAsync<TEntity, TId>) _repositories[type];
    }

    public async Task<int> Commit(CancellationToken cancellationToken) => await _dbContext.SaveChangesAsync(cancellationToken);

    public async Task<int> CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys)
    {
        var result = await _dbContext.SaveChangesAsync(cancellationToken);
        foreach (var cacheKey in cacheKeys) _cache.Remove(cacheKey);

        return result;
    }

    public Task Rollback()
    {
        _dbContext.ChangeTracker.Entries()
                    .ToList()
                    .ForEach(x => x.Reload());

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
            if (disposing)
                _dbContext.Dispose();

        _disposed = true;
    }
}

public class RepositoryAsync<T, TId> : IRepositoryAsync<T, TId> where T : IEntity<TId>
{
    private readonly ApplicationContext _dbContext;

    public RepositoryAsync(ApplicationContext dbContext) => _dbContext = dbContext;

    public IQueryable<T> Entities => _dbContext.Set<T>();

    public async Task<T> AddAsync(T entity)
    {
        await _dbContext.Set<T>().AddAsync(entity);

        return entity;
    }

    public Task DeleteAsync(T entity)
    {
        _dbContext.Set<T>().Remove(entity);

        return Task.CompletedTask;
    }

    public async Task<List<T>> GetAllAsync() => await _dbContext.Set<T>().ToListAsync();

    public async Task<T> GetByIdAsync(TId id) => await _dbContext.Set<T>().FindAsync(id);

    public async Task<List<T>> GetPagedResponseAsync(int pageNumber, int pageSize)
    {
        return await _dbContext.Set<T>()
                                .Skip((pageNumber - 1) * pageSize)
                                .Take(pageSize)
                                .AsNoTracking()
                                .ToListAsync();
    }

    public Task UpdateAsync(T entity)
    {
        var selected = _dbContext.Set<T>().Find(entity.Id);
        if (selected is not null)
            _dbContext.Entry(selected).CurrentValues.SetValues(entity);
        
        return Task.CompletedTask;
    }
}
public static IQueryable<T> Specify<T>(this IQueryable<T> query, ISpecification<T> spec)
            where T : class, IEntity
{
    var queryableResultWithIncludes = spec.Includes.Aggregate(
        query,
        (current, include) => current.Include(include));
    var secondaryResult = spec.IncludeStrings.Aggregate(
        queryableResultWithIncludes,
        (current, include) => current.Include(include));

    return secondaryResult.Where(spec.Criteria);
}

This would allow the following in your business logic:

var data = await _unitOfWork.Repository<MyClass>()
    .Entities.Specify(new MyClassFilterSpecification("some search string"))
    .Select(expression)
    .AsNoTracking();

@ScottKane
Copy link
Contributor

Sorry for the code dump but this touches filtering and repositories at the same time.

@davidbuckleyni
Copy link
Contributor

Dot net in general tends to be stirring away from the repo pattern cause it can be daunting to new comers. Thats why ef core built most of into the system so we wouldnt have to do the repo pattern anymore @csharpfritz can correct me if am wrong here.

@ScottKane
Copy link
Contributor

Last I heard was that it was a strong recommendation to be using the repository pattern for data access full stop, regardless of it coming from EF. That way if you slot in something else that doesn't have it built in for you, you already have everything you need but I'm happy to be wrong on this one.

@murugaratham
Copy link
Author

murugaratham commented Mar 24, 2022

What I felt is that, this library enables very rapid api implementation, but with just crud seems to basic, having filtering & sorting will drive it to have more real world adoption.

with or without repository, it’s the underlying implementation that this library potentially can provide, it would be a game changer if so

@ScottKane
Copy link
Contributor

For me it's a case of what is the goal, are we trying to speed up prototyping, or trying to make your life as a .NET dev easier. I would argue that to achieve the 2nd, you're probably doing the first anyways. I think giving more options for what can be scaffolded is always going to be good. It's not like someone who wants a quick prototype is any worse off, they just don't set up the filtering config

@murugaratham
Copy link
Author

What this library essentially does IMO is removing a lot of boiler plate, less code less bug and higher productivity, but basic CRUD is good to meet 60-70% of use case (pluck out of thin air estimates), but sorting & filtering is pretty much in a lot of other use cases.

@csharpfritz felt that this could be left in developers hands, I respect that, since graphql, OData exists to do just that.

@ScottKane
Copy link
Contributor

As you can see from my code, there is still a lot to type to get filtering up and running. To me, if this product doesn't support the things I need, I and others are unlikely to use it. Because if I use it to prototype and then have to go and write all that crap manually after to support filtering, why not just do that from the start.

@ScottKane
Copy link
Contributor

ScottKane commented Mar 24, 2022

I will happily work on the feature, I am just not a fan of the whole "it's not really in the scope of the project" when it totally is. Filtering/Pagination options are the difference between this being "good for POC" and legit just "good everywhere"

@murugaratham
Copy link
Author

@ScottKane don’t get me wrong, I’m with you on this, I am the issue author 🤣

but rather then trying to get maintainers and collaborators to build this, we can just +1 this issue to let them know about the demand or rather desire to have this feature.

If I get some time, I might think about a pr for it

@ScottKane
Copy link
Contributor

@murugaratham haha sorry we are totally on the same page, I'm not expecting someone else to do it, I just don't want to start on a PR that ultimately wont get approved. I think we need some input from @csharpfritz

@mmassari
Copy link

mmassari commented Apr 4, 2022

Maybe will be easy add an option to enable OData with a configuration.
Something like this: UseOData(p=>p.AllowFiltering, p.AllowSorting, p.AllowPaging, p.AllowSelecting)
So in your GET endpoint you will have all those great stuff already done by OData in a standard and optimized way.
If you don't like OData response format it can be add a switch like RemoveMetadata() and you will get clean json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants