-
Notifications
You must be signed in to change notification settings - Fork 58
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
Comments
This feels like something that should be handed off to a standard framework like GraphQL that this library enables. |
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(); |
Sorry for the code dump but this touches filtering and repositories at the same time. |
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. |
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. |
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 |
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 |
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. |
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. |
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" |
@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 |
@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 |
Maybe will be easy add an option to enable OData with a configuration. |
It would be cool if there’s filtering & sorting
The text was updated successfully, but these errors were encountered: