Skip to content

Commit

Permalink
Add dynamic filterable repository. (fixes #11) (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
soroshsabz authored Nov 16, 2023
1 parent 89635a9 commit 305cb60
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.25" />
<PackageReference Include="Sieve" Version="2.5.5" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.32" />
<PackageReference Include="Sieve" Version="2.4.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using BSN.Commons.Extensions;
using BSN.Commons.Infrastructure;
using BSN.Commons.Orm.EntityFrameworkCore.Extensions;
using Sieve.Exceptions;
using Sieve.Models;
using Sieve.Services;
using System;
using System.Linq;
using System.Linq.Expressions;

namespace BSN.Commons.Orm.EntityFrameworkCore
{
/// <summary>
/// Default implementation of <see cref="IDynamicFilterableRepository{T}"/>
/// </summary>
/// <typeparam name="T"></typeparam>
public class DynamicFilterableRepositoryBase<T> : RepositoryBase<T>, IDynamicFilterableRepository<T> where T : class
{
/// <inheritdoc />
protected DynamicFilterableRepositoryBase(IDatabaseFactory databaseFactory, ISieveProcessor sieveProcessor) : base(databaseFactory)
{
SieveProcessor = sieveProcessor ?? throw new ArgumentNullException(nameof(sieveProcessor));
}

/// <inheritdoc />
public PagedEntityCollection<T> GetMany(Expression<Func<T, bool>> where, string filters, string sorts, uint pageNumber, uint pageSize)
{
if (pageNumber == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageNumber));

if (pageSize == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageSize));

IQueryable<T> query = dbSet.Where(where);

try
{
query = SieveProcessor.Apply(new SieveModel() { Filters = filters, Sorts = sorts, PageSize = (int?)pageSize, Page = (int?)pageNumber }, query, applyPagination: false);
}
catch (SieveException ex)
{
throw new InvalidOperationException(message: ex.ExtractMessage());
}

return query.Paginate(pageNumber, pageSize);
}

/// <inheritdoc />
public PagedEntityCollection<T> GetMany(string filters, string sorts, uint pageNumber, uint pageSize)
{
return GetMany((entity) => true, filters, sorts, pageNumber, pageSize);
}

/// <summary>
/// The engine of filtering
/// </summary>
/// <remarks>
/// So if you want to any extra filtering or changing behaviour of current filtering, you have to use this engine
/// </remarks>
protected ISieveProcessor SieveProcessor { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace BSN.Commons.Extensions
{
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace BSN.Commons.Extensions
{
/// <summary>
/// The place for IQuerubable extenstions
/// </summary>
public static partial class IQueryableExtensions
{
/// <summary>
/// Paginate IQueryable of <typeparamref name="T"/>
/// with given pageNumber and pageSize
/// </summary>
public static PagedEntityCollection<T> Paginate<T>(this IQueryable<T> query, uint pageNumber, uint pageSize)
{
if (pageNumber == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageNumber));

if (pageSize == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageSize));

var result = new PagedEntityCollection<T>
{
CurrentPage = pageNumber,
PageSize = pageSize,
RecordCount = (uint)query.Count(),
Results = query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToList()
};

result.PageCount = (uint)Math.Ceiling((double)result.RecordCount / pageSize);

return result;
}

/// <summary>
/// Paginate IQueryable of <typeparamref name="T"/>
/// with given pageNumber and pageSize
/// </summary>
public static async Task<PagedEntityCollection<T>> PaginateAsync<T>(this IQueryable<T> query, uint pageNumber, uint pageSize)
{
if (pageNumber == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageNumber));

if (pageSize == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageSize));

var result = new PagedEntityCollection<T>
{
CurrentPage = pageNumber,
PageSize = pageSize,
RecordCount = (uint) await query.CountAsync(),
Results = await query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToListAsync()
};

result.PageCount = (uint)Math.Ceiling((double)result.RecordCount / pageSize);

return result;
}
}
}
/// The place for IQuerubable extenstions
/// </summary>
public static partial class IQueryableExtensions
{
/// <summary>
/// Paginate IQueryable of <typeparamref name="T"/>
/// with given pageNumber and pageSize
/// </summary>
public static PagedEntityCollection<T> Paginate<T>(this IQueryable<T> query, uint pageNumber, uint pageSize)
{
if (pageNumber == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageNumber));

if (pageSize == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageSize));

var result = new PagedEntityCollection<T>
{
CurrentPage = pageNumber,
PageSize = pageSize,
RecordCount = (uint)query.Count(),
Results = query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToList()
};

result.PageCount = (uint)Math.Ceiling((double)result.RecordCount / pageSize);

return result;
}

/// <summary>
/// Paginate IQueryable of <typeparamref name="T"/>
/// with given pageNumber and pageSize
/// </summary>
public static async Task<PagedEntityCollection<T>> PaginateAsync<T>(this IQueryable<T> query, uint pageNumber, uint pageSize)
{
if (pageNumber == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageNumber));

if (pageSize == 0)
throw new ArgumentException("Must be greater than zero.", nameof(pageSize));

var result = new PagedEntityCollection<T>
{
CurrentPage = pageNumber,
PageSize = pageSize,
RecordCount = (uint) await query.CountAsync(),
Results = await query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToListAsync()
};

result.PageCount = (uint)Math.Ceiling((double)result.RecordCount / pageSize);

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Sieve.Exceptions;

namespace BSN.Commons.Orm.EntityFrameworkCore.Extensions
{
/// <summary>
/// Add some extensions to make Sieve more easy to use
/// </summary>
public static class SieveExtensions
{
/// <summary>
/// Ordered extract message from <see cref="SieveException"/> based on inner exceptions
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
public static string ExtractMessage(this SieveException ex)
{
string message = ex.InnerException?.InnerException?.Message;

message = message ?? ex.InnerException?.Message;
message = message ?? ex.Message;

return message;
}
}
}
74 changes: 74 additions & 0 deletions Source/BSN.Commons/Infrastructure/IDynamicFilterableRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Linq.Expressions;

namespace BSN.Commons.Infrastructure
{
/// <summary>
/// Add Dynamic filterable query ability to <see cref="IRepository{T}"/> pattern"
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IDynamicFilterableRepository<T> : IRepository<T> where T : class
{
/// <summary>
/// Get list of objects based on <paramref name="filters"/>
/// </summary>
/// <remarks>
/// This method retrieve all objects based on filters and apply pagination.
/// So to retrieve all objects that matched with filters, you need iterate all pages.
/// </remarks>
/// <param name="filters">
/// is a comma-delimited list of <c>{Name}{Operator}{Value}</c> where
/// <list type="bullet">
/// <item>
/// <description>
/// <c>{Name}</c> is the name of a property with the Sieve attribute or the name of a custom filter method for <c>TEntity</c>
/// <list type="bullet">
/// <item>
/// <description>
/// You can also have multiple names (for OR logic) by enclosing them in brackets and using a pipe delimiter,
/// eg. <c> (LikeCount|CommentCount)>10 </c> asks if LikeCount or <c> CommentCount is >10 </c>
/// </description>
/// </item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description>
/// <c>{Operator}</c> is one of the Operators
/// </description>
/// </item>
/// <item>
/// <description>
/// <c>{Value}</c> is the value to use for filtering
/// <list type="bullet">
/// <item>
/// <description>
/// You can also have multiple values (for OR logic) by using a pipe delimiter,
/// eg. <c>Title@=new|hot</c> will return posts with titles that contain the text "new" or "hot"
/// </description>
/// </item>
/// </list>
/// </description>
/// </item>
/// </list>
/// </param>
/// <param name="sorts">is a comma-delimited ordered list of property names to sort by. Adding a <c>-</c>before the name switches to sorting descendingly.</param>
/// <param name="pageNumber">is the number of page to return</param>
/// <param name="pageSize">is the number of items returned per page</param>
/// <returns>Paginated list of matched objects based on filters</returns>
PagedEntityCollection<T> GetMany(string filters, string sorts, uint pageNumber, uint pageSize);

/// <summary>
/// Get list of objects based on <paramref name="filters"/> in scope of <paramref name="where"/> expression
/// <see cref="GetMany(string, string, uint, uint)"/>
/// </summary>
/// <param name="where">A function to test each element for a condition.</param>
/// <param name="filters"><inheritdoc cref="GetMany(string, string, uint, uint)" path='/param[@name="filters"]'/></param>
/// <param name="sorts"><inheritdoc cref="GetMany(string, string, uint, uint)" path='/param[@name="sorts"]'/></param>
/// <param name="pageNumber"><inheritdoc cref="GetMany(string, string, uint, uint)" path='/param[@name="pageNumber"]'/></param>
/// <param name="pageSize"><inheritdoc cref="GetMany(string, string, uint, uint)" path='/param[@name="pageSize"]'/></param>
/// <returns>Paginated list of matched objects based on <paramref name="filters"/> in scope of <paramref name="where"/> expression</returns>
PagedEntityCollection<T> GetMany(Expression<Func<T, bool>> where, string filters, string sorts, uint pageNumber, uint pageSize);
}
}

0 comments on commit 305cb60

Please sign in to comment.